├── .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 |
6 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------