├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml └── runConfigurations.xml ├── LICENSE ├── PRIVACY.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── themarpe │ │ └── openthermalcamera │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ └── themarpe │ │ │ └── openthermalcamera │ │ │ ├── Cobs.java │ │ │ ├── CoreApp.java │ │ │ ├── FixedAspectRatioFrameLayout.java │ │ │ ├── GalleryActivity.java │ │ │ ├── IRPicture.java │ │ │ ├── IRView.java │ │ │ ├── ImageFileFilter.java │ │ │ ├── MLX90640.java │ │ │ ├── MainActivity.java │ │ │ ├── OTC.java │ │ │ ├── OTCFileFilter.java │ │ │ ├── Palette │ │ │ ├── DarkHotPalette.java │ │ │ ├── RainbowPalette.java │ │ │ ├── ThermalPalette.java │ │ │ └── WhiteHotPalette.java │ │ │ ├── Protocol.java │ │ │ ├── SettingsActivity.java │ │ │ ├── SettingsFragment.java │ │ │ ├── SnapOnScrollListener.java │ │ │ └── UsbService.java │ └── res │ │ ├── drawable │ │ ├── arrow_back.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_baseline_delete_outline_24px.xml │ │ ├── ic_baseline_image_search_24px.xml │ │ ├── ic_baseline_share_24px.xml │ │ ├── ic_camera.xml │ │ ├── ic_gallery.xml │ │ ├── ic_insert_otc_ani_frame_1.xml │ │ ├── ic_insert_otc_ani_frame_2.xml │ │ ├── ic_settings.xml │ │ └── insert_otc_animation.xml │ │ ├── layout │ │ ├── activity_gallery.xml │ │ ├── activity_main.xml │ │ └── activity_settings.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── settings.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── file_provider_paths.xml │ │ ├── otc_bootloader_device.xml │ │ ├── otc_device.xml │ │ └── settings_screen.xml │ └── test │ └── java │ └── com │ └── themarpe │ └── openthermalcamera │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/androidstudio 3 | # Edit at https://www.gitignore.io/?templates=androidstudio 4 | 5 | ### AndroidStudio ### 6 | # Covers files to be ignored for android development using Android Studio. 7 | 8 | # Built application files 9 | *.apk 10 | *.ap_ 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | 23 | # Gradle files 24 | .gradle 25 | .gradle/ 26 | build/ 27 | 28 | # Signing files 29 | .signing/ 30 | 31 | # Local configuration file (sdk path, etc) 32 | local.properties 33 | 34 | # Proguard folder generated by Eclipse 35 | proguard/ 36 | 37 | # Log Files 38 | *.log 39 | 40 | # Android Studio 41 | /*/build/ 42 | /*/local.properties 43 | /*/out 44 | /*/*/build 45 | /*/*/production 46 | captures/ 47 | .navigation/ 48 | *.ipr 49 | *~ 50 | *.swp 51 | 52 | # Android Patch 53 | gen-external-apklibs 54 | 55 | # External native build folder generated in Android Studio 2.2 and later 56 | .externalNativeBuild 57 | 58 | # NDK 59 | obj/ 60 | 61 | # IntelliJ IDEA 62 | *.iml 63 | *.iws 64 | /out/ 65 | 66 | # User-specific configurations 67 | .idea/caches/ 68 | .idea/libraries/ 69 | .idea/shelf/ 70 | .idea/workspace.xml 71 | .idea/tasks.xml 72 | .idea/.name 73 | .idea/compiler.xml 74 | .idea/copyright/profiles_settings.xml 75 | .idea/encodings.xml 76 | .idea/misc.xml 77 | .idea/modules.xml 78 | .idea/scopes/scope_settings.xml 79 | .idea/dictionaries 80 | .idea/vcs.xml 81 | .idea/jsLibraryMappings.xml 82 | .idea/datasources.xml 83 | .idea/dataSources.ids 84 | .idea/sqlDataSources.xml 85 | .idea/dynamic.xml 86 | .idea/uiDesigner.xml 87 | .idea/assetWizardSettings.xml 88 | 89 | # OS-specific files 90 | .DS_Store 91 | .DS_Store? 92 | ._* 93 | .Spotlight-V100 94 | .Trashes 95 | ehthumbs.db 96 | Thumbs.db 97 | 98 | # Legacy Eclipse project files 99 | .classpath 100 | .project 101 | .cproject 102 | .settings/ 103 | 104 | # Mobile Tools for Java (J2ME) 105 | .mtj.tmp/ 106 | 107 | # Package Files # 108 | *.war 109 | *.ear 110 | 111 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 112 | hs_err_pid* 113 | 114 | ## Plugin-specific files: 115 | 116 | # mpeltonen/sbt-idea plugin 117 | .idea_modules/ 118 | 119 | # JIRA plugin 120 | atlassian-ide-plugin.xml 121 | 122 | # Mongo Explorer plugin 123 | .idea/mongoSettings.xml 124 | 125 | # Crashlytics plugin (for Android Studio and IntelliJ) 126 | com_crashlytics_export_strings.xml 127 | crashlytics.properties 128 | crashlytics-build.properties 129 | fabric.properties 130 | 131 | ### AndroidStudio Patch ### 132 | 133 | !/gradle/wrapper/gradle-wrapper.jar 134 | 135 | # End of https://www.gitignore.io/api/androidstudio -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Open Thermal Camera Privacy Policy # 2 | 3 | Open Thermal Camera will use camera sensor in case "Camera Overlay" is enabled, to enable the functionality of thermal overlay. 4 | When taking pictures the application writes to external storage to provide persistant storage of images. 5 | USB Host feature is used to be able to connect with Open Thermal Camera hardware gadget. 6 | 7 | No data is sent to any user controlled servers. 8 | Except for anonymous crash logs (Service provided by Firebase Crashlytics). 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android application for Open Thermal Camera 2 | 3 | Application for displaying, taking and viewing thermal photos. 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'io.fabric' 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.themarpe.openthermalcamera" 7 | minSdkVersion 19 8 | targetSdkVersion 28 9 | versionCode 2 10 | versionName "0.2.0" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | buildToolsVersion '28.0.3' 20 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_1_8 22 | targetCompatibility JavaVersion.VERSION_1_8 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation 'com.github.felHR85:UsbSerial:5.0.0' 28 | implementation 'com.otaliastudios:cameraview:2.0.0-beta04' 29 | implementation 'androidx.appcompat:appcompat:1.1.0' 30 | implementation 'androidx.preference:preference:1.1.0' 31 | 32 | //Firebase core 33 | implementation 'com.google.firebase:firebase-core:17.2.0' 34 | 35 | //Crashlytics 36 | implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' 37 | 38 | } 39 | 40 | apply plugin: 'com.google.gms.google-services' 41 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "222922332991", 4 | "firebase_url": "https://open-thermal-camera-d0df3.firebaseio.com", 5 | "project_id": "open-thermal-camera-d0df3", 6 | "storage_bucket": "open-thermal-camera-d0df3.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:222922332991:android:6d76a51b7f1d7ce9e0918a", 12 | "android_client_info": { 13 | "package_name": "com.themarpe.openthermalcamera" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "222922332991-qh4qn7m730ee4pcrnm36v78j9t53gn7j.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyBqaOZv6LXJh8ivwa0WMnDg3SRB7vq1vLM" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "222922332991-qh4qn7m730ee4pcrnm36v78j9t53gn7j.apps.googleusercontent.com", 32 | "client_type": 3 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | ], 39 | "configuration_version": "1" 40 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/themarpe/openthermalcamera/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.example.erikk.usb8", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 62 | 63 | 68 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/Cobs.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | public class Cobs { 4 | 5 | 6 | public static int encodeDstBufMaxLen(int srcLen){ 7 | return ((srcLen) + (((srcLen) + 253)/254)); 8 | } 9 | 10 | public static int decodeDstBufMaxLen(int srcLen){ 11 | return (((srcLen) == 0) ? 0 : ((srcLen) - 1)); 12 | } 13 | 14 | 15 | public static enum EncodeStatus { 16 | OK, 17 | NULL_POINTER, 18 | OUT_BUFFER_OVERFLOW; 19 | } 20 | 21 | public static class EncodeResult { 22 | int outLen; 23 | EncodeStatus status; 24 | } 25 | 26 | public static enum DecodeStatus { 27 | OK, 28 | NULL_POINTER, 29 | OUT_BUFFER_OVERFLOW, 30 | ZERO_BYTE_IN_INPUT, 31 | INPUT_TOO_SHORT; 32 | } 33 | 34 | public static class DecodeResult { 35 | int outLen; 36 | DecodeStatus status; 37 | } 38 | 39 | public static EncodeResult encode(char[] dst_buf_ptr, int dst_buf_len, char[] src_ptr, int src_len){ 40 | EncodeResult result = new EncodeResult(); 41 | result.outLen = 0; 42 | result.status = EncodeStatus.OK; 43 | 44 | int dst_write_counter = 1; 45 | int dst_code_write_counter = 0; 46 | int dst_buf_end_counter = dst_buf_len; 47 | 48 | int src_ptr_counter = 0; 49 | int src_end_counter = src_len; 50 | 51 | int search_len = 1; 52 | 53 | if(dst_buf_ptr == null || src_ptr == null){ 54 | result.status = EncodeStatus.NULL_POINTER; 55 | return result; 56 | } 57 | 58 | 59 | if (src_len != 0) 60 | { 61 | /* Iterate over the source bytes */ 62 | for (;;) 63 | { 64 | /* Check for running out of output buffer space */ 65 | if (dst_write_counter >= dst_buf_end_counter) 66 | { 67 | result.status = EncodeStatus.OUT_BUFFER_OVERFLOW; 68 | break; 69 | } 70 | 71 | int src_byte = (int) src_ptr[src_ptr_counter++]; 72 | 73 | if (src_byte == 0) 74 | { 75 | /* We found a zero byte */ 76 | dst_buf_ptr[dst_code_write_counter] = (char) (search_len & 0xFF); 77 | dst_code_write_counter = dst_write_counter++; 78 | search_len = 1; 79 | if (src_ptr_counter >= src_end_counter) 80 | { 81 | break; 82 | } 83 | } 84 | else 85 | { 86 | /* Copy the non-zero byte to the destination buffer */ 87 | dst_buf_ptr[dst_write_counter++] = (char) (src_byte & 0xFF); 88 | 89 | search_len++; 90 | if (src_ptr_counter >= src_end_counter) 91 | { 92 | break; 93 | } 94 | if (search_len == 0xFF) 95 | { 96 | /* We have a long string of non-zero bytes, so we need 97 | * to write out a length code of 0xFF. */ 98 | dst_buf_ptr[dst_code_write_counter] = (char) (search_len & 0xFF); 99 | 100 | dst_code_write_counter = dst_write_counter++; 101 | search_len = 1; 102 | } 103 | } 104 | } 105 | } 106 | 107 | /* We've reached the end of the source data (or possibly run out of output buffer) 108 | * Finalise the remaining output. In particular, write the code (length) byte. 109 | * Update the pointer to calculate the final output length. 110 | */ 111 | if (dst_code_write_counter >= dst_buf_end_counter) 112 | { 113 | /* We've run out of output buffer to write the code byte. */ 114 | result.status = EncodeStatus.OUT_BUFFER_OVERFLOW; 115 | dst_write_counter = dst_buf_end_counter; 116 | } 117 | else 118 | { 119 | /* Write the last code (length) byte. */ 120 | dst_buf_ptr[dst_code_write_counter] = (char) (search_len & 0xFF); 121 | } 122 | 123 | /* Calculate the output length, from the value of dst_code_write_ptr */ 124 | result.outLen = dst_write_counter; 125 | 126 | return result; 127 | } 128 | 129 | 130 | 131 | public static DecodeResult decode(char[] dst_buf_ptr, int dst_buf_len, char[] src_ptr, int src_len){ 132 | DecodeResult result = new DecodeResult(); 133 | result.outLen = 0; 134 | result.status = DecodeStatus.OK; 135 | 136 | int src_ptr_counter = 0; 137 | int src_end_counter = src_len; 138 | int dst_buf_end_counter = dst_buf_len; 139 | int dst_write_counter = 0; 140 | int remaining_bytes; 141 | int src_byte; 142 | int i; 143 | int len_code; 144 | 145 | 146 | 147 | /* First, do a NULL pointer check and return immediately if it fails. */ 148 | if ((dst_buf_ptr == null) || (src_ptr == null)) 149 | { 150 | result.status = DecodeStatus.NULL_POINTER; 151 | return result; 152 | } 153 | 154 | if (src_len != 0) 155 | { 156 | for (;;) 157 | { 158 | len_code = (int) src_ptr[src_ptr_counter++]; 159 | if (len_code == 0) 160 | { 161 | result.status = DecodeStatus.ZERO_BYTE_IN_INPUT; 162 | break; 163 | } 164 | len_code--; 165 | 166 | /* Check length code against remaining input bytes */ 167 | remaining_bytes = src_end_counter - src_ptr_counter; 168 | if (len_code > remaining_bytes) 169 | { 170 | result.status = DecodeStatus.INPUT_TOO_SHORT; 171 | len_code = remaining_bytes; 172 | } 173 | 174 | /* Check length code against remaining output buffer space */ 175 | remaining_bytes = dst_buf_end_counter - dst_write_counter; 176 | if (len_code > remaining_bytes) 177 | { 178 | result.status = DecodeStatus.OUT_BUFFER_OVERFLOW; 179 | len_code = remaining_bytes; 180 | } 181 | 182 | for (i = len_code; i != 0; i--) 183 | { 184 | src_byte = (char) (src_ptr[src_ptr_counter++] & 0xFF); 185 | if (src_byte == 0) 186 | { 187 | result.status = DecodeStatus.ZERO_BYTE_IN_INPUT; 188 | } 189 | 190 | dst_buf_ptr[dst_write_counter++] = (char) src_byte; 191 | } 192 | 193 | if (src_ptr_counter >= src_end_counter) 194 | { 195 | break; 196 | } 197 | 198 | /* Add a zero to the end */ 199 | if (len_code != 0xFE) 200 | { 201 | if (dst_write_counter >= dst_buf_end_counter) 202 | { 203 | result.status = DecodeStatus.OUT_BUFFER_OVERFLOW; 204 | break; 205 | } 206 | dst_buf_ptr[dst_write_counter++] = 0; 207 | } 208 | } 209 | } 210 | 211 | result.outLen = dst_write_counter; 212 | 213 | return result; 214 | } 215 | 216 | 217 | 218 | } -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/CoreApp.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.app.AlarmManager; 4 | import android.app.Application; 5 | import android.app.PendingIntent; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.SharedPreferences; 9 | import android.preference.PreferenceManager; 10 | import android.util.Log; 11 | 12 | import com.crashlytics.android.Crashlytics; 13 | import com.crashlytics.android.core.CrashlyticsCore; 14 | 15 | import io.fabric.sdk.android.Fabric; 16 | import io.fabric.sdk.android.InitializationCallback; 17 | 18 | public class CoreApp extends Application { 19 | 20 | 21 | private static Thread.UncaughtExceptionHandler mDefaultUEH = null; 22 | private Thread.UncaughtExceptionHandler mCaughtExceptionHandler = new Thread.UncaughtExceptionHandler() { 23 | 24 | @Override 25 | public void uncaughtException(Thread t, Throwable throwable) { 26 | //If this exception comes from CameraView, ignore, disable camera and make a toast 27 | if(throwable.getStackTrace()[0].getClassName().equals("android.hardware.Camera")){ 28 | SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); 29 | SharedPreferences.Editor editor = preferences.edit(); 30 | try { 31 | editor.putBoolean("overlay_enabled", false); 32 | editor.putBoolean("cameraview_crashed", true); 33 | editor.commit(); 34 | } catch (Throwable everything){ 35 | Log.w("ExceptionHandler", "putting boolean to preferences crashed... " + everything.getMessage()); 36 | } 37 | Log.w("ExceptionHandler", "CameraView error caught and disabled RGB overlay: " + throwable.getMessage()); 38 | 39 | //try to rerun mainactivity 40 | Intent mStartActivity = new Intent(CoreApp.this, MainActivity.class); 41 | int mPendingIntentId = 123456; 42 | PendingIntent mPendingIntent = PendingIntent.getActivity(CoreApp.this, mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT); 43 | AlarmManager mgr = (AlarmManager) CoreApp.this.getSystemService(Context.ALARM_SERVICE); 44 | mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 500, mPendingIntent); 45 | } 46 | 47 | //let crashlytics handle the exception after that 48 | mDefaultUEH.uncaughtException(t, throwable); 49 | 50 | } 51 | 52 | }; 53 | 54 | @Override 55 | public void onCreate() { 56 | super.onCreate(); 57 | 58 | mDefaultUEH = Thread.getDefaultUncaughtExceptionHandler(); 59 | Thread.setDefaultUncaughtExceptionHandler(mCaughtExceptionHandler); 60 | 61 | //handle CameraView crash and still report to crashlytics 62 | CrashlyticsCore core = new CrashlyticsCore.Builder() 63 | //.disabled(BuildConfig.DEBUG) 64 | .build(); 65 | Fabric.with(new Fabric.Builder(this).kits(new Crashlytics.Builder() 66 | .core(core) 67 | .build()) 68 | .initializationCallback(new InitializationCallback() { 69 | @Override 70 | public void success(Fabric fabric) { 71 | //Thread.setDefaultUncaughtExceptionHandler(mCaughtExceptionHandler); 72 | if(mDefaultUEH != Thread.getDefaultUncaughtExceptionHandler()){ 73 | mDefaultUEH = Thread.getDefaultUncaughtExceptionHandler(); 74 | Thread.setDefaultUncaughtExceptionHandler(mCaughtExceptionHandler); 75 | } 76 | } 77 | @Override 78 | public void failure(Exception e) { 79 | 80 | } 81 | }) 82 | .build()); 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/FixedAspectRatioFrameLayout.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.FrameLayout; 6 | 7 | public class FixedAspectRatioFrameLayout extends FrameLayout 8 | { 9 | private int mAspectRatioWidth; 10 | private int mAspectRatioHeight; 11 | 12 | public FixedAspectRatioFrameLayout(Context context) 13 | { 14 | super(context); 15 | } 16 | 17 | public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs) 18 | { 19 | super(context, attrs); 20 | 21 | init(context, attrs); 22 | } 23 | 24 | public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs, int defStyle) 25 | { 26 | super(context, attrs, defStyle); 27 | 28 | init(context, attrs); 29 | } 30 | 31 | private void init(Context context, AttributeSet attrs) 32 | { 33 | mAspectRatioWidth = 3; 34 | mAspectRatioHeight = 4; 35 | } 36 | // **overrides** 37 | 38 | @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) 39 | { 40 | int originalWidth = MeasureSpec.getSize(widthMeasureSpec); 41 | 42 | int originalHeight = MeasureSpec.getSize(heightMeasureSpec); 43 | 44 | int calculatedHeight = originalWidth * mAspectRatioHeight / mAspectRatioWidth; 45 | 46 | int finalWidth, finalHeight; 47 | 48 | if (calculatedHeight > originalHeight) 49 | { 50 | finalWidth = originalHeight * mAspectRatioWidth / mAspectRatioHeight; 51 | finalHeight = originalHeight; 52 | } 53 | else 54 | { 55 | finalWidth = originalWidth; 56 | finalHeight = calculatedHeight; 57 | } 58 | 59 | super.onMeasure( 60 | MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), 61 | MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)); 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/GalleryActivity.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.content.DialogInterface; 4 | import android.content.Intent; 5 | import android.graphics.BitmapFactory; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.os.Environment; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.FrameLayout; 13 | import android.widget.ImageView; 14 | import android.widget.LinearLayout; 15 | import android.widget.RelativeLayout; 16 | import android.widget.TextView; 17 | import android.widget.Toast; 18 | 19 | import java.io.File; 20 | import java.io.FileFilter; 21 | import java.text.SimpleDateFormat; 22 | import java.util.Arrays; 23 | import java.util.Date; 24 | import java.util.Locale; 25 | 26 | import androidx.annotation.NonNull; 27 | import androidx.appcompat.app.AlertDialog; 28 | import androidx.appcompat.app.AppCompatActivity; 29 | import androidx.core.app.ShareCompat; 30 | import androidx.core.content.FileProvider; 31 | import androidx.recyclerview.widget.LinearLayoutManager; 32 | import androidx.recyclerview.widget.PagerSnapHelper; 33 | import androidx.recyclerview.widget.RecyclerView; 34 | 35 | import com.themarpe.openthermalcamera.BuildConfig; 36 | import com.themarpe.openthermalcamera.R; 37 | 38 | 39 | public class GalleryActivity extends AppCompatActivity { 40 | 41 | private static final String TAG = "GalleryActivity"; 42 | 43 | /* 44 | This is going to be a horizontal scroll list 45 | When scrolling to a next item, the now missing rightmost item will be buffered 46 | The list of items is going to be scanned from sdcard/pictures/openthermalcamera 47 | */ 48 | SimpleDateFormat filenameDateFormat; 49 | ImageAdapter imageAdapter; 50 | RecyclerView recyclerView; 51 | File folderToScan; 52 | FileFilter imageFilter; 53 | File[] imageFiles = new File[0]; 54 | TextView txtDatetime; 55 | int currentSnapPosition = 0; 56 | RelativeLayout noPicturesFound; 57 | FrameLayout frameLayout; 58 | 59 | protected void onCreate(Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | setContentView(R.layout.activity_gallery); 62 | 63 | //get framelayout 64 | frameLayout = findViewById(R.id.frameLayout); 65 | 66 | //get layoutNoPicturesFound 67 | noPicturesFound = findViewById(R.id.layoutNoPicturesFound); 68 | 69 | txtDatetime = findViewById(R.id.txtDatetime); 70 | imageFilter = new ImageFileFilter(); 71 | folderToScan = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "/OpenThermalCamera"); 72 | 73 | 74 | //get recycler view 75 | //create N item horizontal scroll list 76 | imageAdapter = new ImageAdapter(); 77 | recyclerView = findViewById(R.id.recyclerView); 78 | recyclerView.setAdapter(imageAdapter); 79 | 80 | LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false); 81 | 82 | PagerSnapHelper pagerSnapHelper = new PagerSnapHelper(); 83 | pagerSnapHelper.attachToRecyclerView(recyclerView); 84 | 85 | filenameDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); 86 | 87 | recyclerView.addOnScrollListener(new SnapOnScrollListener(pagerSnapHelper, SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL_STATE_IDLE, position -> { 88 | //position has changed 89 | //set text to timestamp 90 | setDatetime(position); 91 | currentSnapPosition = position; 92 | })); 93 | 94 | recyclerView.setLayoutManager(layoutManager); 95 | 96 | //back button 97 | findViewById(R.id.btnBack).setOnClickListener((View v) -> finish()); 98 | 99 | //delete button 100 | findViewById(R.id.btnDelete).setOnClickListener((View v) -> deleteCurrentImage()); 101 | 102 | //share button 103 | findViewById(R.id.btnShare).setOnClickListener((View v) -> { 104 | 105 | final Uri theUri = FileProvider.getUriForFile(this, 106 | BuildConfig.APPLICATION_ID + ".provider", 107 | imageFiles[currentSnapPosition]); 108 | 109 | Intent shareIntent = ShareCompat.IntentBuilder.from(this) 110 | .setType("image/png") 111 | .setStream(theUri) 112 | .getIntent(); 113 | shareIntent.setData(theUri); 114 | shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 115 | startActivity(Intent.createChooser(shareIntent, "Share image:")); 116 | 117 | }); 118 | 119 | checkIfAnyImagesAvailable(); 120 | 121 | } 122 | 123 | private void deleteCurrentImage(){ 124 | new AlertDialog.Builder(this) 125 | .setTitle("Delete") 126 | .setMessage("Are you sure to delete this image?") 127 | .setPositiveButton("Delete", (DialogInterface di, int which) -> { 128 | //delete currentSnapPosition image 129 | //get File 130 | boolean wasImgFileDeleted = false; 131 | try { 132 | //get otc file aswell 133 | String otcFilename = imageFiles[currentSnapPosition].getName().split("\\.")[0] + ".otc"; 134 | 135 | File parent = imageFiles[currentSnapPosition].getParentFile(); 136 | File otcFile = new File(parent, otcFilename); 137 | File imgFile = imageFiles[currentSnapPosition]; 138 | 139 | //try and delete otc and img files 140 | otcFile.delete(); 141 | wasImgFileDeleted = imgFile.delete(); 142 | 143 | } catch (Exception ex){ 144 | Toast.makeText(this, "Unexpected error while deleting image", Toast.LENGTH_SHORT); 145 | Log.d(TAG, "Error while trying to delete img and otc files: " + ex.getLocalizedMessage()); 146 | } 147 | 148 | //notify imageadapter of this change 149 | if(wasImgFileDeleted){ 150 | imageAdapter.notifyItemRemoved(currentSnapPosition); 151 | } 152 | 153 | di.dismiss(); 154 | }) 155 | .setNegativeButton("Cancel", (DialogInterface di, int which) -> { 156 | di.dismiss(); 157 | }) 158 | .create().show(); 159 | } 160 | 161 | private void setDatetime(int position){ 162 | Log.d(TAG, "Setting datetime of position: " + position); 163 | if(position != RecyclerView.NO_POSITION && imageFiles != null && position < imageFiles.length ){ 164 | //try to parse the timestamp 165 | try { 166 | String filename = imageFiles[position].getName(); 167 | filename = filename.replaceFirst("IMG_", ""); 168 | String datetime = filename.split("\\.")[0]; 169 | 170 | Date date = filenameDateFormat.parse(datetime); 171 | 172 | SimpleDateFormat humanFrendlyDatetimeFormat = new SimpleDateFormat("HH:mm MMMM dd, yyyy", Locale.getDefault()); 173 | String displayDatetime = humanFrendlyDatetimeFormat.format(date); 174 | 175 | txtDatetime.setText(displayDatetime); 176 | 177 | } catch (Exception a){ 178 | txtDatetime.setText("Date and time unknown."); 179 | Log.d(TAG, "Exception when parsing datetime: " + a.getLocalizedMessage()); 180 | } 181 | } 182 | } 183 | 184 | 185 | @Override 186 | protected void onStart() { 187 | super.onStart(); 188 | } 189 | 190 | @Override 191 | protected void onResume() { 192 | super.onResume(); 193 | 194 | if(folderToScan.isDirectory()){ 195 | File[] tmp = folderToScan.listFiles(imageFilter); 196 | //compare if change has happened and notify recyclerview 197 | if(!Arrays.equals(tmp, imageFiles)){ 198 | imageFiles = tmp; 199 | 200 | //sort images by datetime (newest first) 201 | Arrays.sort(imageFiles, (File a, File b) -> b.getName().compareTo(a.getName())); 202 | 203 | imageAdapter.notifyDataSetChanged(); 204 | } 205 | } 206 | 207 | checkIfAnyImagesAvailable(); 208 | 209 | setDatetime(0); 210 | 211 | } 212 | 213 | private void checkIfAnyImagesAvailable(){ 214 | //check if dataset is empty 215 | if(imageFiles.length == 0){ 216 | noPicturesFound.setVisibility(View.VISIBLE); 217 | recyclerView.setVisibility(View.GONE); 218 | frameLayout.bringChildToFront(noPicturesFound); 219 | 220 | //disable all buttons 221 | 222 | findViewById(R.id.btnInspect).setAlpha(0.2f); 223 | findViewById(R.id.btnInspect).setEnabled(false); 224 | findViewById(R.id.btnShare).setAlpha(0.2f); 225 | findViewById(R.id.btnShare).setEnabled(false); 226 | findViewById(R.id.btnDelete).setAlpha(0.2f); 227 | findViewById(R.id.btnDelete).setEnabled(false); 228 | 229 | } else { 230 | noPicturesFound.setVisibility(View.INVISIBLE); 231 | recyclerView.setVisibility(View.VISIBLE); 232 | frameLayout.bringChildToFront(recyclerView); 233 | 234 | //enable all buttons 235 | findViewById(R.id.btnInspect).setAlpha(1.0f); 236 | findViewById(R.id.btnInspect).setEnabled(true); 237 | findViewById(R.id.btnShare).setAlpha(1.0f); 238 | findViewById(R.id.btnShare).setEnabled(true); 239 | findViewById(R.id.btnDelete).setAlpha(1.0f); 240 | findViewById(R.id.btnDelete).setEnabled(true); 241 | 242 | } 243 | 244 | } 245 | 246 | 247 | @Override 248 | protected void onPause() { 249 | super.onPause(); 250 | } 251 | 252 | @Override 253 | protected void onStop() { 254 | super.onStop(); 255 | } 256 | 257 | @Override 258 | protected void onDestroy() { 259 | super.onDestroy(); 260 | } 261 | 262 | 263 | public class ImageAdapter extends RecyclerView.Adapter{ 264 | 265 | @NonNull 266 | @Override 267 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 268 | LinearLayout parentLayout = new LinearLayout(GalleryActivity.this); 269 | 270 | ImageView imageView = new ImageView(GalleryActivity.this); 271 | 272 | //create image of same size as recycler view 273 | View recyclerView = findViewById(R.id.recyclerView); 274 | imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); 275 | imageView.setTag("image"); 276 | 277 | //give it margin 278 | LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(recyclerView.getWidth(), recyclerView.getHeight()); 279 | layoutParams.setMargins(20,0,20,0); 280 | 281 | parentLayout.addView(imageView, layoutParams); 282 | 283 | return new ViewHolder(parentLayout); 284 | } 285 | 286 | @Override 287 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 288 | //load the image 289 | holder.image.setImageBitmap(BitmapFactory.decodeFile(imageFiles[position].getAbsolutePath())); 290 | } 291 | 292 | @Override 293 | public int getItemCount() { 294 | return imageFiles.length; 295 | } 296 | 297 | class ViewHolder extends RecyclerView.ViewHolder{ 298 | private ImageView image; 299 | public ViewHolder(View linearLayoutView) { 300 | super(linearLayoutView); 301 | image = (ImageView) linearLayoutView.findViewWithTag("image"); 302 | } 303 | } 304 | } 305 | 306 | 307 | 308 | } 309 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/IRPicture.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Point; 5 | 6 | import com.themarpe.openthermalcamera.Palette.RainbowPalette; 7 | import com.themarpe.openthermalcamera.Palette.ThermalPalette; 8 | 9 | public class IRPicture { 10 | 11 | private ThermalPalette thermalPalette; 12 | private double[][] tempData; 13 | Bitmap irBitmap; 14 | 15 | 16 | private static final int SPECTRUM_RESOLUTION = 256; 17 | Bitmap spectrumBitmap; 18 | 19 | protected boolean customTemperatureRange = false; 20 | 21 | protected double customMinTemperature; 22 | protected double customMaxTemperature; 23 | 24 | private int width, height; 25 | private double minTemp, maxTemp, maxTempSearchArea; 26 | private int searchAreaSize=3; 27 | public void setSeachAreaSize(int size){searchAreaSize=size;} 28 | 29 | protected boolean dynamicRange; 30 | protected float dynamicRangeMinDifference = 0; 31 | 32 | private Point maxTempPixel = new Point(0,0); 33 | private Point maxTempPixelInSearchArea = new Point(0,0); 34 | 35 | IRPicture(IRPicture toCopy){ 36 | 37 | this(toCopy.width, toCopy.height); 38 | tempData = toCopy.tempData.clone(); 39 | thermalPalette = toCopy.thermalPalette; 40 | irBitmap = Bitmap.createBitmap(toCopy.irBitmap); 41 | customMaxTemperature = toCopy.customMaxTemperature; 42 | customMinTemperature = toCopy.customMinTemperature; 43 | minTemp = toCopy.minTemp; 44 | maxTemp = toCopy.maxTemp; 45 | dynamicRange = toCopy.dynamicRange; 46 | dynamicRangeMinDifference = toCopy.dynamicRangeMinDifference; 47 | maxTempPixel = new Point(maxTempPixel); 48 | 49 | spectrumBitmap = Bitmap.createBitmap(toCopy.spectrumBitmap); 50 | 51 | } 52 | 53 | IRPicture(int width, int height){ 54 | this(width, height, new RainbowPalette()); 55 | } 56 | 57 | IRPicture(int width, int height, ThermalPalette thermalPalette){ 58 | 59 | this.width = width; 60 | this.height = height; 61 | tempData = new double[height][width]; 62 | this.thermalPalette = thermalPalette; 63 | irBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 64 | 65 | spectrumBitmap = Bitmap.createBitmap(SPECTRUM_RESOLUTION, 1, Bitmap.Config.ARGB_8888); 66 | } 67 | 68 | public void updateTemperatureData(double[][] temperatureData){ 69 | minTemp = temperatureData[0][0]; 70 | maxTemp = temperatureData[0][0]; 71 | 72 | maxTempPixel.set(0,0); //TODO: why??? 73 | for(int i = 0; i maxTemp){ 80 | maxTemp = temperatureData[i][j]; 81 | maxTempPixel.set(j, i); 82 | } 83 | } 84 | } 85 | tempData = temperatureData; 86 | 87 | //Find max temp pixel in searchArea 88 | maxTempSearchArea=-40; 89 | int xMid = temperatureData.length/2; 90 | for(int i = xMid-searchAreaSize; i< xMid + searchAreaSize; i++){ 91 | int yMid = temperatureData[i].length/2; 92 | 93 | for(int j = yMid-searchAreaSize; j maxTempSearchArea){ 96 | maxTempSearchArea = temperatureData[i][j]; 97 | maxTempPixelInSearchArea.set(j, i); 98 | } 99 | } 100 | } 101 | 102 | // min difference setting 103 | double tmpMinTemp = minTemp, tmpMaxTemp = maxTemp; 104 | double avgTemp = (minTemp+maxTemp) / 2.0; 105 | if(dynamicRangeMinDifference > 0 && dynamicRangeMinDifference > (maxTemp - minTemp) ){ 106 | tmpMinTemp = avgTemp - (dynamicRangeMinDifference / 2.0); 107 | tmpMaxTemp = avgTemp + (dynamicRangeMinDifference / 2.0); 108 | } 109 | 110 | //convert temperatures 111 | for(int y = 0; y < OTC.IR_HEIGHT; y++){ 112 | for(int x = 0 ; x invalidate()); 120 | 121 | } 122 | 123 | 124 | public void update() { 125 | 126 | if(irPicture == null) return; 127 | 128 | setImageBitmap(irPicture.getBitmap()); 129 | 130 | //get max temp 131 | if(!searchAreaEnabled){ //Display max temp from IR picture 132 | xyMarkerVector[0] = irPicture.getMaxTemperaturePixel().x; 133 | xyMarkerVector[1] = irPicture.getMaxTemperaturePixel().y; 134 | } 135 | else{ //Display max temp from IR picture INSIDE the searchArea! 136 | xyMarkerVector[0] = irPicture.getMaxTemperaturePixelInSearchArea().x; 137 | xyMarkerVector[1] = irPicture.getMaxTemperaturePixelInSearchArea().y; 138 | } 139 | 140 | //move to middle of the pixel 141 | xyMarkerVector[0] += 0.5f; 142 | xyMarkerVector[1] += 0.5f; 143 | 144 | //rotate 145 | //rotate and scale image and marker 146 | irViewMatrix.reset(); 147 | 148 | //rotate 149 | irViewMatrix.preRotate(90f, 0, 0); 150 | //move 151 | irViewMatrix.postTranslate(OTC.IR_HEIGHT, 0); 152 | //scale 153 | irViewMatrix.postScale(((float) getWidth())/OTC.IR_HEIGHT, ((float) getHeight()) / OTC.IR_WIDTH, 0,0); 154 | 155 | //apply to marker 156 | irViewMatrix.mapPoints(xyMarkerVector); 157 | 158 | setScaleType(ImageView.ScaleType.MATRIX); //required 159 | //apply to ir image 160 | setImageMatrix(irViewMatrix); 161 | 162 | //new frame available, invalidate irView 163 | invalidate(); 164 | } 165 | 166 | 167 | 168 | public void startFlashAnimation(){ 169 | effectRectangle.set(0,0, getWidth(), getHeight()); 170 | effectFlashAnimation.end(); 171 | effectFlashAnimation.start(); 172 | } 173 | 174 | private boolean isFlashAnimationInProgress(){ 175 | if(effectFlashAnimation == null){ 176 | return false; 177 | } else { 178 | return effectFlashAnimation.isRunning(); 179 | } 180 | } 181 | 182 | 183 | @Override 184 | protected void onDraw(Canvas canvas) { 185 | 186 | if(filterBitmap){ 187 | canvas.setDrawFilter(filterPaint); 188 | } else { 189 | canvas.setDrawFilter(noFilterPaint); 190 | } 191 | 192 | super.onDraw(canvas); 193 | 194 | canvas.setDrawFilter(null); 195 | 196 | if(maxMarkerEnabled) { 197 | 198 | float markerSize = getHeight() * maxMarkerScale; 199 | float x = xyMarkerVector[0]; 200 | float y = xyMarkerVector[1]; 201 | 202 | //draw max temp pointer 203 | canvas.drawLine(x, y - markerSize, x, y - markerSize / 4.0f, maxMarkerPaint); 204 | canvas.drawLine(x, y + markerSize, x, y + markerSize / 4.0f, maxMarkerPaint); 205 | canvas.drawLine(x - markerSize, y, x - markerSize / 4.0f, y, maxMarkerPaint); 206 | canvas.drawLine(x + markerSize, y, x + markerSize / 4.0f, y, maxMarkerPaint); 207 | 208 | } 209 | if(searchAreaEnabled){ 210 | float yy = getHeight()/ OTC.IR_WIDTH; 211 | float xx = getWidth()/OTC.IR_HEIGHT; 212 | canvas.drawRect(xx*(OTC.IR_HEIGHT/2-searchAreaSize),yy*(OTC.IR_WIDTH/2-searchAreaSize),xx*(OTC.IR_HEIGHT/2+searchAreaSize), yy*(OTC.IR_WIDTH/2+searchAreaSize), searchAreaPaint); 213 | } 214 | 215 | //take a picture effect 216 | if(isFlashAnimationInProgress()){ 217 | canvas.drawRect(effectRectangle, effectPaint); 218 | } 219 | 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/ImageFileFilter.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import java.io.File; 4 | import java.io.FileFilter; 5 | 6 | public class ImageFileFilter implements FileFilter { 7 | 8 | private final String[] okFileExtensions = new String[] {"jpeg", "jpg", "png", "gif"}; 9 | 10 | public boolean accept(File file) { 11 | for (String extension : okFileExtensions) { 12 | if (file.getName().toLowerCase().endsWith(extension)) { 13 | return true; 14 | } 15 | } 16 | return false; 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.Manifest; 4 | import android.annotation.SuppressLint; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.SharedPreferences; 8 | import android.content.pm.PackageManager; 9 | import android.graphics.Bitmap; 10 | import android.graphics.BitmapFactory; 11 | import android.graphics.Canvas; 12 | import android.graphics.Matrix; 13 | import android.graphics.Paint; 14 | import android.graphics.drawable.AnimationDrawable; 15 | import android.hardware.SensorManager; 16 | import android.media.MediaScannerConnection; 17 | import android.os.Bundle; 18 | import android.os.Environment; 19 | import android.preference.PreferenceManager; 20 | import android.util.Base64; 21 | import android.util.Log; 22 | import android.view.Display; 23 | import android.view.OrientationEventListener; 24 | import android.view.Surface; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | import android.view.WindowManager; 28 | import android.view.animation.Animation; 29 | import android.view.animation.LinearInterpolator; 30 | import android.view.animation.RotateAnimation; 31 | import android.widget.ImageButton; 32 | import android.widget.ImageView; 33 | import android.widget.TextView; 34 | import android.widget.Toast; 35 | 36 | import androidx.appcompat.app.AppCompatActivity; 37 | import androidx.core.app.ActivityCompat; 38 | import androidx.core.content.ContextCompat; 39 | 40 | import com.otaliastudios.cameraview.Audio; 41 | import com.otaliastudios.cameraview.CameraListener; 42 | import com.otaliastudios.cameraview.CameraView; 43 | import com.otaliastudios.cameraview.Gesture; 44 | import com.otaliastudios.cameraview.GestureAction; 45 | import com.otaliastudios.cameraview.PictureResult; 46 | 47 | import org.json.JSONArray; 48 | import org.json.JSONException; 49 | import org.json.JSONObject; 50 | 51 | import java.io.ByteArrayOutputStream; 52 | import java.io.File; 53 | import java.io.FileOutputStream; 54 | import java.io.IOException; 55 | import java.io.OutputStreamWriter; 56 | import java.text.DecimalFormat; 57 | import java.text.SimpleDateFormat; 58 | import java.util.ArrayList; 59 | import java.util.Calendar; 60 | import java.util.Date; 61 | 62 | import com.themarpe.openthermalcamera.Palette.ThermalPalette; 63 | 64 | import com.themarpe.openthermalcamera.R; 65 | 66 | 67 | public class MainActivity extends AppCompatActivity { 68 | 69 | private static final String TAG = "MainActivity"; 70 | 71 | IRView irView; 72 | IRPicture irPicture = null; 73 | CameraView camera; 74 | 75 | ImageView imgTempSpectrum; 76 | 77 | ImageView imgInsertOtc = null; 78 | AnimationDrawable insertOtcAni = null; 79 | 80 | TextView textMinIrTemp; 81 | TextView textMaxIrTemp; 82 | TextView textAvgIrTemp; 83 | 84 | OTC otc = null; 85 | 86 | ArrayList viewsToRotate = new ArrayList<>(); 87 | 88 | LayoutRotateOnOrientation layoutRotateOnOrientation = null; 89 | 90 | 91 | @Override 92 | protected void onDestroy() { 93 | super.onDestroy(); 94 | 95 | layoutRotateOnOrientation.disable(); 96 | layoutRotateOnOrientation = null; 97 | 98 | } 99 | 100 | @SuppressLint("ClickableViewAccessibility") 101 | @Override 102 | protected void onCreate(Bundle savedInstanceState) { 103 | super.onCreate(savedInstanceState); 104 | setContentView(R.layout.activity_main); 105 | 106 | //Get shared preferences (Settings) 107 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); 108 | 109 | //Toast if runtime exception happened 110 | if(sharedPreferences.getBoolean("cameraview_crashed", false)){ 111 | Toast.makeText(this, "CameraView error caught, disabled RGB overlay", Toast.LENGTH_SHORT).show(); 112 | sharedPreferences.edit().remove("cameraview_crashed").apply(); 113 | } 114 | 115 | //CAMERA 116 | camera = findViewById(R.id.camera); 117 | //first set cameraview enabled/disabled then adjust settings 118 | boolean overlay_enabled = sharedPreferences.getBoolean("overlay_enabled", false); 119 | setCameraViewEnabled(overlay_enabled); 120 | camera.setAudio(Audio.OFF); 121 | //camera.mapGesture(Gesture.PINCH, GestureAction.ZOOM); // Pinch to zoom! 122 | camera.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); // Tap to focus! 123 | camera.addCameraListener(new CameraListener() { 124 | @Override 125 | public void onPictureTaken(PictureResult pic) { 126 | super.onPictureTaken(pic); 127 | saveTakenPicture(takeIrPicture(), pic, layoutRotateOnOrientation.getCurrentOrientation()); 128 | } 129 | }); 130 | 131 | //This activity should keep the screen on! 132 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 133 | 134 | //request for permissions 135 | ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE},1); 136 | 137 | //irView (extended ImageView) 138 | irView = findViewById(R.id.irView); 139 | irPicture = new IRPicture(OTC.IR_WIDTH, OTC.IR_HEIGHT); 140 | irView.setIRPicture(irPicture); 141 | 142 | //rotate layout listener 143 | layoutRotateOnOrientation = new LayoutRotateOnOrientation(); 144 | 145 | //get min max avg temp labels 146 | textMinIrTemp = findViewById(R.id.txtTempMin); 147 | textMaxIrTemp = findViewById(R.id.txtTempMax); 148 | textAvgIrTemp = findViewById(R.id.txtTempMid); 149 | 150 | // get imgTempSpectrum 151 | imgTempSpectrum = findViewById(R.id.imgTempSpectrum); 152 | imgTempSpectrum.setImageBitmap(irPicture.getSpectrumBitmap()); 153 | 154 | //get all views to rotate 155 | getAllRotatableViews(viewsToRotate, (ViewGroup) findViewById(R.id.layoutActivityMain)); 156 | 157 | // insert otc animation 158 | imgInsertOtc = findViewById(R.id.imgInsertOtc); 159 | insertOtcAni = (AnimationDrawable) imgInsertOtc.getDrawable(); 160 | insertOtcAni.setVisible(false, true); 161 | 162 | //OTC 163 | otc = new OTC(this, new OTCStateListener()); 164 | otc.setResponseListener(new OTC.ResponseListener(){ 165 | 166 | @Override 167 | public void onResponsePre(Protocol.RspStruct rsp) { 168 | //do nothing 169 | } 170 | 171 | @Override 172 | public void onResponsePost(Protocol.RspStruct rsp) { 173 | if(rsp.responseCode == Protocol.RSP_GET_FRAME_DATA){ 174 | 175 | //set temperatures and irView 176 | irPicture.updateTemperatureData(otc.getIrTemp()); 177 | irView.update(); 178 | 179 | //new min max avg temps available, update 180 | DecimalFormat df = new DecimalFormat("#.0"); 181 | textMinIrTemp.setText(df.format(otc.getMinIrTemp())); 182 | textMaxIrTemp.setText(df.format(otc.getMaxIrTemp())); 183 | textAvgIrTemp.setText(df.format((otc.getMaxIrTemp() + otc.getMinIrTemp()) / 2.0 )); 184 | 185 | //Display temp spectrum according to template 186 | imgTempSpectrum.setImageBitmap(irView.getIRPicture().getSpectrumBitmap()); 187 | imgTempSpectrum.invalidate(); 188 | 189 | } 190 | } 191 | 192 | }); 193 | 194 | 195 | //take picture button 196 | findViewById(R.id.btnTakePicture).setOnClickListener((View v) -> { 197 | 198 | //flash animation 199 | irView.startFlashAnimation(); 200 | 201 | //check if permissions 202 | if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 203 | != PackageManager.PERMISSION_GRANTED) { 204 | // Permission is not granted 205 | Toast.makeText(MainActivity.this, "No permission to take picture", Toast.LENGTH_SHORT); 206 | return; 207 | } 208 | 209 | //Check if RGB+IR or IR only 210 | if(sharedPreferences.getBoolean("overlay_enabled", false)){ 211 | //RGB+IR 212 | camera.takePictureSnapshot(); 213 | } else { 214 | saveTakenPicture(takeIrPicture(), null, layoutRotateOnOrientation.getCurrentOrientation()); 215 | } 216 | 217 | }); 218 | 219 | findViewById(R.id.btnSettings).setOnClickListener(new View.OnClickListener() { 220 | @Override 221 | public void onClick(View v) { 222 | Intent settingsIntent = new Intent(MainActivity.this, SettingsActivity.class); 223 | startActivity(settingsIntent); 224 | } 225 | }); 226 | 227 | findViewById(R.id.btnGallery).setOnClickListener((View v) -> { 228 | Intent galleryIntent = new Intent(MainActivity.this, GalleryActivity.class); 229 | startActivity(galleryIntent); 230 | }); 231 | 232 | } 233 | 234 | 235 | void setCameraViewEnabled(boolean enabled){ 236 | 237 | if(enabled){ 238 | camera.setLifecycleOwner(this); 239 | camera.setVisibility(View.VISIBLE); 240 | } else { 241 | camera.setVisibility(View.GONE); 242 | } 243 | } 244 | 245 | 246 | IRPicture takeIrPicture(){ 247 | return new IRPicture(irView.getIRPicture()); 248 | } 249 | 250 | void saveTakenPicture(IRPicture irPicture, PictureResult rgbPictureResult, int orientation){ 251 | 252 | try { 253 | 254 | Bitmap fusedImage = Bitmap.createBitmap(irView.getWidth(), irView.getHeight(), Bitmap.Config.ARGB_8888); 255 | Canvas temporaryCanvas = new Canvas(fusedImage); 256 | Paint rgbPaint = new Paint(); 257 | Paint irPaint = new Paint(); 258 | irPaint.setAlpha(irView.getImageAlpha()); 259 | Matrix irTransformationMatrix = irView.getIrViewMatrix(); 260 | Matrix rgbTransformationMatrix = new Matrix(); 261 | 262 | JSONObject otc = new JSONObject(); 263 | 264 | otc.put("orientation", orientation); 265 | 266 | otc.put("fusion", false); 267 | if (rgbPictureResult != null) { 268 | 269 | //prepare and correctly rotate RGB image 270 | 271 | //fuse with IR image 272 | Bitmap rgbBitmap = BitmapFactory.decodeByteArray(rgbPictureResult.getData(), 0, rgbPictureResult.getData().length); 273 | 274 | //rotate accordingly to rotation 275 | //portrait 276 | int finalRotationInDegrees = 0; 277 | 278 | float w = rgbBitmap.getWidth(); 279 | float h = rgbBitmap.getHeight(); 280 | 281 | //not needed (snapshot is of same size as irView) 282 | float sx = 1.0f; 283 | float sy = 1.0f; 284 | 285 | //then rotate in middle 286 | //rgbTransformationMatrix.postTranslate(-w/2.0f, -h/2.0f); 287 | //if width > height -> landscape 288 | // else portrait 289 | if(w > h){ 290 | //scale 291 | rgbTransformationMatrix.preScale(sy, sx,0,0); 292 | rgbTransformationMatrix.postRotate(360 - orientation, w/2.0f,h/2.0f); 293 | } else { 294 | //scale 295 | rgbTransformationMatrix.preScale(sx, sy,0,0); 296 | rgbTransformationMatrix.postRotate(orientation, w/2.0f,h/2.0f); 297 | } 298 | rgbTransformationMatrix.postTranslate((fusedImage.getWidth() - w) / 2.0f , (fusedImage.getHeight() - h)/2.0f); 299 | 300 | 301 | //paint 302 | temporaryCanvas.drawBitmap(rgbBitmap, rgbTransformationMatrix, rgbPaint); 303 | 304 | Log.d(TAG, "Orientation = " + orientation + " rgbPictureResult.size() = " + rgbPictureResult.getSize() +" rgbPictureResult.getRotation() = " + rgbPictureResult.getRotation() + ", rgbBitmap w,h = " + rgbBitmap.getWidth() + "," + rgbBitmap.getHeight()); 305 | 306 | ByteArrayOutputStream rgbJpegCompressed = new ByteArrayOutputStream(); 307 | fusedImage.compress(Bitmap.CompressFormat.JPEG, 90, rgbJpegCompressed); 308 | 309 | //save to JSON 310 | otc.put("fusion", true); 311 | 312 | JSONObject rgbPicture = new JSONObject(); 313 | rgbPicture.put("width", rgbPictureResult.getSize().getWidth()); 314 | rgbPicture.put("height", rgbPictureResult.getSize().getHeight()); 315 | 316 | rgbPicture.put("format", "jpg:base64"); 317 | rgbPicture.put("data", Base64.encodeToString(rgbJpegCompressed.toByteArray(), Base64.NO_WRAP)); 318 | 319 | otc.put("rgb_picture", rgbPicture); 320 | 321 | } 322 | 323 | if (irPicture != null) { 324 | JSONObject ir = new JSONObject(); 325 | 326 | ir.put("width", irPicture.getWidth()); 327 | ir.put("height", irPicture.getHeight()); 328 | ir.put("temperature", new JSONArray(irPicture.getTemperatureData())); 329 | ir.put("dynamic_range", irPicture.dynamicRange); 330 | ir.put("custom_max_temperature", irPicture.customMaxTemperature); 331 | ir.put("custom_min_temperature", irPicture.customMinTemperature); 332 | ir.put("custom_temp_range", irPicture.customTemperatureRange); 333 | ir.put("thermal_palette", irPicture.getThermalPalette().getClass().getName()); 334 | 335 | ByteArrayOutputStream pngCompressed = new ByteArrayOutputStream(); 336 | irPicture.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, pngCompressed); 337 | ir.put("format", "png:base64"); 338 | ir.put("data", Base64.encodeToString(pngCompressed.toByteArray(), Base64.NO_WRAP)); 339 | 340 | if(irView.filterBitmap){ 341 | temporaryCanvas.setDrawFilter(irView.filterPaint); 342 | } else { 343 | temporaryCanvas.setDrawFilter(irView.noFilterPaint); 344 | } 345 | temporaryCanvas.drawBitmap(irPicture.getBitmap(), irTransformationMatrix, irPaint); 346 | 347 | otc.put("ir_picture", ir); 348 | 349 | } 350 | 351 | //rotate final image according to orientation parameter (portrait / landscape) 352 | Bitmap targetBitmap; 353 | if(orientation == 90 || orientation == 270){ 354 | targetBitmap = Bitmap.createBitmap(irView.getHeight(), irView.getWidth(), Bitmap.Config.ARGB_8888); 355 | } else { 356 | targetBitmap = Bitmap.createBitmap(irView.getWidth(), irView.getHeight(), Bitmap.Config.ARGB_8888); 357 | } 358 | Canvas targetCanvas = new Canvas(targetBitmap); 359 | 360 | Matrix finalTransformation = new Matrix(); 361 | //then rotate in middle 362 | finalTransformation.postRotate(orientation, fusedImage.getWidth()/2.0f,fusedImage.getHeight()/2.0f); 363 | finalTransformation.postTranslate((targetBitmap.getWidth() - fusedImage.getWidth())/2.0f, (targetBitmap.getHeight() - fusedImage.getHeight())/2.0f); 364 | 365 | targetCanvas.drawBitmap(fusedImage, finalTransformation, new Paint()); 366 | 367 | 368 | //TODO saving pictures 369 | 370 | 371 | //get timestamp 372 | Date timestamp = Calendar.getInstance().getTime(); 373 | SimpleDateFormat tsFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); 374 | 375 | File folderToSave = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "/OpenThermalCamera/"); 376 | //create dir if it doesn't exist 377 | folderToSave.mkdirs(); 378 | 379 | 380 | try{ 381 | 382 | File targetBitmapFile = new File(folderToSave, "IMG_" + tsFormat.format(timestamp) + ".jpg"); 383 | targetBitmapFile.setWritable(true); 384 | FileOutputStream fo = new FileOutputStream(targetBitmapFile); 385 | 386 | //save targetBitmap as png 387 | targetBitmap.compress(Bitmap.CompressFormat.JPEG, 90, fo); 388 | 389 | fo.close(); 390 | 391 | Log.d(TAG, "Saved targetBitmap as: " + targetBitmapFile.getAbsolutePath()); 392 | 393 | } catch (IOException ioException){ 394 | Log.d(TAG, "IOException while trying to write target bitmap: " + ioException.getLocalizedMessage()); 395 | } 396 | 397 | try{ 398 | File otcFile = new File(folderToSave, "IMG_" + tsFormat.format(timestamp) + ".otc"); 399 | otcFile.setWritable(true); 400 | FileOutputStream fos = new FileOutputStream(otcFile); 401 | OutputStreamWriter out = new OutputStreamWriter(fos); 402 | out.write(otc.toString(4)); 403 | 404 | out.close(); 405 | fos.close(); 406 | 407 | Log.d(TAG, "Saved otc file as: " + otcFile.getAbsolutePath()); 408 | 409 | } catch (IOException ioException){ 410 | Log.d(TAG, "IOException while trying to write otc file: " + ioException.getLocalizedMessage()); 411 | } 412 | 413 | //notify to scan the folder 414 | //media scanner connection 415 | MediaScannerConnection.scanFile(MainActivity.this, new String[]{folderToSave.getAbsolutePath()}, null, null); 416 | 417 | } catch (JSONException jsonException){ 418 | Log.d(TAG, "JSONException: " + jsonException.getMessage()); 419 | } 420 | } 421 | 422 | class OTCStateListener implements OTC.StateListener { 423 | 424 | @Override 425 | public void onStateChanged(OTC.OTCState otcState, OTC.UsbState usbState) { 426 | Log.d("MainActivity", "State changed: otc = " + otcState.name() + ", usb = " + usbState.name()); 427 | //start animation if OTC is unplugged 428 | if(usbState == OTC.UsbState.CONNECTED){ 429 | stopOtcInsertAnimation(); 430 | } else { 431 | startOtcInsertAnimation(); 432 | } 433 | 434 | if(otcState == OTC.OTCState.READY){ 435 | // OTC is ready 436 | stopOtcInsertAnimation(); 437 | onResume(); 438 | } 439 | } 440 | } 441 | 442 | void startOtcInsertAnimation() { 443 | //usb disconnected 444 | // hide IR and RGB views 445 | // display animation 446 | // disable picture button 447 | 448 | camera.setVisibility(View.GONE); 449 | irView.setVisibility(View.GONE); 450 | 451 | insertOtcAni.setVisible(true, true); 452 | imgInsertOtc.setVisibility(View.VISIBLE); 453 | 454 | // Start the animation (looped playback by default). 455 | insertOtcAni.start(); 456 | 457 | //disable picture button 458 | findViewById(R.id.btnTakePicture).setAlpha(0.2f); 459 | findViewById(R.id.btnTakePicture).setEnabled(false); 460 | } 461 | 462 | void stopOtcInsertAnimation(){ 463 | 464 | insertOtcAni.setVisible(false, true); 465 | imgInsertOtc.setVisibility(View.INVISIBLE); 466 | 467 | //bring back IR and RGB views 468 | //Will be brought back after OTC initializes and calls onResume 469 | 470 | //ReEnable take picture button 471 | findViewById(R.id.btnTakePicture).setAlpha(1.0f); 472 | findViewById(R.id.btnTakePicture).setEnabled(true); 473 | 474 | } 475 | 476 | 477 | @Override 478 | protected void onStart() { 479 | super.onStart(); 480 | //start usb service (if not started) 481 | otc.startUsbService(); 482 | 483 | } 484 | 485 | @Override 486 | protected void onStop() { 487 | super.onStop(); 488 | 489 | //stop usb service 490 | otc.stopUsbService(); 491 | } 492 | 493 | 494 | 495 | @Override 496 | protected void onResume() { 497 | super.onResume(); 498 | 499 | //Layout rotation listener 500 | layoutRotateOnOrientation.enable(); 501 | 502 | //OTC continue 503 | otc.resumeOTC(); 504 | //update otc with ?new? settings 505 | otc.sendSettings(otc.getSettingsFromSharedPreferences()); 506 | 507 | //check all settings 508 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 509 | boolean overlay_enabled = sharedPreferences.getBoolean("overlay_enabled", false); 510 | boolean filter_enabled = sharedPreferences.getBoolean("filter_enabled", false); 511 | 512 | int emissivityPercent = 90; 513 | try { 514 | emissivityPercent = sharedPreferences.getInt("emissivity", 90); 515 | } catch(Exception ex){ 516 | try { 517 | emissivityPercent = Integer.parseInt(sharedPreferences.getString("emissivity", "90")); 518 | }catch (Exception ex1){} 519 | } 520 | double emissivity = emissivityPercent / 100.0; //percent to absolute 521 | //set emissivity 522 | otc.setEmissivity(emissivity); 523 | 524 | //set current palette 525 | ThermalPalette selectedThermalPalette = ThermalPalette.getCurrentSelectedPalette(this); 526 | Log.d(TAG, "Setting thermal palette: " + selectedThermalPalette.toString()); 527 | irView.getIRPicture().setThermalPalette(selectedThermalPalette); 528 | 529 | //set dynamic range 530 | boolean dynamic_range_enabled = sharedPreferences.getBoolean("dynamic_range_enabled", false); 531 | Log.d(TAG, "Dynamic range enabled: " + dynamic_range_enabled); 532 | irView.getIRPicture().setDynamicRange(dynamic_range_enabled); 533 | 534 | //set dynamic range min difference 535 | float dynamic_range_min_difference = sharedPreferences.getInt("dynamic_range_min_difference", 0); 536 | Log.d(TAG, "Dynamic range min difference: " + dynamic_range_min_difference); 537 | irView.getIRPicture().setDynamicRangeMinDifference(dynamic_range_min_difference); 538 | 539 | //set custom range 540 | irView.getIRPicture().setCustomTemperatureRange(sharedPreferences.getBoolean("custom_range_enabled", false)); 541 | irView.getIRPicture().setCustomMinTemperature(sharedPreferences.getInt("custom_range_min", -5)); 542 | irView.getIRPicture().setCustomMaxTemperature(sharedPreferences.getInt("custom_range_max", 50)); 543 | 544 | //enabled/disable max temp marker 545 | boolean maxTempMarkerEnabled = sharedPreferences.getBoolean("max_temperature_marker_enabled", false); 546 | irView.setMaxMarkerEnabled(maxTempMarkerEnabled); 547 | 548 | 549 | //searchArea enabled/disabled and searchAreaSize 550 | irView.setSearchAreaEnabled(sharedPreferences.getBoolean("search_area_enabled", false)); 551 | int searchAreaSize = sharedPreferences.getInt("search_area_size", 3); 552 | irView.setSearchAreaSize(searchAreaSize); 553 | irPicture.setSeachAreaSize(searchAreaSize); 554 | 555 | //act on settings: 556 | //enable / disable camera 557 | setCameraViewEnabled(overlay_enabled); 558 | //if overlay not enabled, hide camera layout 559 | Log.d(TAG, "overlay_enabled: " + overlay_enabled); 560 | if(!overlay_enabled){ 561 | camera.setVisibility(View.GONE); 562 | irView.setVisibility(View.VISIBLE); 563 | irView.setImageAlpha(255); 564 | } else { 565 | //read overlay alpha value 566 | camera.setVisibility(View.VISIBLE); 567 | irView.setVisibility(View.VISIBLE); 568 | irView.setImageAlpha(sharedPreferences.getInt("ir_alpha_value", 150)); 569 | } 570 | 571 | //filter bitmap? 572 | irView.setImageFilter(filter_enabled); 573 | 574 | 575 | // check USB state and if USB not connected, display "insert OTC" animation 576 | //debug 577 | Log.d(TAG, "USB state: " + otc.getUsbState() + ", OTC state: " + otc.getOTCState()); 578 | if(otc.getUsbState() == OTC.UsbState.DISCONNECTED && otc.getOTCState() == OTC.OTCState.NOT_READY){ 579 | startOtcInsertAnimation(); 580 | } 581 | if(otc.getUsbState() == OTC.UsbState.CONNECTED && otc.getOTCState() == OTC.OTCState.READY) { 582 | stopOtcInsertAnimation(); 583 | } 584 | 585 | } 586 | 587 | @Override 588 | protected void onPause() { 589 | super.onPause(); 590 | 591 | layoutRotateOnOrientation.disable(); 592 | 593 | otc.pauseOTC(); 594 | 595 | } 596 | 597 | /* Orientation change, rotate all icons, texts and buttons */ 598 | 599 | private int getRotationInDegrees(int rotationConstant){ 600 | int currentRotationInDegrees = 0; 601 | switch(rotationConstant){ 602 | case Surface.ROTATION_0 : 603 | currentRotationInDegrees = 0; 604 | break; 605 | 606 | case Surface.ROTATION_90: 607 | currentRotationInDegrees = 90; 608 | break; 609 | 610 | case Surface.ROTATION_180: 611 | currentRotationInDegrees = 180; 612 | break; 613 | 614 | case Surface.ROTATION_270: 615 | currentRotationInDegrees = 270; 616 | break; 617 | } 618 | 619 | return currentRotationInDegrees; 620 | } 621 | 622 | 623 | public void getAllRotatableViews(ArrayList toAdd, ViewGroup parent) { 624 | for (int i = 0; i < parent.getChildCount(); i++) { 625 | final View child = parent.getChildAt(i); 626 | if (child instanceof ViewGroup) { 627 | getAllRotatableViews(toAdd, (ViewGroup) child); 628 | } else { 629 | if (child != null) { 630 | // DO SOMETHING WITH VIEW 631 | if(child instanceof ImageButton || child instanceof TextView){ 632 | toAdd.add(child); 633 | } 634 | } 635 | } 636 | } 637 | } 638 | 639 | 640 | private class LayoutRotateOnOrientation extends OrientationEventListener { 641 | 642 | 643 | public int getCurrentOrientation(){ 644 | return current_orientation; 645 | } 646 | 647 | private int previousRotationInDegrees = 0; 648 | private int current_orientation = 0; 649 | private int diff = 0; 650 | 651 | Display display = null; 652 | LayoutRotateOnOrientation(){ 653 | super(MainActivity.this, SensorManager.SENSOR_DELAY_UI); 654 | display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); 655 | 656 | } 657 | 658 | public void uiRotate(int angle){ 659 | 660 | for (View v : viewsToRotate) { 661 | 662 | RotateAnimation rotate = new RotateAnimation(previousRotationInDegrees, angle, 663 | Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 664 | 0.5f); 665 | 666 | rotate.setDuration(1000); 667 | rotate.setRepeatCount(0); 668 | rotate.setInterpolator(new LinearInterpolator()); 669 | rotate.setFillAfter(true); 670 | 671 | v.startAnimation(rotate); 672 | } 673 | 674 | previousRotationInDegrees = angle; 675 | 676 | } 677 | 678 | 679 | 680 | @Override 681 | public void onOrientationChanged(int orientation) { 682 | 683 | //Algorithm adopted from Open Camera project by Mark Harman 684 | 685 | if( orientation != OrientationEventListener.ORIENTATION_UNKNOWN ) { 686 | 687 | diff = Math.abs(orientation - current_orientation); 688 | if( diff > 180 ) { 689 | diff = 360 - diff; 690 | } 691 | 692 | // threshold 693 | if( diff > 60 ) { 694 | orientation = ((orientation + 45) / 90 * 90) % 360; 695 | 696 | if( orientation != current_orientation ) { 697 | current_orientation = orientation; 698 | 699 | Log.d(TAG, "current_orientation is now: " + current_orientation); 700 | 701 | int rotation = MainActivity.this.getWindowManager().getDefaultDisplay().getRotation(); 702 | int degrees = 0; 703 | switch (rotation) { 704 | case Surface.ROTATION_0: degrees = 0; break; 705 | case Surface.ROTATION_90: degrees = 90; break; 706 | case Surface.ROTATION_180: degrees = 180; break; 707 | case Surface.ROTATION_270: degrees = 270; break; 708 | default: 709 | break; 710 | } 711 | 712 | int relative_orientation = (current_orientation + degrees) % 360; 713 | 714 | Log.d(TAG, " current_orientation = " + current_orientation); 715 | Log.d(TAG, " degrees = " + degrees); 716 | Log.d(TAG, " relative_orientation = " + relative_orientation); 717 | 718 | final int ui_rotation = (360 - relative_orientation) % 360; 719 | uiRotate(ui_rotation); 720 | 721 | } 722 | } 723 | 724 | } 725 | 726 | } 727 | } 728 | 729 | 730 | 731 | } 732 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/OTC.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.content.ServiceConnection; 9 | import android.content.SharedPreferences; 10 | import android.os.Bundle; 11 | import android.os.Handler; 12 | import android.os.IBinder; 13 | import android.os.Message; 14 | import android.preference.PreferenceManager; 15 | import android.util.Log; 16 | import android.widget.Toast; 17 | 18 | import java.lang.ref.WeakReference; 19 | import java.util.ArrayList; 20 | import java.util.Queue; 21 | import java.util.Set; 22 | 23 | class OTC { 24 | 25 | public static final String TAG = "OTC"; 26 | 27 | //Otc private variables 28 | public static final float MLX_MIN_TEMP = -40.0f; 29 | public static final float MLX_MAX_TEMP = 300.0f; 30 | 31 | public static final int IR_WIDTH = 32; 32 | public static final int IR_HEIGHT = 24; 33 | 34 | public static final int IR_SPECTRUM_WIDTH = 1000; 35 | public static final int IR_SPECTRUM_HEIGHT = 1; 36 | 37 | private static double minIrTemp = 9999.0, maxIrTemp = -9999.0; 38 | 39 | private static double[] irTemp = new double[IR_WIDTH * IR_HEIGHT]; 40 | private static double[] irTempFlipped = new double[IR_WIDTH * IR_HEIGHT]; 41 | 42 | private static int taShift = 8; 43 | 44 | private Context ctx = null; 45 | 46 | private static Protocol protocol = null; 47 | private static MLX90640 mlxapi = new MLX90640(); 48 | private static MLX90640.Params mlxparams = new MLX90640.Params(); 49 | 50 | private static boolean parametersAvailable = false; 51 | 52 | private static double emissivity = 0.9; 53 | 54 | //Usb private variables 55 | private static UsbService usbService; 56 | private static MyHandler mHandler = null; 57 | 58 | enum UsbState { 59 | CONNECTED, DISCONNECTED 60 | } 61 | private static UsbState usbState = UsbState.DISCONNECTED; 62 | 63 | enum OTCState { 64 | READY, NO_PERMISSIONS, NOT_READY 65 | } 66 | private static OTCState otcState = OTCState.NOT_READY; 67 | 68 | 69 | public OTCState getOTCState(){ 70 | return otcState; 71 | } 72 | 73 | public UsbState getUsbState(){ 74 | return usbState; 75 | } 76 | 77 | 78 | //settings 79 | static class Settings { 80 | Protocol.RefreshRate refreshRate; 81 | Protocol.ScanMode scanMode; 82 | Protocol.Resolution resolution; 83 | } 84 | 85 | interface FirmwareVersionListener { 86 | void firmwareVersion(Protocol.FirmwareVersion firmwareVerion); 87 | } 88 | ArrayList fwListener = new ArrayList<>(); 89 | 90 | interface ResponseListener{ 91 | void onResponsePre(Protocol.RspStruct rsp); 92 | void onResponsePost(Protocol.RspStruct rsp); 93 | } 94 | ResponseListener responseListener = null; 95 | 96 | void setResponseListener(ResponseListener rl){ 97 | responseListener = rl; 98 | } 99 | 100 | 101 | interface StateListener { 102 | void onStateChanged(OTCState otcState, UsbState usbState); 103 | } 104 | StateListener stateListener = null; 105 | 106 | 107 | private static ArrayList> objectReferences = new ArrayList<>(); 108 | 109 | public OTC(Context ctx, StateListener stateListener){ 110 | //add to pool of OTC objects. 111 | objectReferences.add(new WeakReference<>(this)); 112 | 113 | //state listener 114 | this.stateListener = stateListener; 115 | 116 | //context 117 | this.ctx = ctx; 118 | 119 | //create the protocol driver if it doesnt exist yet 120 | if(protocol == null) { 121 | protocol = new Protocol(new Protocol.ISender() { 122 | @Override 123 | public void sendBytes(byte[] bytesToSend) { 124 | String test = "["; 125 | for (int i = 0; i < bytesToSend.length; i++) { 126 | if (i != 0) { 127 | test += " ,"; 128 | } 129 | test += bytesToSend[i]; 130 | } 131 | test += "]"; 132 | Log.d("OTC", "About to send " + bytesToSend.length + " = " + test); 133 | if (usbService != null) { 134 | usbService.write(bytesToSend); 135 | } 136 | } 137 | }, new Protocol.IResponseListener() { 138 | @Override 139 | public void onResponse(Queue q) { 140 | while (q.size() > 0) { 141 | Protocol.RspStruct rsp = q.poll(); 142 | handleResponse(rsp); 143 | } 144 | } 145 | }); 146 | } 147 | 148 | //create usb serial connection 149 | if(mHandler == null){ 150 | mHandler = new MyHandler(protocol); 151 | } 152 | } 153 | 154 | 155 | 156 | // Events from UsbService 157 | private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { 158 | @Override 159 | public void onReceive(Context context, Intent intent) { 160 | switch (intent.getAction()) { 161 | 162 | case UsbService.ACTION_USB_READY: 163 | Toast.makeText(context, "USB Ready", Toast.LENGTH_SHORT).show(); 164 | 165 | //usb state disconnected, otc 166 | usbState = UsbState.CONNECTED; 167 | otcState = OTCState.READY; 168 | 169 | break; 170 | 171 | case UsbService.ACTION_USB_PERMISSION_GRANTED: // USB PERMISSION GRANTED 172 | Toast.makeText(context, "USB Permissions granted", Toast.LENGTH_SHORT).show(); 173 | 174 | //usb state disconnected, otc 175 | usbState = UsbState.CONNECTED; 176 | otcState = OTCState.NOT_READY; 177 | 178 | break; 179 | case UsbService.ACTION_USB_PERMISSION_NOT_GRANTED: // USB PERMISSION NOT GRANTED 180 | Toast.makeText(context, "USB Permission not granted", Toast.LENGTH_SHORT).show(); 181 | 182 | //usb state disconnected, otc 183 | usbState = UsbState.CONNECTED; 184 | otcState = OTCState.NO_PERMISSIONS; 185 | 186 | break; 187 | case UsbService.ACTION_NO_USB: // NO USB CONNECTED 188 | Toast.makeText(context, "No USB connected", Toast.LENGTH_SHORT).show(); 189 | 190 | //usb state disconnected, otc 191 | usbState = UsbState.DISCONNECTED; 192 | otcState = OTCState.NOT_READY; 193 | 194 | break; 195 | case UsbService.ACTION_USB_DISCONNECTED: // USB DISCONNECTED 196 | Toast.makeText(context, "USB disconnected", Toast.LENGTH_SHORT).show(); 197 | 198 | //usb state disconnected, otc 199 | usbState = UsbState.DISCONNECTED; 200 | otcState = OTCState.NOT_READY; 201 | 202 | break; 203 | case UsbService.ACTION_USB_NOT_SUPPORTED: // USB NOT SUPPORTED 204 | Toast.makeText(context, "USB device not supported", Toast.LENGTH_SHORT).show(); 205 | 206 | //otc not ready 207 | otcState = OTCState.NOT_READY; 208 | usbState = UsbState.CONNECTED; 209 | 210 | break; 211 | } 212 | 213 | 214 | //notify listeners 215 | if(stateListener != null) { 216 | stateListener.onStateChanged(otcState, usbState); 217 | } 218 | 219 | } 220 | }; 221 | 222 | 223 | 224 | private final ServiceConnection usbConnection = new ServiceConnection() { 225 | @Override 226 | public void onServiceConnected(ComponentName arg0, IBinder arg1) { 227 | usbService = ((UsbService.UsbBinder) arg1).getService(); 228 | usbService.setHandler(mHandler); 229 | } 230 | 231 | @Override 232 | public void onServiceDisconnected(ComponentName arg0) { 233 | usbService = null; 234 | } 235 | }; 236 | 237 | 238 | public void startUsbService() { 239 | setFilters(); // Start listening notifications from UsbService 240 | startService(UsbService.class, usbConnection, null); // Start UsbService(if it was not started before) and Bind it 241 | } 242 | 243 | public void stopUsbService() { 244 | //stop autoframe sending 245 | setAutoFrameSending(false); 246 | 247 | 248 | ctx.unregisterReceiver(mUsbReceiver); 249 | ctx.unbindService(usbConnection); 250 | } 251 | 252 | public void pauseOTC(){ 253 | //stop autoframe sending 254 | if(usbState == UsbState.CONNECTED && otcState == OTCState.READY) { 255 | setAutoFrameSending(false); 256 | } 257 | } 258 | public void resumeOTC(){ 259 | //if usb connected 260 | if(usbState == UsbState.CONNECTED && otcState == OTCState.READY) { 261 | //check if eeprom data is available 262 | if(!parametersAvailable){ 263 | requestDumpEE(); 264 | } 265 | 266 | setAutoFrameSending(true); 267 | } 268 | } 269 | 270 | public void setEmissivity(double emissivity){ 271 | this.emissivity = emissivity; 272 | } 273 | 274 | public double getEmissivity(){ 275 | return emissivity; 276 | } 277 | 278 | private void startService(Class service, ServiceConnection serviceConnection, Bundle extras) { 279 | if (!UsbService.SERVICE_CONNECTED) { 280 | Intent startService = new Intent(ctx, service); 281 | if (extras != null && !extras.isEmpty()) { 282 | Set keys = extras.keySet(); 283 | for (String key : keys) { 284 | String extra = extras.getString(key); 285 | startService.putExtra(key, extra); 286 | } 287 | } 288 | ctx.startService(startService); 289 | } 290 | Intent bindingIntent = new Intent(ctx, service); 291 | ctx.bindService(bindingIntent, serviceConnection, Context.BIND_AUTO_CREATE); 292 | } 293 | 294 | private void setFilters() { 295 | IntentFilter filter = new IntentFilter(); 296 | filter.addAction(UsbService.ACTION_USB_READY); 297 | filter.addAction(UsbService.ACTION_USB_PERMISSION_GRANTED); 298 | filter.addAction(UsbService.ACTION_NO_USB); 299 | filter.addAction(UsbService.ACTION_USB_DISCONNECTED); 300 | filter.addAction(UsbService.ACTION_USB_NOT_SUPPORTED); 301 | filter.addAction(UsbService.ACTION_USB_PERMISSION_NOT_GRANTED); 302 | ctx.registerReceiver(mUsbReceiver, filter); 303 | } 304 | 305 | /* 306 | * This handler will be passed to UsbService. Data received from serial port is displayed through this handler 307 | */ 308 | private static class MyHandler extends Handler { 309 | private final WeakReference mProtocol; 310 | 311 | public MyHandler(Protocol protocol) { 312 | mProtocol = new WeakReference<>(protocol); 313 | } 314 | 315 | @Override 316 | public void handleMessage(Message msg) { 317 | switch (msg.what) { 318 | case UsbService.MESSAGE_FROM_SERIAL_PORT: 319 | 320 | mProtocol.get().handleNewData((byte[]) msg.obj); 321 | 322 | break; 323 | 324 | } 325 | } 326 | } 327 | 328 | 329 | public void jumpToBootloader(){ 330 | Protocol.CmdStruct cmd = new Protocol.CmdStruct(); 331 | cmd.commandCode = Protocol.CMD_JUMP_TO_BOOTLOADER; 332 | cmd.dataLength = 0; 333 | protocol.sendCommand(cmd); 334 | } 335 | 336 | public void getFirmwareVersion(FirmwareVersionListener listener){ 337 | fwListener.add(listener); 338 | requestFirmwareVersion(); 339 | } 340 | 341 | 342 | public Settings getSettingsFromSharedPreferences(){ 343 | Settings cur = new Settings(); 344 | 345 | //get settings 346 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); 347 | 348 | cur.refreshRate = Protocol.RefreshRate.valueOf(prefs.getString("refresh_rate", Protocol.RefreshRate.HZ_2.name())); 349 | cur.resolution = Protocol.Resolution.valueOf(prefs.getString("resolution", Protocol.Resolution.BIT_16.name())); 350 | cur.scanMode = Protocol.ScanMode.valueOf(prefs.getString("mode", Protocol.ScanMode.CHESS.name())); 351 | 352 | return cur; 353 | } 354 | 355 | 356 | public void sendSettings(Settings settings){ 357 | 358 | //initiate sequence of commands 359 | //request EE 360 | requestDumpEE(); 361 | 362 | //set refresh rate 363 | setRefreshRate(settings.refreshRate); 364 | 365 | //set resolution 366 | setResolution(settings.resolution); 367 | 368 | //set ScanMode 369 | setScanMode(settings.scanMode); 370 | 371 | //enable auto frame sending 372 | setAutoFrameSending(true); 373 | 374 | } 375 | 376 | //2d temp datat 377 | static double[][] tempData2D = new double[OTC.IR_HEIGHT][OTC.IR_WIDTH]; 378 | 379 | //returns ir temperature data [height][width] 380 | public double[][] getIrTemp(){ 381 | return tempData2D; 382 | } 383 | 384 | public double getMinIrTemp(){ 385 | return minIrTemp; 386 | } 387 | public double getMaxIrTemp(){ 388 | return maxIrTemp; 389 | } 390 | 391 | 392 | private void setAutoFrameSending(boolean enabled){ 393 | Protocol.CmdStruct cmd = new Protocol.CmdStruct(); 394 | cmd.commandCode = Protocol.CMD_SET_AUTO_FRAME_DATA_SENDING; 395 | cmd.dataLength = 1; 396 | 397 | if(enabled){ 398 | //send auto frame data enabled command 399 | cmd.data.add(1); 400 | }else{ 401 | //send auto frame data disabled command 402 | cmd.data.add(0); 403 | } 404 | 405 | protocol.sendCommand(cmd); 406 | } 407 | 408 | 409 | private void setRefreshRate(Protocol.RefreshRate refreshRate){ 410 | Protocol.CmdStruct cmd = new Protocol.CmdStruct(); 411 | cmd.commandCode = Protocol.CMD_SET_REFRESH_RATE; 412 | cmd.dataLength = 1; 413 | 414 | cmd.data.add(refreshRate.getValue()); 415 | 416 | protocol.sendCommand(cmd); 417 | } 418 | 419 | private void setResolution(Protocol.Resolution resolution){ 420 | Protocol.CmdStruct cmd = new Protocol.CmdStruct(); 421 | cmd.commandCode = Protocol.CMD_SET_RESOLUTION; 422 | cmd.dataLength = 1; 423 | 424 | cmd.data.add(resolution.getValue()); 425 | 426 | protocol.sendCommand(cmd); 427 | } 428 | 429 | private void setScanMode(Protocol.ScanMode scanMode){ 430 | Protocol.CmdStruct cmd = new Protocol.CmdStruct(); 431 | cmd.commandCode = Protocol.CMD_SET_MODE; 432 | cmd.dataLength = 1; 433 | 434 | cmd.data.add(scanMode.getValue()); 435 | 436 | protocol.sendCommand(cmd); 437 | } 438 | 439 | private void requestDumpEE(){ 440 | Protocol.CmdStruct cmd = new Protocol.CmdStruct(); 441 | cmd.commandCode = Protocol.CMD_DUMP_EE; 442 | cmd.dataLength = 0; 443 | 444 | protocol.sendCommand(cmd); 445 | } 446 | 447 | private void requestFirmwareVersion(){ 448 | Protocol.CmdStruct cmd = new Protocol.CmdStruct(); 449 | cmd.commandCode = Protocol.CMD_GET_FIRMWARE_VERSION; 450 | cmd.dataLength = 0; 451 | protocol.sendCommand(cmd); 452 | } 453 | 454 | public static void handleResponse(Protocol.RspStruct rsp) { 455 | 456 | for(WeakReference wrOtc : objectReferences){ 457 | OTC ref = wrOtc.get(); 458 | if(ref != null){ 459 | if(ref.responseListener != null) { 460 | ref.responseListener.onResponsePre(rsp); 461 | } 462 | } 463 | } 464 | 465 | 466 | switch (rsp.responseCode) { 467 | case Protocol.RSP_PING: //PING 468 | if(rsp.data.size() == 1) { 469 | int pong = rsp.data.get(0); 470 | Log.d("PONG", "Pong = " + pong); 471 | } 472 | break; 473 | 474 | case Protocol.RSP_DUMP_EE: //DumpEE 475 | 476 | //eeDump are 16bit values, so we combine 2 bytes together 477 | int[] eedump = new int[832]; 478 | for(int i = 0; i<832; i++){ 479 | eedump[i] = ((rsp.data.get(i*2 + 0) & 0xFF) << 8) | ((rsp.data.get(i*2 + 1) & 0xFF)); 480 | } 481 | 482 | //extract parameters 483 | int error = mlxapi.ExtractParameters(eedump, mlxparams); 484 | 485 | if(error != 0){ 486 | Log.e("OTC","DumpEE, error code = " + error); 487 | } else { 488 | //extraction successful 489 | parametersAvailable = true; 490 | } 491 | 492 | break; 493 | 494 | case Protocol.RSP_GET_FRAME_DATA: //GetFrameData 495 | 496 | //framedata are 16bit values, so we combine 2 bytes together 497 | int[] frameData = new int[834]; 498 | for(int i = 0; i<834; i++){ 499 | frameData[i] = ((rsp.data.get(i*2 + 0) & 0xFF) << 8) | ((rsp.data.get(i*2 + 1) & 0xFF)); 500 | } 501 | 502 | //Log.d("GetFrameData", "rsp.data.size() = " + rsp.data.size() + ", rsp.dataLength = " + rsp.dataLength); 503 | 504 | 505 | //Get Subpage number first 506 | int subpage = mlxapi.GetSubPageNumber(frameData); 507 | 508 | //Get scan mode (0 = interleaved, 1 chessmode) 509 | //TODO get scanmode from settings 510 | Protocol.ScanMode scanMode = Protocol.ScanMode.CHESS; 511 | 512 | double tr = mlxapi.GetTa(frameData, mlxparams) - taShift; 513 | 514 | mlxapi.CalculateTo(frameData, mlxparams, emissivity, tr, irTemp); 515 | 516 | //flip the temperature 517 | for(int y = 0; y wrOtc : objectReferences){ 562 | OTC ref = wrOtc.get(); 563 | if(ref != null){ 564 | for(FirmwareVersionListener listener : ref.fwListener){ 565 | listener.firmwareVersion(deviceVersion); 566 | } 567 | ref.fwListener.clear(); 568 | } 569 | } 570 | 571 | 572 | Log.d("GetFirmwareVersion","Device version = " + deviceVersion); 573 | 574 | break; 575 | 576 | default: 577 | //NOT COOL! 578 | //TODO: Add handler for this 579 | break; 580 | } 581 | 582 | for(WeakReference wrOtc : objectReferences){ 583 | OTC ref = wrOtc.get(); 584 | if(ref != null){ 585 | if(ref.responseListener != null) { 586 | ref.responseListener.onResponsePost(rsp); 587 | } 588 | } 589 | } 590 | 591 | } 592 | 593 | 594 | 595 | } 596 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/OTCFileFilter.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import java.io.File; 4 | 5 | public class OTCFileFilter { 6 | 7 | private final String[] okFileExtensions = new String[] {"otc"}; 8 | 9 | public boolean accept(File file) { 10 | for (String extension : okFileExtensions) { 11 | if (file.getName().toLowerCase().endsWith(extension)) { 12 | return true; 13 | } 14 | } 15 | return false; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/Palette/DarkHotPalette.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera.Palette; 2 | 3 | import android.graphics.Color; 4 | 5 | public class DarkHotPalette extends ThermalPalette { 6 | 7 | 8 | @Override 9 | public double getDefaultMinTemperature() { 10 | return 5; 11 | } 12 | 13 | @Override 14 | public double getDefaultMaxTemperature() { 15 | return 45; 16 | } 17 | 18 | @Override 19 | public int temperatureToColor(double temperature, double minTemperature, double maxTemperature) { 20 | 21 | double tempPercent = (temperature - minTemperature) / (maxTemperature - minTemperature); 22 | if(tempPercent > 1.0) tempPercent = 1.0; 23 | else if(tempPercent < 0.0) tempPercent = 0.0; 24 | 25 | tempPercent = 1.0 - tempPercent; 26 | int darkLevel = (int) Math.round(tempPercent * 255); 27 | 28 | return Color.argb(255, darkLevel, darkLevel, darkLevel); 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/Palette/RainbowPalette.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera.Palette; 2 | 3 | import androidx.annotation.ColorInt; 4 | 5 | public class RainbowPalette extends ThermalPalette { 6 | 7 | 8 | 9 | //lookup 10 | static int[] lookupTable = new int[] 11 | {0x263923,0x263924,0x263825,0x253825,0x253826,0x243827,0x243828,0x233729,0x23372a,0x23372b,0x22372c,0x21372d,0x21362e,0x21362f,0x203630,0x203631, 12 | 0x203632,0x1f3632,0x1f3633,0x1e3534,0x1e3535,0x1e3536,0x1d3537,0x1d3538,0x1c3539,0x1c353a,0x1c353b,0x1b353c,0x1b343d,0x1a343e,0x1a343f,0x1a3440, 13 | 0x193441,0x193442,0x183443,0x183444,0x183445,0x183446,0x173447,0x173448,0x163449,0x16334a,0x16334b,0x16334c,0x15334c,0x15334d,0x15334e,0x15334f, 14 | 0x143350,0x143351,0x143352,0x133353,0x133354,0x133355,0x123356,0x123357,0x123358,0x123458,0x123459,0x11345a,0x11345b,0x11345c,0x10345d,0x10345e, 15 | 0x10345f,0x103460,0x103461,0xf3462,0xf3463,0xf3464,0xf3465,0xe3465,0xe3566,0xe3567,0xe3568,0xe3569,0xe356a,0xd356b,0xd356c,0xd356d, 16 | 0xd366e,0xd366f,0xc3670,0xc3671,0xc3672,0xc3673,0xc3774,0xc3775,0xc3776,0xb3776,0xb3777,0xb3778,0xb3879,0xb387a,0xb387b,0xb387c, 17 | 0xb397d,0xb397e,0xa397e,0xa397f,0xa3980,0xa3a81,0xa3a82,0xa3a83,0xa3a84,0xa3b85,0xa3b86,0xa3b87,0xa3c88,0xa3c89,0xa3c8a,0xa3c8b, 18 | 0xa3d8b,0xa3d8c,0xa3d8d,0xa3e8e,0xa3e8f,0x93e90,0x93f91,0x93f92,0x93f93,0xa3f93,0xa4094,0xa4095,0xa4196,0xa4197,0xa4198,0xa4298, 19 | 0xa4299,0xa429a,0xa439b,0xa439c,0xa439d,0xa449d,0xa449e,0xa449f,0xa459f,0xa45a0,0xa45a1,0xa46a2,0xa46a3,0xa47a3,0xb47a4,0xb47a5, 20 | 0xb48a5,0xb48a6,0xb48a7,0xb49a8,0xb49a9,0xb4aa9,0xb4aaa,0xb4aab,0xc4bab,0xc4bac,0xc4cad,0xc4cae,0xc4daf,0xc4db0,0xc4eb0,0xd4eb1, 21 | 0xd4fb2,0xd4fb3,0xd50b4,0xd50b5,0xe51b5,0xe51b6,0xe52b7,0xe52b8,0xe53b9,0xf53b9,0xf53ba,0xf54ba,0xf54bb,0xf55bb,0xf55bc,0x1056bd, 22 | 0x1056be,0x1057be,0x1057bf,0x1158c0,0x1159c1,0x1159c2,0x115ac2,0x125ac3,0x125bc3,0x125bc4,0x125cc5,0x135cc5,0x135cc6,0x135dc6,0x135dc7,0x135ec7, 23 | 0x135ec8,0x145ec8,0x145fc9,0x1460ca,0x1560ca,0x1560cb,0x1561cb,0x1561cc,0x1562cc,0x1662cc,0x1662cd,0x1663cd,0x1663ce,0x1664ce,0x1764cf,0x1765d0, 24 | 0x1866d1,0x1867d2,0x1968d3,0x1968d4,0x1969d4,0x1a69d4,0x1a6ad5,0x1a6bd6,0x1b6bd6,0x1b6bd7,0x1b6cd7,0x1c6dd8,0x1c6ed9,0x1d6ed9,0x1d6fda,0x1d70db, 25 | 0x1e70db,0x1e71db,0x1e71dc,0x1f71dc,0x1f72dd,0x1f73dd,0x2073de,0x2074de,0x2074df,0x2175df,0x2175e0,0x2176e0,0x2276e0,0x2277e1,0x2277e2,0x2378e2, 26 | 0x2379e3,0x2479e3,0x247ae3,0x247ae4,0x257be4,0x257be5,0x257ce5,0x267ce5,0x267de6,0x277ee6,0x277ee7,0x277fe7,0x287fe8,0x2880e8,0x2981e9,0x2982e9, 27 | 0x2a82ea,0x2a83ea,0x2b83eb,0x2b84eb,0x2c85ec,0x2c86ec,0x2d86ed,0x2d87ed,0x2e87ed,0x2e88ee,0x2f89ee,0x2f89ef,0x2f8aef,0x308aef,0x308af0,0x308bf0, 28 | 0x318bf0,0x318cf0,0x318cf1,0x328df1,0x328ef1,0x338ef2,0x338ff2,0x348ff3,0x3490f3,0x3591f3,0x3591f4,0x3592f4,0x3692f4,0x3693f5,0x3793f5,0x3794f5, 29 | 0x3894f5,0x3895f6,0x3996f6,0x3996f7,0x3a97f7,0x3a98f7,0x3b98f7,0x3b99f8,0x3c99f8,0x3c9af8,0x3d9af9,0x3d9bf9,0x3e9cf9,0x3e9dfa,0x3f9dfa,0x3f9efa, 30 | 0x409efa,0x409ffb,0x419ffb,0x41a0fb,0x42a1fb,0x42a1fc,0x43a2fc,0x44a3fc,0x44a3fd,0x45a4fd,0x45a5fd,0x46a5fd,0x46a6fe,0x47a6fe,0x47a7fe,0x48a7fe, 31 | 0x48a8fe,0x49a8ff,0x49a9ff,0x4aa9ff,0x4aaaff,0x4babff,0x4cabff,0x4cacff,0x4dadff,0x4eaeff,0x4fafff,0x50b0ff,0x50b1ff,0x51b1ff,0x52b2ff,0x52b3ff, 32 | 0x53b3ff,0x54b4ff,0x54b5ff,0x55b5ff,0x55b6ff,0x56b6ff,0x56b7ff,0x57b7ff,0x57b8ff,0x58b8ff,0x58b9ff,0x59b9ff,0x59baff,0x5abaff,0x5bbbff,0x5bbcff, 33 | 0x5cbcff,0x5dbdff,0x5dbeff,0x5ebeff,0x5fbfff,0x60c0ff,0x61c1ff,0x62c2ff,0x63c3ff,0x64c3ff,0x64c4ff,0x65c4ff,0x65c5ff,0x66c5ff,0x66c6ff,0x67c6ff, 34 | 0x67c7ff,0x68c7ff,0x68c8ff,0x69c8ff,0x6ac9ff,0x6bcaff,0x6ccbff,0x6dcbff,0x6dccff,0x6eccff,0x6ecdff,0x6fcdff,0x70ceff,0x70cfff,0x71cfff,0x72d0ff, 35 | 0x73d1ff,0x74d1ff,0x74d2ff,0x75d2ff,0x75d3ff,0x76d3ff,0x77d4ff,0x78d4ff,0x78d5ff,0x79d5ff,0x79d6ff,0x7ad6ff,0x7bd7ff,0x7cd8ff,0x7dd8ff,0x7dd9ff, 36 | 0x7ed9ff,0x7edaff,0x7fdaff,0x80dbff,0x81dbff,0x81dcff,0x82dcff,0x83ddff,0x84ddff,0x84deff,0x85deff,0x86dfff,0x87dfff,0x87e0ff,0x88e0ff,0x89e1ff, 37 | 0x8ae2ff,0x8be2ff,0x8ce3ff,0x8de4ff,0x8ee4ff,0x8fe5ff,0x90e5ff,0x90e6ff,0x91e6ff,0x92e7ff,0x93e7fe,0x94e8fe,0x95e8fe,0x95e9fe,0x96e9fd,0x97e9fd, 38 | 0x97eafd,0x98eafd,0x98eafc,0x99ebfc,0x9aebfc,0x9aebfb,0x9becfb,0x9cecfb,0x9cedfb,0x9dedfa,0x9eedfa,0x9eeefa,0x9feef9,0xa0eef9,0xa0eff9,0xa1eff8, 39 | 0xa2eff8,0xa2f0f8,0xa3f0f7,0xa4f0f7,0xa4f1f7,0xa5f1f6,0xa6f1f6,0xa7f1f5,0xa7f2f5,0xa8f2f5,0xa9f2f4,0xa9f3f4,0xaaf3f4,0xaaf3f3,0xabf3f3,0xabf4f3, 40 | 0xacf4f2,0xadf4f2,0xadf4f1,0xaef4f1,0xaef5f1,0xaff5f0,0xb0f5f0,0xb0f5ef,0xb1f6ef,0xb2f6ef,0xb2f6ee,0xb3f6ee,0xb4f6ed,0xb4f7ed,0xb5f7ed,0xb5f7ec, 41 | 0xb6f7ec,0xb6f7eb,0xb7f7eb,0xb7f8eb,0xb8f8ea,0xb9f8ea,0xb9f8e9,0xbaf8e9,0xbaf8e8,0xbbf9e8,0xbcf9e7,0xbdf9e6,0xbef9e6,0xbef9e5,0xbff9e5,0xbffae4, 42 | 0xc0fae4,0xc0fae3,0xc1fae3,0xc1fae2,0xc2fae2,0xc3fae1,0xc4fae0,0xc5fae0,0xc5fadf,0xc5fbdf,0xc6fbdf,0xc6fbde,0xc7fbde,0xc7fbdd,0xc8fbdd,0xc8fbdc, 43 | 0xc9fbdc,0xc9fbdb,0xcafbdb,0xcafbda,0xcbfbda,0xcbfbd9,0xccfbd9,0xccfbd8,0xcdfbd8,0xcdfbd7,0xcefbd7,0xcefbd6,0xcffbd6,0xcffbd5,0xd0fbd5,0xd0fbd4, 44 | 0xd1fbd4,0xd1fbd3,0xd2fbd3,0xd2fbd2,0xd3fbd1,0xd4fbd0,0xd5fbcf,0xd5fbce,0xd6fbce,0xd6fbcd,0xd6facd,0xd7facc,0xd8facb,0xd8faca,0xd9faca,0xd9fac9, 45 | 0xdafac9,0xdafac8,0xdbfac8,0xdbfac7,0xdbfac6,0xdcf9c6,0xdcf9c5,0xddf9c5,0xddf9c4,0xdef9c4,0xdef9c3,0xdff9c2,0xdff8c1,0xe0f8c1,0xe0f8c0,0xe1f8bf, 46 | 0xe1f8be,0xe2f8be,0xe2f8bd,0xe2f7bd,0xe3f7bd,0xe3f7bc,0xe3f7bb,0xe4f7bb,0xe4f7ba,0xe5f6ba,0xe5f6b9,0xe5f6b8,0xe6f6b8,0xe6f6b7,0xe7f6b6,0xe7f5b6, 47 | 0xe7f5b5,0xe8f5b5,0xe8f5b4,0xe8f5b3,0xe9f4b3,0xe9f4b2,0xeaf4b1,0xeaf3b0,0xebf3af,0xebf3ae,0xecf3ae,0xecf2ae,0xecf2ad,0xedf2ac,0xedf2ab,0xedf1ab, 48 | 0xeef1aa,0xeef1a9,0xeff0a9,0xeff0a8,0xeff0a7,0xf0efa6,0xf0efa5,0xf1efa4,0xf1eea4,0xf1eea3,0xf2eea2,0xf2eda2,0xf2eda1,0xf3eda1,0xf3eda0,0xf3ec9f, 49 | 0xf4ec9e,0xf4eb9d,0xf5eb9c,0xf5ea9b,0xf5ea9a,0xf6ea9a,0xf6e999,0xf6e998,0xf7e898,0xf7e897,0xf7e896,0xf7e796,0xf8e795,0xf8e794,0xf8e694,0xf8e693, 50 | 0xf9e693,0xf9e692,0xf9e592,0xf9e591,0xfae590,0xfae490,0xfae48f,0xfae38e,0xfbe38e,0xfbe38d,0xfbe28c,0xfbe28b,0xfce18b,0xfce18a,0xfce189,0xfce089, 51 | 0xfde088,0xfddf87,0xfddf86,0xfdde86,0xfede85,0xfede84,0xfedd84,0xfedd83,0xffdc82,0xffdc81,0xffdb81,0xffdb80,0xffda7f,0xffda7e,0xffd97e,0xffd97d, 52 | 0xffd87c,0xffd87b,0xffd77b,0xffd77a,0xffd679,0xffd678,0xffd578,0xffd577,0xffd476,0xffd475,0xffd375,0xffd374,0xffd273,0xffd172,0xffd171,0xffd071, 53 | 0xffd070,0xffcf6f,0xffcf6e,0xffce6e,0xffce6d,0xffcd6d,0xffcd6c,0xffcc6b,0xffcb6a,0xffcb69,0xffca69,0xffca68,0xffc967,0xffc866,0xffc865,0xffc765, 54 | 0xffc764,0xffc664,0xffc663,0xffc562,0xffc461,0xffc460,0xffc360,0xffc35f,0xffc25f,0xffc25e,0xffc15d,0xffc05c,0xffbf5b,0xffbf5a,0xffbe5a,0xffbe59, 55 | 0xffbd58,0xffbc57,0xffbc56,0xffbb56,0xffbb55,0xffba55,0xffba54,0xffb954,0xffb953,0xffb852,0xffb751,0xffb650,0xffb54f,0xffb54e,0xffb44e,0xffb44d, 56 | 0xffb34d,0xffb34c,0xffb24c,0xffb24b,0xffb14b,0xffb14a,0xffb049,0xffaf48,0xffae47,0xffad47,0xffad46,0xffac46,0xffac45,0xffab45,0xffab44,0xffaa43, 57 | 0xffa942,0xffa841,0xffa740,0xffa63f,0xffa53e,0xffa43d,0xffa33c,0xffa23b,0xffa13a,0xffa139,0xffa039,0xffa038,0xff9f38,0xff9f37,0xff9e37,0xff9e36, 58 | 0xff9d36,0xff9c35,0xff9b34,0xff9a33,0xff9932,0xff9831,0xff9730,0xff962f,0xff952e,0xff942e,0xff942d,0xff932d,0xff932c,0xff922c,0xff922b,0xff912b, 59 | 0xff912a,0xff902a,0xff9029,0xff8f29,0xff8f28,0xff8e28,0xff8d27,0xff8c26,0xff8b25,0xff8a25,0xff8a24,0xff8924,0xff8923,0xff8823,0xff8722,0xff8621, 60 | 0xff8520,0xff841f,0xff831f,0xff831e,0xff821e,0xff821d,0xff811d,0xff811c,0xff801c,0xff7f1b,0xff7e1a,0xff7d19,0xff7c19,0xff7b18,0xff7a17,0xff7917, 61 | 0xff7916,0xff7816,0xff7815,0xff7715,0xff7714,0xff7614,0xff7513,0xff7412,0xff7312,0xff7311,0xff7211,0xff7110,0xff7010,0xff700f,0xff6f0f,0xff6f0e, 62 | 0xff6e0e,0xff6d0d,0xff6c0c,0xff6b0c,0xff6b0b,0xff6a0b,0xff690a,0xff680a,0xff6809,0xff6709,0xff6608,0xff6508,0xff6507,0xff6407,0xff6406,0xff6306, 63 | 0xff6205,0xff6105,0xff6104,0xff6004,0xff5f03,0xff5e03,0xff5e02,0xff5d02,0xff5c01,0xff5b01,0xff5b00,0xff5a00,0xff5900,0xff5800,0xff5700,0xff5600, 64 | 0xfe5500,0xfe5400,0xfe5300,0xfe5200,0xfd5200,0xfd5100,0xfd5000,0xfd4f00,0xfc4f00,0xfc4e00,0xfc4d00,0xfc4c00,0xfb4c00,0xfb4b00,0xfb4a00,0xfb4900, 65 | 0xfa4900,0xfa4800,0xfa4700,0xf94600,0xf94500,0xf94400,0xf84300,0xf84200,0xf84100,0xf74100,0xf74000,0xf73f00,0xf63e00,0xf63d00,0xf63c00,0xf53c00, 66 | 0xf53b00,0xf53a00,0xf43900,0xf43800,0xf43700,0xf33700,0xf33600,0xf33500,0xf23500,0xf23400,0xf23300,0xf13200,0xf13100,0xf03100,0xf03000,0xf02f00, 67 | 0xef2e00,0xef2d00,0xee2c00,0xee2b00,0xed2a00,0xed2900,0xec2800,0xec2700,0xeb2700,0xeb2600,0xeb2500,0xea2500,0xea2400,0xea2300,0xe92300,0xe92200, 68 | 0xe82100,0xe82000,0xe72000,0xe71f00,0xe71e00,0xe61e00,0xe61d00,0xe51c00,0xe51b00,0xe41b00,0xe41a00,0xe31900,0xe31800,0xe21800,0xe21700,0xe21600, 69 | 0xe11600,0xe11500,0xe01500,0xe01400,0xdf1300,0xdf1200,0xde1200,0xde1100,0xdd1100,0xdd1000,0xdc0f00,0xdb0e00,0xdb0d00,0xda0d00,0xda0c00,0xd90c00, 70 | 0xd90b00,0xd80b00,0xd80a00,0xd70a00,0xd70900,0xd60900,0xd60800,0xd50800,0xd50700,0xd40700,0xd40600,0xd30600,0xd30500,0xd20500,0xd20400,0xd10400, 71 | 0xd10300,0xd00300,0xd00200,0xcf0200,0xce0100,0xcd0100,0xcd0000,0xcc0000,0xcb0000,0xca0000,0xc90000,0xc80000,0xc70000,0xc60000,0xc50000,0xc40000, 72 | 0xc30000,0xc20000,0xc10000,0xc00000,0xbf0000,0xbe0000,0xbd0000,0xbc0000,0xbb0000,0xba0000,0xb90000,0xb80000,0xb70000,0xb60000,0xb50000,0xb40001, 73 | 0xb40002,0xb30002,0xb30003,0xb20004,0xb20005,0xb10005,0xb10006,0xb00007,0xb00008,0xaf0009,0xaf000a,0xae000a,0xae000b,0xad000c,0xad000d,0xac000e, 74 | 0xac000f,0xab000f,0xab0010,0xaa0011,0xaa0012,0xa90013,0xa90014,0xa80015,0xa80016,0xa70017,0xa70018,0xa60019,0xa6001a,0xa5001b,0xa5001c,0xa4001d}; 75 | 76 | 77 | private static int convert(double temperature, double minimumTemperature, double maximimumTemperature){ 78 | //map to 0-1023 (inclusive) 79 | int lookup = (int) Math.round(((temperature - minimumTemperature) / (maximimumTemperature - minimumTemperature)) * 1023); 80 | lookup = Math.min(lookup, 1023); 81 | lookup = Math.max(lookup, 0); 82 | 83 | return (lookupTable[lookup] | 0xFF << 24); 84 | 85 | } 86 | 87 | @Override 88 | public double getDefaultMinTemperature() { 89 | return -5.0; 90 | } 91 | 92 | @Override 93 | public double getDefaultMaxTemperature() { 94 | return 50.0; 95 | } 96 | 97 | @ColorInt 98 | @Override 99 | public int temperatureToColor(double temperature, double min, double max) { 100 | return convert(temperature, min, max); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/Palette/ThermalPalette.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera.Palette; 2 | 3 | import android.content.Context; 4 | import android.preference.PreferenceManager; 5 | import android.util.Log; 6 | 7 | import androidx.annotation.ColorInt; 8 | 9 | public abstract class ThermalPalette { 10 | 11 | public static ThermalPalette getCurrentSelectedPalette(Context ctx) { 12 | 13 | String currentPalette = PreferenceManager.getDefaultSharedPreferences(ctx).getString("thermal_palette", "RainbowPalette"); 14 | 15 | //Try to get ThermalPalette from value... 16 | String className = ThermalPalette.class.getPackage().getName() + "." + currentPalette; 17 | try { 18 | return (ThermalPalette) Class.forName(className).newInstance(); 19 | } catch(Exception ex){ 20 | Log.d("ThermalPalette", "Problem with getting current selected palette: " + ex.getMessage()); 21 | return new RainbowPalette(); 22 | } 23 | 24 | } 25 | 26 | abstract public double getDefaultMinTemperature(); 27 | abstract public double getDefaultMaxTemperature(); 28 | 29 | @ColorInt 30 | public int temperatureToColor(double temperature) { 31 | return temperatureToColor(temperature, getDefaultMinTemperature(), getDefaultMaxTemperature()); 32 | } 33 | 34 | public abstract int temperatureToColor(double temperature, double minTemperature, double maxTemperature); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/Palette/WhiteHotPalette.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera.Palette; 2 | 3 | import android.graphics.Color; 4 | 5 | public class WhiteHotPalette extends ThermalPalette { 6 | 7 | 8 | @Override 9 | public double getDefaultMinTemperature() { 10 | return 5; 11 | } 12 | 13 | @Override 14 | public double getDefaultMaxTemperature() { 15 | return 45; 16 | } 17 | 18 | @Override 19 | public int temperatureToColor(double temperature, double minTemperature, double maxTemperature) { 20 | 21 | double tempPercent = (temperature - minTemperature) / (maxTemperature - minTemperature); 22 | if(tempPercent > 1.0) tempPercent = 1.0; 23 | else if(tempPercent < 0.0) tempPercent = 0.0; 24 | 25 | int whiteLevel = (int) Math.round(tempPercent * 255); 26 | 27 | return Color.argb(255, whiteLevel, whiteLevel, whiteLevel); 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/Protocol.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedList; 5 | import java.util.Queue; 6 | 7 | class Protocol { 8 | 9 | //two interfaces: 10 | //1 for sending data 11 | //1 for receiving new responses 12 | interface ISender{ 13 | void sendBytes(byte[] bytesToSend); 14 | } 15 | ISender sender = null; 16 | 17 | interface IResponseListener { 18 | void onResponse(Queue q); 19 | } 20 | IResponseListener responseListener = null; 21 | 22 | 23 | public Protocol(ISender sender, IResponseListener responseListener){ 24 | this.sender = sender; 25 | this.responseListener = responseListener; 26 | } 27 | 28 | 29 | public static final int COMMAND_HEADER_SIZE = 3; 30 | public static final int NO_COMMAND = 0xFF; 31 | 32 | //commandCode 33 | public static final int CMD_PING = 0x00; 34 | public static final int CMD_DUMP_EE = 0x01; 35 | public static final int CMD_GET_FRAME_DATA = 0x02; 36 | public static final int CMD_SET_RESOLUTION = 0x03; 37 | public static final int CMD_GET_CUR_RESOLUTION = 0x04; 38 | public static final int CMD_SET_REFRESH_RATE = 0x05; 39 | public static final int CMD_GET_REFRESH_RATE = 0x06; 40 | public static final int CMD_SET_MODE = 0x07; 41 | public static final int CMD_GET_CUR_MODE = 0x08; 42 | public static final int CMD_SET_AUTO_FRAME_DATA_SENDING = 0x09; 43 | public static final int CMD_GET_FIRMWARE_VERSION = 0x0A; 44 | public static final int CMD_JUMP_TO_BOOTLOADER = 0x0B; 45 | 46 | 47 | 48 | public static class CmdStruct{ 49 | int commandCode; 50 | int dataLength; 51 | ArrayList data = new ArrayList<>(); 52 | } 53 | 54 | 55 | public static final int RESPONSE_HEADER_SIZE = 4; 56 | public static final int NO_RESPONSE = 0xFF; 57 | 58 | //responseCode 59 | public static final int RSP_PING = 0x00; 60 | public static final int RSP_DUMP_EE = 0x01; 61 | public static final int RSP_GET_FRAME_DATA = 0x02; 62 | public static final int RSP_SET_RESOLUTION = 0x03; 63 | public static final int RSP_GET_CUR_RESOLUTION = 0x04; 64 | public static final int RSP_SET_REFRESH_RATE = 0x05; 65 | public static final int RSP_GET_REFRESH_RATE = 0x06; 66 | public static final int RSP_SET_MODE = 0x07; 67 | public static final int RSP_GET_CUR_MODE = 0x08; 68 | public static final int RSP_SET_AUTO_FRAME_DATA_SENDING = 0x09; 69 | public static final int RSP_GET_FIRMWARE_VERSION = 0x0A; 70 | public static final int RSP_JUMP_TO_BOOTLOADER = 0x0B; 71 | 72 | //dataCode 73 | public static final int CODE_OK = 0; 74 | public static final int CODE_NACK = -1; 75 | public static final int CODE_WRITTEN_VALUE_NOT_SAME = -2; 76 | public static final int CODE_I2C_FREQ_TOO_LOW = -8; 77 | 78 | public static class RspStruct{ 79 | int responseCode; 80 | int dataCode; 81 | int dataLength; 82 | ArrayList data = new ArrayList<>(); 83 | } 84 | 85 | 86 | //Refresh rates 87 | public enum RefreshRate { 88 | HZ_MIN (0), 89 | HZ_1 (1), 90 | HZ_2 (2), 91 | HZ_4 (3), 92 | HZ_8 (4), 93 | HZ_16 (5), 94 | HZ_32 (6), 95 | HZ_64 (7); 96 | 97 | private final int rate; 98 | RefreshRate(int rate) { this.rate = rate; } 99 | 100 | public int getValue() { return rate; } 101 | } 102 | 103 | //Resolution 104 | public enum Resolution { 105 | BIT_16 (0), 106 | BIT_17 (1), 107 | BIT_18 (2), 108 | BIT_19 (3); 109 | 110 | private final int resolution; 111 | Resolution(int resolution) { this.resolution = resolution; } 112 | public int getValue() { return resolution; } 113 | } 114 | 115 | //FirmwareVersion structure 116 | public static class FirmwareVersion { 117 | int major; 118 | int minor; 119 | int revision; 120 | 121 | public static FirmwareVersion parse(ArrayList arr){ 122 | FirmwareVersion version = new FirmwareVersion(); 123 | 124 | if(arr.size() < 12) return null; 125 | version.major = arr.get(0) << 24 | arr.get(1) << 16 | arr.get(2) << 8 | arr.get(3); 126 | version.minor = arr.get(4) << 24 | arr.get(5) << 16 | arr.get(6) << 8 | arr.get(7); 127 | version.revision = arr.get(8) << 24 | arr.get(9) << 16 | arr.get(10) << 8 | arr.get(11); 128 | return version; 129 | } 130 | 131 | public String toString(){ 132 | return major + "." + minor + "." + revision; 133 | } 134 | } 135 | 136 | public enum ScanMode { 137 | CHESS (1), 138 | INTERLEAVED (0); 139 | 140 | private final int mode; 141 | ScanMode(int mode) { this.mode = mode; } 142 | public int getValue() { return mode; } 143 | } 144 | 145 | 146 | Queue messageBuffer = new LinkedList(); 147 | 148 | Queue responseQueue = new LinkedList(); 149 | 150 | 151 | public void handleNewData(byte[] data){ 152 | 153 | //received a chunk of message. store in message buffer 154 | for(int i = 0; i < data.length; i++ ){ 155 | messageBuffer.add(data[i]); 156 | } 157 | 158 | //count number of messages 159 | int numDelimiters = 0; 160 | for(Byte b : messageBuffer){ 161 | //if current byte is a message delimiter (0x00), increase counter 162 | if(b == 0x00){ 163 | numDelimiters++; 164 | } 165 | } 166 | 167 | //extract messages 168 | for(int i = 0; i < numDelimiters; i++){ 169 | //Add data to array 170 | ArrayList arrayOfEncoded = new ArrayList(); 171 | byte b; 172 | while( messageBuffer.size() > 0 && (b = messageBuffer.poll()) != 0x00){ 173 | char c = (char) (((char) b) & 0xFF); 174 | arrayOfEncoded.add(c); 175 | } 176 | 177 | char[] cobsEncoded = new char[arrayOfEncoded.size()]; 178 | for(int k = 0; k< arrayOfEncoded.size(); k++){ 179 | cobsEncoded[k] = arrayOfEncoded.get(k); 180 | } 181 | 182 | char[] cobsDecoded = new char[Cobs.decodeDstBufMaxLen(cobsEncoded.length)]; 183 | 184 | //decode message 185 | Cobs.DecodeResult decodeResult = Cobs.decode(cobsDecoded, cobsDecoded.length, cobsEncoded, cobsEncoded.length); 186 | 187 | //create empty response 188 | Protocol.RspStruct response = new Protocol.RspStruct(); 189 | //set to no response 190 | response.responseCode = Protocol.NO_RESPONSE; 191 | 192 | 193 | //check size 194 | if(decodeResult.outLen >= Protocol.RESPONSE_HEADER_SIZE){ 195 | //size ok 196 | 197 | response.responseCode = ((int) cobsDecoded[0]) & 0xFF; 198 | response.dataCode = ((int) cobsDecoded[1]) & 0xFF; 199 | response.dataLength = (( ( (int) cobsDecoded[2]) & 0xFF) << 8) | ( ((int) cobsDecoded[3]) & 0xFF); 200 | 201 | //check data length 202 | if(response.dataLength == decodeResult.outLen - Protocol.RESPONSE_HEADER_SIZE){ 203 | 204 | // data length ok, copy data to response structure 205 | for(int f = 0; f < response.dataLength; f++){ 206 | int counter = Protocol.RESPONSE_HEADER_SIZE + f; 207 | response.data.add(((int) cobsDecoded[counter]) & 0xFF); 208 | } 209 | 210 | //response successfuly created, add it to queue 211 | responseQueue.add(response); 212 | 213 | //notify 214 | if(responseListener != null) responseListener.onResponse(responseQueue); 215 | 216 | } 217 | 218 | 219 | } 220 | 221 | 222 | } 223 | } 224 | 225 | 226 | 227 | public void sendCommand(Protocol.CmdStruct cmd){ 228 | //prepare to send command 229 | //create char array 230 | 231 | if(cmd.dataLength != cmd.data.size()) return; 232 | 233 | char[] message = new char[Protocol.COMMAND_HEADER_SIZE + cmd.dataLength]; 234 | message[0] = (char) (cmd.commandCode); 235 | message[1] = (char) ((cmd.dataLength >> 8) & 0xFF); 236 | message[2] = (char) (cmd.dataLength & 0xFF); 237 | 238 | for(int i = 0; i finish()); 25 | 26 | } 27 | 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.os.Bundle; 4 | import android.util.Log; 5 | 6 | import com.themarpe.openthermalcamera.Palette.ThermalPalette; 7 | 8 | import com.themarpe.openthermalcamera.R; 9 | 10 | import androidx.preference.ListPreference; 11 | import androidx.preference.Preference; 12 | import androidx.preference.PreferenceFragmentCompat; 13 | import androidx.preference.PreferenceManager; 14 | import androidx.preference.SeekBarPreference; 15 | 16 | public class SettingsFragment extends PreferenceFragmentCompat { 17 | 18 | OTC otcHandle; 19 | 20 | private static final String TAG = "SettingsFragment"; 21 | 22 | @Override 23 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 24 | setPreferencesFromResource(R.xml.settings_screen, rootKey); 25 | 26 | otcHandle = new OTC(getContext(), null); 27 | 28 | //crashlytics test button 29 | 30 | /* 31 | findPreference("btn_crashlytics").setOnPreferenceClickListener(View -> { 32 | Crashlytics.getInstance().crash(); 33 | return true; 34 | }); 35 | 36 | findPreference("btn_bootloader").setOnPreferenceClickListener(View -> { 37 | otcHandle.jumpToBootloader(); 38 | 39 | try { 40 | Thread.sleep(2000); 41 | } catch (Exception ex){ 42 | 43 | } 44 | 45 | return true; 46 | }); 47 | */ 48 | 49 | 50 | //Emissivity 51 | SeekBarPreference emissivity = findPreference("emissivity"); 52 | 53 | //thermal palette selection 54 | ThermalPaletteChangeListener tpcl = new ThermalPaletteChangeListener(); 55 | 56 | String currentValue = PreferenceManager.getDefaultSharedPreferences(getContext()).getString("thermal_palette",""); 57 | findPreference("thermal_palette").setOnPreferenceChangeListener(tpcl); 58 | tpcl.onPreferenceChange(findPreference("thermal_palette"), currentValue); 59 | 60 | SeekBarPreference customRangeMin = (SeekBarPreference) findPreference("custom_range_min"); 61 | SeekBarPreference customRangeMax = (SeekBarPreference) findPreference("custom_range_max"); 62 | 63 | 64 | //depencency on dynamic_range 65 | findPreference("dynamic_range_enabled").setOnPreferenceChangeListener((preference, newValue) -> { 66 | findPreference("dynamic_range_min_difference").setEnabled((boolean)newValue); 67 | return true; 68 | }); 69 | 70 | 71 | //CUSTOM TEMPERATURE RANGE 72 | customRangeMin.setOnPreferenceChangeListener((preference, newValue) -> { 73 | int cur = (Integer) newValue; 74 | if(cur > customRangeMax.getValue()){ 75 | customRangeMax.setValue(cur + 1); 76 | } 77 | return true; 78 | }); 79 | 80 | customRangeMax.setOnPreferenceChangeListener(((preference, newValue) -> { 81 | int cur = (Integer) newValue; 82 | if(cur < customRangeMin.getValue()){ 83 | customRangeMin.setValue(cur - 1); 84 | } 85 | return true; 86 | })); 87 | 88 | } 89 | 90 | class ThermalPaletteChangeListener implements Preference.OnPreferenceChangeListener { 91 | 92 | @Override 93 | public boolean onPreferenceChange(Preference preference, Object newValue) { 94 | ListPreference thermalPeletteList = (ListPreference) preference; 95 | 96 | try{ 97 | String val = (String) newValue; 98 | //Try to get ThermalPalette from value... 99 | String className = ThermalPalette.class.getPackage().getName()+"."+val; 100 | Log.d("SettingsFragment", "Trying to load className: " + className); 101 | ThermalPalette palette = (ThermalPalette) Class.forName(className).newInstance(); 102 | 103 | double min = palette.getDefaultMinTemperature(); 104 | double max = palette.getDefaultMaxTemperature(); 105 | 106 | findPreference("thermal_palette_default_range_info").setTitle("Default Range is from " + min + " to " + max + "."); 107 | 108 | //current selected to summary 109 | CharSequence entry = thermalPeletteList.getEntries()[thermalPeletteList.findIndexOfValue(val)]; 110 | Log.d("SettingsFragment", "Changing summary to: " + entry); 111 | ((ListPreference)preference).setSummary(entry); 112 | 113 | } catch (Exception ex) { 114 | ex.printStackTrace(); 115 | } 116 | 117 | 118 | 119 | return true; 120 | 121 | } 122 | } 123 | 124 | 125 | private static void bindPreferenceSummaryToValue(Preference pref) { 126 | // Set the listener to watch for value changes. 127 | pref.setOnPreferenceChangeListener((preference1, newValue) -> { 128 | String newSummary = PreferenceManager.getDefaultSharedPreferences(preference1.getContext()).getString(preference1.getKey(), ""); 129 | Log.d("SettingsFragment", "Changing summary to: " + newSummary); 130 | preference1.setSummary(newSummary); 131 | return true; 132 | }); 133 | 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/SnapOnScrollListener.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | 4 | import android.view.View; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.recyclerview.widget.RecyclerView; 8 | import androidx.recyclerview.widget.SnapHelper; 9 | 10 | /* 11 | Adapted from Nick Rout's snippet 12 | https://medium.com/over-engineering/detecting-snap-changes-with-androids-recyclerview-snaphelper-9e9f5e95c424 13 | */ 14 | 15 | class SnapOnScrollListener extends RecyclerView.OnScrollListener{ 16 | 17 | enum Behavior { 18 | NOTIFY_ON_SCROLL, 19 | NOTIFY_ON_SCROLL_STATE_IDLE; 20 | } 21 | 22 | interface OnSnapPositionChangeListener{ 23 | void onSnapPositionChange(int position); 24 | } 25 | 26 | SnapOnScrollListener(SnapHelper sh, Behavior b, OnSnapPositionChangeListener cl){ 27 | snapHelper = sh; 28 | behavior = b; 29 | listener = cl; 30 | } 31 | 32 | int snapPosition = RecyclerView.NO_POSITION; 33 | private SnapHelper snapHelper; 34 | private Behavior behavior; 35 | private OnSnapPositionChangeListener listener = null; 36 | 37 | 38 | int getSnapPosition(SnapHelper sh, RecyclerView recyclerView) { 39 | RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); 40 | if(layoutManager == null) return RecyclerView.NO_POSITION; 41 | View snapView = snapHelper.findSnapView(layoutManager); 42 | if(snapView == null) return RecyclerView.NO_POSITION; 43 | return layoutManager.getPosition(snapView); 44 | } 45 | 46 | @Override 47 | public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 48 | super.onScrolled(recyclerView, dx, dy); 49 | if (behavior == Behavior.NOTIFY_ON_SCROLL) { 50 | maybeNotifySnapPositionChange(recyclerView); 51 | } 52 | } 53 | 54 | @Override 55 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 56 | super.onScrollStateChanged(recyclerView, newState); 57 | if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE 58 | && newState == RecyclerView.SCROLL_STATE_IDLE) { 59 | maybeNotifySnapPositionChange(recyclerView); 60 | } 61 | } 62 | 63 | private void maybeNotifySnapPositionChange(RecyclerView recyclerView) { 64 | int curPosition = getSnapPosition(snapHelper, recyclerView); 65 | if (curPosition != snapPosition) { 66 | if(listener != null){ 67 | listener.onSnapPositionChange(curPosition); 68 | } 69 | snapPosition = curPosition; 70 | } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/themarpe/openthermalcamera/UsbService.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import android.app.PendingIntent; 4 | import android.app.Service; 5 | import android.content.BroadcastReceiver; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.IntentFilter; 9 | import android.content.res.XmlResourceParser; 10 | import android.hardware.usb.UsbDevice; 11 | import android.hardware.usb.UsbDeviceConnection; 12 | import android.hardware.usb.UsbManager; 13 | import android.os.Binder; 14 | import android.os.Handler; 15 | import android.os.IBinder; 16 | import android.util.Log; 17 | 18 | import com.felhr.usbserial.CDCSerialDevice; 19 | import com.felhr.usbserial.UsbSerialDevice; 20 | import com.felhr.usbserial.UsbSerialInterface; 21 | 22 | import org.xmlpull.v1.XmlPullParser; 23 | 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | import com.themarpe.openthermalcamera.R; 28 | 29 | public class UsbService extends Service { 30 | 31 | public static int OTC_PID = 0x2170; 32 | public static int OTC_VID = 0x1209; 33 | 34 | 35 | public static final String ACTION_USB_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED"; 36 | public static final String ACTION_USB_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED"; 37 | 38 | private static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; 39 | 40 | public static final String ACTION_USB_READY = PACKAGE_NAME + ".USB_READY"; 41 | public static final String ACTION_USB_NOT_SUPPORTED = PACKAGE_NAME + ".USB_NOT_SUPPORTED"; 42 | public static final String ACTION_NO_USB = PACKAGE_NAME + ".NO_USB"; 43 | public static final String ACTION_USB_PERMISSION_GRANTED = PACKAGE_NAME + ".USB_PERMISSION_GRANTED"; 44 | public static final String ACTION_USB_PERMISSION_NOT_GRANTED = PACKAGE_NAME + ".USB_PERMISSION_NOT_GRANTED"; 45 | public static final String ACTION_USB_DISCONNECTED = PACKAGE_NAME + ".USB_DISCONNECTED"; 46 | 47 | 48 | public static final int MESSAGE_FROM_SERIAL_PORT = 0; 49 | private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; 50 | private static final int BAUD_RATE = 115200; // BaudRate. Change this value if you need 51 | public static boolean SERVICE_CONNECTED = false; 52 | 53 | private IBinder binder = new UsbBinder(); 54 | 55 | private Context context; 56 | private Handler mHandler; 57 | private UsbManager usbManager; 58 | private UsbDevice device; 59 | private UsbDeviceConnection connection; 60 | private UsbSerialDevice serialPort; 61 | 62 | private boolean serialPortConnected; 63 | /* 64 | * Data received from serial port will be received here. Just populate onReceivedData with your code 65 | * In this particular example. byte stream is converted to String and send to UI thread to 66 | * be treated there. 67 | */ 68 | private UsbSerialInterface.UsbReadCallback mCallback = new UsbSerialInterface.UsbReadCallback() { 69 | @Override 70 | public void onReceivedData(byte[] arg0) { 71 | if (mHandler != null) mHandler.obtainMessage(MESSAGE_FROM_SERIAL_PORT, arg0).sendToTarget(); 72 | } 73 | }; 74 | 75 | 76 | /* 77 | * Different notifications from OS will be received here (USB attached, detached, permission responses...) 78 | * About BroadcastReceiver: http://developer.android.com/reference/android/content/BroadcastReceiver.html 79 | */ 80 | private final BroadcastReceiver usbReceiver = new BroadcastReceiver() { 81 | @Override 82 | public void onReceive(Context arg0, Intent arg1) { 83 | if (arg1.getAction().equals(ACTION_USB_PERMISSION)) { 84 | boolean granted = arg1.getExtras().getBoolean(UsbManager.EXTRA_PERMISSION_GRANTED); 85 | if (granted) // User accepted our USB connection. Try to open the device as a serial port 86 | { 87 | Intent intent = new Intent(ACTION_USB_PERMISSION_GRANTED); 88 | arg0.sendBroadcast(intent); 89 | connection = usbManager.openDevice(device); 90 | new ConnectionThread().start(); 91 | } else // User not accepted our USB connection. Send an Intent to the Main Activity 92 | { 93 | Intent intent = new Intent(ACTION_USB_PERMISSION_NOT_GRANTED); 94 | arg0.sendBroadcast(intent); 95 | } 96 | } else if (arg1.getAction().equals(ACTION_USB_ATTACHED)) { 97 | if (!serialPortConnected) 98 | findSerialPortDevice(); // A USB device has been attached. Try to open it as a Serial port 99 | } else if (arg1.getAction().equals(ACTION_USB_DETACHED)) { 100 | // Usb device was disconnected. send an intent to the Main Activity 101 | Intent intent = new Intent(ACTION_USB_DISCONNECTED); 102 | arg0.sendBroadcast(intent); 103 | if (serialPortConnected) { 104 | serialPort.close(); 105 | } 106 | serialPortConnected = false; 107 | } 108 | } 109 | }; 110 | 111 | /* 112 | * onCreate will be executed when service is started. It configures an IntentFilter to listen for 113 | * incoming Intents (USB ATTACHED, USB DETACHED...) and it tries to open a serial port. 114 | */ 115 | @Override 116 | public void onCreate() { 117 | this.context = this; 118 | serialPortConnected = false; 119 | UsbService.SERVICE_CONNECTED = true; 120 | setFilter(); 121 | usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); 122 | findSerialPortDevice(); 123 | } 124 | 125 | /* MUST READ about services 126 | * http://developer.android.com/guide/components/services.html 127 | * http://developer.android.com/guide/components/bound-services.html 128 | */ 129 | @Override 130 | public IBinder onBind(Intent intent) { 131 | return binder; 132 | } 133 | 134 | @Override 135 | public int onStartCommand(Intent intent, int flags, int startId) { 136 | return Service.START_NOT_STICKY; 137 | } 138 | 139 | @Override 140 | public void onDestroy() { 141 | super.onDestroy(); 142 | try{ 143 | unregisterReceiver(usbReceiver); 144 | } catch (IllegalArgumentException e){ 145 | //that's cool 146 | } 147 | UsbService.SERVICE_CONNECTED = false; 148 | } 149 | 150 | /* 151 | * This function will be called from MainActivity to write data through Serial Port 152 | */ 153 | public void write(byte[] data) { 154 | if (serialPort != null) 155 | serialPort.write(data); 156 | } 157 | 158 | public void setHandler(Handler mHandler) { 159 | this.mHandler = mHandler; 160 | } 161 | 162 | private void findSerialPortDevice() { 163 | // This snippet will try to open the first encountered usb device connected, excluding usb root hubs 164 | HashMap usbDevices = usbManager.getDeviceList(); 165 | if (!usbDevices.isEmpty()) { 166 | boolean keep = true; 167 | for (Map.Entry entry : usbDevices.entrySet()) { 168 | device = entry.getValue(); 169 | int deviceVID = device.getVendorId(); 170 | int devicePID = device.getProductId(); 171 | 172 | // 173 | try { 174 | XmlResourceParser parser = context.getResources().getXml(R.xml.otc_device); 175 | int event = parser.getEventType(); 176 | while (event != XmlPullParser.END_DOCUMENT) { 177 | if ("usb-device".equals(parser.getName())) { 178 | if ("otc".equals(parser.getAttributeValue(null, "name"))) { 179 | OTC_VID = Integer.parseInt(parser.getAttributeValue(null, "vendor-id")); 180 | OTC_PID = Integer.parseInt(parser.getAttributeValue(null, "product-id")); 181 | Log.d("UsbService", "Succesfully extracted VID/PID from otc_device.xml: VID: " + OTC_VID + " PID: " + OTC_PID); 182 | break; 183 | } 184 | } 185 | event = parser.next(); 186 | } 187 | } catch (Exception ex){ 188 | Log.d("UsbService", "Cannot get VID/PID pair from otc_device_device.xml"); 189 | } 190 | 191 | if(devicePID == OTC_PID && deviceVID == OTC_VID){ 192 | 193 | // There is a device connected to our Android device. Try to open it as a Serial Port. 194 | requestUserPermission(); 195 | keep = false; 196 | } else { 197 | connection = null; 198 | device = null; 199 | } 200 | 201 | if (!keep) 202 | break; 203 | } 204 | if (!keep) { 205 | // There is no USB devices connected (but usb host were listed). Send an intent to MainActivity. 206 | Intent intent = new Intent(ACTION_NO_USB); 207 | sendBroadcast(intent); 208 | } 209 | } else { 210 | // There is no USB devices connected. Send an intent to MainActivity 211 | Intent intent = new Intent(ACTION_NO_USB); 212 | sendBroadcast(intent); 213 | } 214 | } 215 | 216 | private void setFilter() { 217 | IntentFilter filter = new IntentFilter(); 218 | filter.addAction(ACTION_USB_PERMISSION); 219 | filter.addAction(ACTION_USB_DETACHED); 220 | filter.addAction(ACTION_USB_ATTACHED); 221 | registerReceiver(usbReceiver, filter); 222 | } 223 | 224 | /* 225 | * Request user permission. The response will be received in the BroadcastReceiver 226 | */ 227 | private void requestUserPermission() { 228 | PendingIntent mPendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); 229 | usbManager.requestPermission(device, mPendingIntent); 230 | } 231 | 232 | public class UsbBinder extends Binder { 233 | public UsbService getService() { 234 | return UsbService.this; 235 | } 236 | } 237 | 238 | /* 239 | * A simple thread to open a serial port. 240 | * Although it should be a fast operation. moving usb operations away from UI thread is a good thing. 241 | */ 242 | private class ConnectionThread extends Thread { 243 | @Override 244 | public void run() { 245 | serialPort = UsbSerialDevice.createUsbSerialDevice(device, connection); 246 | if (serialPort != null) { 247 | if (serialPort.open()) { 248 | serialPortConnected = true; 249 | serialPort.setBaudRate(BAUD_RATE); 250 | serialPort.setDataBits(UsbSerialInterface.DATA_BITS_8); 251 | serialPort.setStopBits(UsbSerialInterface.STOP_BITS_1); 252 | serialPort.setParity(UsbSerialInterface.PARITY_NONE); 253 | /** 254 | * Current flow control Options: 255 | * UsbSerialInterface.FLOW_CONTROL_OFF 256 | */ 257 | serialPort.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF); 258 | serialPort.read(mCallback); 259 | 260 | // 261 | // Some Arduinos would need some sleep because firmware wait some time to know whether a new sketch is going 262 | // to be uploaded or not 263 | //Thread.sleep(2000); // sleep some. YMMV with different chips. 264 | 265 | // Everything went as expected. Send an intent to MainActivity 266 | Intent intent = new Intent(ACTION_USB_READY); 267 | context.sendBroadcast(intent); 268 | } else { 269 | Log.d("UsbService", "Problems..."); 270 | // Serial port could not be opened, maybe an I/O error or if CDC driver was chosen, it does not really fit 271 | // Send an Intent to Main Activity 272 | if (serialPort instanceof CDCSerialDevice) { 273 | //Intent intent = new Intent(ACTION_CDC_DRIVER_NOT_WORKING); 274 | //context.sendBroadcast(intent); 275 | } else { 276 | //Intent intent = new Intent(ACTION_USB_DEVICE_NOT_WORKING); 277 | //context.sendBroadcast(intent); 278 | } 279 | } 280 | } else { 281 | // No driver for given device, even generic CDC driver could not be loaded 282 | Intent intent = new Intent(ACTION_USB_NOT_SUPPORTED); 283 | context.sendBroadcast(intent); 284 | } 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/arrow_back.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_delete_outline_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_image_search_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_share_24px.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gallery.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_insert_otc_ani_frame_1.xml: -------------------------------------------------------------------------------- 1 | 6 | 15 | 23 | 32 | 41 | 42 | 43 | 52 | 61 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_insert_otc_ani_frame_2.xml: -------------------------------------------------------------------------------- 1 | 6 | 15 | 23 | 32 | 41 | 42 | 43 | 52 | 61 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/insert_otc_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_gallery.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 20 | 21 | 27 | 28 | 29 | 30 | 35 | 36 | 40 | 41 | 45 | 46 | 47 | 48 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 73 | 74 | 75 | 79 | 80 | 89 | 90 | 91 | 95 | 96 | 105 | 106 | 107 | 108 | 112 | 113 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 18 | 19 | 20 | 35 | 36 | 37 | 38 | 43 | 44 | 49 | 50 | 55 | 56 | 57 | 58 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 84 | 85 | 86 | 91 | 92 | 103 | 104 | 107 | 108 | 109 | 115 | 116 | 122 | 123 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 145 | 146 | 147 | 148 | 154 | 155 | 156 | 160 | 161 | 174 | 175 | 176 | 177 | 181 | 182 | 195 | 196 | 197 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 17 | 18 | 26 | 27 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 0.5 Hz 7 | 1 Hz 8 | 2 Hz 9 | 4 Hz 10 | 8 Hz 11 | 16 Hz 12 | 16 | 17 | 18 | 19 | HZ_MIN 20 | HZ_1 21 | HZ_2 22 | HZ_4 23 | HZ_8 24 | HZ_16 25 | 29 | 30 | 31 | 32 | 16-bit 33 | 17-bit 34 | 18-bit 35 | 19-bit 36 | 37 | 38 | 39 | BIT_16 40 | BIT_17 41 | BIT_18 42 | BIT_19 43 | 44 | 45 | 46 | Interleaved 47 | Chess pattern 48 | 49 | 50 | 51 | INTERLEAVED 52 | CHESS 53 | 54 | 55 | 56 | Rainbow Palette 57 | White Hot Palette 58 | Dark Hot Palette 59 | 60 | 61 | 62 | RainbowPalette 63 | WhiteHotPalette 64 | DarkHotPalette 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ff9800 4 | #f44336 5 | #ff9800 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #26A69A 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | refresh_rate 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Open Thermal Camera 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/xml/otc_bootloader_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/xml/otc_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/xml/settings_screen.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 20 | 21 | 27 | 28 | 35 | 36 | 40 | 41 | 45 | 46 | 53 | 54 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 72 | 73 | 78 | 79 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 97 | 98 | 106 | 107 | 108 | 109 | 110 | 118 | 119 | 126 | 127 | 128 | 135 | 136 | 143 | 144 | 145 | 146 | 147 | 161 | 162 | -------------------------------------------------------------------------------- /app/src/test/java/com/themarpe/openthermalcamera/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.themarpe.openthermalcamera; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | maven { url 'https://maven.fabric.io/public' } 9 | 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.5.1' 13 | 14 | classpath 'com.google.gms:google-services:4.3.2' 15 | 16 | classpath 'io.fabric.tools:gradle:1.29.0' // Crashlytics plugin 17 | 18 | 19 | // NOTE: Do not place your application dependencies here; they belong 20 | // in the individual module build.gradle files 21 | } 22 | } 23 | 24 | allprojects { 25 | repositories { 26 | google() 27 | jcenter() 28 | maven { url "https://jitpack.io" } 29 | maven { url 'https://maven.fabric.io/public' } 30 | } 31 | } 32 | 33 | task clean(type: Delete) { 34 | delete rootProject.buildDir 35 | } 36 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | android.useAndroidX=true 15 | android.enableJetifier=true 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openthermalcamera/Android-Application/5e2aea04fa003a2643965277a974f35f4e1dbbe4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Oct 22 19:28:16 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------