├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── e_regular_games
│ │ └── arduator
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── e_regular_games
│ │ │ └── arduator
│ │ │ ├── AboutActivity.java
│ │ │ ├── ActivityConfig.java
│ │ │ ├── HelpActivity.java
│ │ │ ├── MainActivity.java
│ │ │ └── arduino
│ │ │ ├── ArduinoComm.java
│ │ │ ├── ArduinoCommBle.java
│ │ │ ├── ArduinoCommBt.java
│ │ │ ├── ArduinoCommManager.java
│ │ │ ├── ArduinoCommManagerAny.java
│ │ │ ├── ArduinoCommManagerBle.java
│ │ │ ├── ArduinoCommManagerBt.java
│ │ │ ├── ArduinoCommUpdater.java
│ │ │ └── Firmware.java
│ └── res
│ │ ├── drawable
│ │ ├── ic_circuit.xml
│ │ ├── ic_company_logo_square.xml
│ │ ├── ic_connections.xml
│ │ ├── ic_logo.xml
│ │ └── ic_logo_negative.xml
│ │ ├── layout
│ │ ├── activity_about.xml
│ │ ├── activity_help.xml
│ │ └── activity_main.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── e_regular_games
│ └── arduator
│ └── ExampleUnitTest.java
├── art
├── circuit.png
├── circuit.svg
├── company_logo.png
├── company_logo_square.svg
├── connections.svg
├── feature.png
├── feature.svg
├── google-play-badge.png
├── logo.svg
├── logo_negative.png
└── logo_negative.svg
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 E-Regular Games LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Arduator
2 | **A Firmware (.hex) Uploader for Arduino using Bluetooth 2.0 or 4.0LE (BLE).**
3 |
4 | Use Arduator to upload a .hex file produced by the Arduino compiler to a properly equipped **Arduino Nano or Uno** device.
5 |
6 | In App help documentation explains how to build a programming circuit and setup/connect the Arduino, bluetooth module, and programming circuit.
7 |
8 | **Only Arduino Nano and Uno are currently supported. A programming circuit is required.**
9 |
10 | ### Like the code? Want to encourage and support the developer? Buy the App; only $0.99!
11 | [
](https://play.google.com/store/apps/details?id=com.e_regular_games.arduator)
12 |
13 | ### The Programming Circuit
14 |
15 |
16 |
17 | In this diagram, STATE is an output from the Bluetooth module and RST is connected to the Arduino RST pin input.
18 |
19 | A switch is used to provide power to an Inverter logic gate. When the switch is ON and a connection is established to the Bluetooth module, the capacitor discharges pulling the RST pin to LOW temporarily. This causes the Arduino to reset and accept bootloader commands.
20 |
21 | There are programming circuit designs provided by others online. Any similar working design will function with Arduator.
22 |
23 | ### Brought to You By
24 |
25 | [
](http://e-regular-games.com)
26 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 25
5 | buildToolsVersion "25.0.0"
6 | defaultConfig {
7 | applicationId "com.e_regular_games.arduator"
8 | minSdkVersion 19
9 | targetSdkVersion 25
10 | versionCode 2
11 | versionName "1.0.0-alpha.1"
12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
13 | }
14 | buildTypes {
15 | release {
16 | minifyEnabled false
17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | compile fileTree(dir: 'libs', include: ['*.jar'])
24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
25 | exclude group: 'com.android.support', module: 'support-annotations'
26 | })
27 | compile 'com.android.support:appcompat-v7:25.3.1'
28 | compile 'com.android.support.constraint:constraint-layout:1.0.2'
29 | compile 'com.android.support:design:25.3.1'
30 | testCompile 'junit:junit:4.12'
31 | }
32 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\SRE\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/e_regular_games/arduator/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator;
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 | * Instrumentation 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() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.e_regular_games.arduator", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/AboutActivity.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator;
2 |
3 | import android.os.Bundle;
4 | import android.support.design.widget.FloatingActionButton;
5 | import android.support.design.widget.Snackbar;
6 | import android.support.v7.app.AppCompatActivity;
7 | import android.support.v7.widget.Toolbar;
8 | import android.view.MenuItem;
9 | import android.view.View;
10 |
11 | public class AboutActivity extends AppCompatActivity {
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_about);
17 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
18 | setSupportActionBar(toolbar);
19 |
20 | // add back arrow to toolbar
21 | if (getSupportActionBar() != null){
22 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
23 | getSupportActionBar().setDisplayShowHomeEnabled(true);
24 | }
25 | }
26 |
27 | @Override
28 | public boolean onOptionsItemSelected(MenuItem item) {
29 | // handle arrow click here
30 | if (item.getItemId() == android.R.id.home) {
31 | finish(); // close this activity and return to preview activity (if there is any)
32 | }
33 |
34 | return super.onOptionsItemSelected(item);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/ActivityConfig.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 |
6 | import com.e_regular_games.arduator.arduino.ArduinoComm;
7 |
8 | /**
9 | * Created by SRE on 5/29/2017.
10 | */
11 |
12 | public class ActivityConfig {
13 | private SharedPreferences preferences;
14 | private ArduinoComm arduino;
15 |
16 | private static final String fileKey ="com.e_regular_games.arduator.PREFERENCES";
17 | private static final String keyFavoriteBtDeviceAddr = "FAV_BT_DEV_ADDR";
18 | private static final String keyFavoriteBtDeviceName = "FAV_BT_DEV_NAME";
19 | private static final String keyBtMode = "BT_MODE";
20 | private static final String keyBtServiceUuid = "BT_SERV_UUID";
21 | private static final String keyBtPinCode = "BT_PIN_CODE";
22 |
23 | public ActivityConfig(Context context) {
24 | preferences = context.getSharedPreferences(fileKey, Context.MODE_PRIVATE);
25 | }
26 |
27 | public void setFavoriteBtDevice(String name, String address) {
28 | SharedPreferences.Editor edit = preferences.edit();
29 | edit.putString(keyFavoriteBtDeviceName, name);
30 | edit.putString(keyFavoriteBtDeviceAddr, address);
31 | edit.commit();
32 | }
33 |
34 | public String getFavoriteBtDeviceName() {
35 | return preferences.getString(keyFavoriteBtDeviceName, null);
36 | }
37 |
38 | // MAC address of bluetooth device
39 | public String getFavoriteBtDeviceAddr() {
40 | return preferences.getString(keyFavoriteBtDeviceAddr, null);
41 | }
42 |
43 | public void setBtMode(String mode) {
44 | SharedPreferences.Editor edit = preferences.edit();
45 | edit.putString(keyBtMode, mode);
46 | edit.commit();
47 | }
48 |
49 | public String getBtMode() {
50 | return preferences.getString(keyBtMode, "2.0");
51 | }
52 |
53 | // should be 4 characters long
54 | public void setBtServiceUuid(String uuid) {
55 | SharedPreferences.Editor edit = preferences.edit();
56 | edit.putString(keyBtServiceUuid, uuid);
57 | edit.commit();
58 | }
59 |
60 | public String getBtServiceUuid() {
61 | return preferences.getString(keyBtServiceUuid, "FFE0");
62 | }
63 |
64 | // should be 4 characters long
65 | public void setBtPinCode(String pin) {
66 | SharedPreferences.Editor edit = preferences.edit();
67 | edit.putString(keyBtPinCode, pin);
68 | edit.commit();
69 | }
70 |
71 | public String getBtPinCode() {
72 | return preferences.getString(keyBtPinCode, "1234");
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/HelpActivity.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator;
2 |
3 | import android.content.Intent;
4 | import android.graphics.BitmapFactory;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.support.design.widget.FloatingActionButton;
8 | import android.support.design.widget.Snackbar;
9 | import android.support.v7.app.AppCompatActivity;
10 | import android.support.v7.widget.Toolbar;
11 | import android.view.MenuItem;
12 | import android.view.View;
13 | import android.widget.Button;
14 | import android.widget.ImageView;
15 |
16 | public class HelpActivity extends AppCompatActivity {
17 |
18 | @Override
19 | protected void onCreate(Bundle savedInstanceState) {
20 | super.onCreate(savedInstanceState);
21 | setContentView(R.layout.activity_help);
22 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
23 | setSupportActionBar(toolbar);
24 |
25 | // add back arrow to toolbar
26 | if (getSupportActionBar() != null){
27 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
28 | getSupportActionBar().setDisplayShowHomeEnabled(true);
29 | }
30 |
31 | Button email = (Button) findViewById(R.id.help_button_email);
32 | email.setOnClickListener(new View.OnClickListener() {
33 | @Override
34 | public void onClick(View v) {
35 | Intent intent = new Intent(Intent.ACTION_SENDTO);
36 | intent.setData(Uri.parse("mailto:"));
37 | intent.putExtra(Intent.EXTRA_EMAIL, new String[] {"e.regular.games.llc@gmail.com"});
38 | intent.putExtra(Intent.EXTRA_SUBJECT, "Arduator: Setup Question?");
39 | startActivity(intent);
40 | }
41 | });
42 | }
43 |
44 | @Override
45 | public boolean onOptionsItemSelected(MenuItem item) {
46 | // handle arrow click here
47 | if (item.getItemId() == android.R.id.home) {
48 | finish(); // close this activity and return to preview activity (if there is any)
49 | }
50 |
51 | return super.onOptionsItemSelected(item);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator;
2 |
3 | import android.bluetooth.BluetoothAdapter;
4 | import android.bluetooth.BluetoothDevice;
5 | import android.content.Intent;
6 | import android.database.Cursor;
7 | import android.net.Uri;
8 | import android.os.Build;
9 | import android.provider.OpenableColumns;
10 | import android.support.v7.app.AppCompatActivity;
11 | import android.os.Bundle;
12 | import android.text.Editable;
13 | import android.text.TextWatcher;
14 | import android.view.KeyEvent;
15 | import android.view.View;
16 | import android.widget.AdapterView;
17 | import android.widget.ArrayAdapter;
18 | import android.widget.Button;
19 | import android.widget.EditText;
20 | import android.widget.ImageButton;
21 | import android.widget.ImageView;
22 | import android.widget.LinearLayout;
23 | import android.widget.ProgressBar;
24 | import android.widget.Spinner;
25 | import android.widget.TextView;
26 |
27 | import com.e_regular_games.arduator.arduino.ArduinoComm;
28 | import com.e_regular_games.arduator.arduino.ArduinoCommBle;
29 | import com.e_regular_games.arduator.arduino.ArduinoCommBt;
30 | import com.e_regular_games.arduator.arduino.ArduinoCommManager;
31 | import com.e_regular_games.arduator.arduino.ArduinoCommManagerAny;
32 | import com.e_regular_games.arduator.arduino.ArduinoCommUpdater;
33 |
34 | import java.io.IOException;
35 | import java.io.InputStream;
36 | import java.util.HashMap;
37 | import java.util.Map;
38 |
39 | public class MainActivity extends AppCompatActivity {
40 |
41 | private ActivityConfig config;
42 | private ArduinoCommManagerAny arduinoMgr;
43 | private Button btnSearchStart, btnSearchStop, btnUpload, btnSourceCode, btnHelp, btnAbout;
44 | private ImageButton btnFileLookup;
45 | private LinearLayout layoutStatus;
46 | private TextView textStatus, textService, textPin;
47 | private ImageView imgError;
48 | private ProgressBar prgBusy;
49 | private Spinner spnDevices;
50 | ArrayAdapter spnDevicesAdapter;
51 | private Spinner spnMode;
52 | EditText editService, editPin, editFile;
53 | private Uri uriFirmware;
54 | private BluetoothDevice device;
55 | private ArduinoCommUpdater updater;
56 | private boolean bSearching = false, bError = false;
57 |
58 | private void updateButtons() {
59 | if (device != null && uriFirmware != null && updater == null) {
60 | btnUpload.setEnabled(true);
61 | } else {
62 | btnUpload.setEnabled(false);
63 | }
64 |
65 | if (updater != null || bSearching) {
66 | btnFileLookup.setEnabled(false);
67 | btnSearchStart.setEnabled(false);
68 | btnSearchStop.setEnabled(bSearching);
69 | } else {
70 | btnSearchStop.setEnabled(false);
71 | btnFileLookup.setEnabled(true);
72 | btnSearchStart.setEnabled(true);
73 | }
74 | }
75 |
76 | private AdapterView.OnItemSelectedListener btDeviceChange = new AdapterView.OnItemSelectedListener() {
77 | @Override
78 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
79 | BtFoundDevice sel = (BtFoundDevice) spnDevices.getSelectedItem();
80 | config.setFavoriteBtDevice(sel.toString(), sel.getDevice().getAddress());
81 | device = sel.getDevice();
82 | updateButtons();
83 | }
84 |
85 | @Override
86 | public void onNothingSelected(AdapterView> parent) {
87 | config.setFavoriteBtDevice(null, null);
88 | device = null;
89 | updateButtons();
90 | }
91 | };
92 |
93 | private AdapterView.OnItemSelectedListener btModeChange = new AdapterView.OnItemSelectedListener() {
94 | @Override
95 | public void onItemSelected(AdapterView> parent, View view, int position, long id) {
96 | updateAfterBtModeChange(spnMode.getItemAtPosition(position).toString());
97 | }
98 |
99 | @Override
100 | public void onNothingSelected(AdapterView> parent) {
101 | spnMode.setSelection(0);
102 | updateAfterBtModeChange(spnMode.getItemAtPosition(0).toString());
103 | }
104 | };
105 |
106 | private Map foundDevices = new HashMap<>();
107 |
108 | private static class BtFoundDevice {
109 | BluetoothDevice device;
110 | String sName;
111 |
112 | BtFoundDevice(String sName, BluetoothDevice device) {
113 | this.device = device;
114 | this.sName = sName;
115 | }
116 |
117 | public String toString() {
118 | return sName;
119 | }
120 |
121 | public BluetoothDevice getDevice() {
122 | return device;
123 | }
124 | }
125 |
126 | private ArduinoCommManager.ManagerEvent mgrEvent = new ArduinoCommManager.ManagerEvent() {
127 | public void onFind(BluetoothDevice device, boolean saved) {
128 | String name = device.getName() == null || device.getName().equals("") ? device.getAddress() : device.getName();
129 |
130 | if (!foundDevices.containsKey(name)) {
131 | spnDevicesAdapter.add(new BtFoundDevice(name, device));
132 | foundDevices.put(name, true);
133 | }
134 | }
135 |
136 | public void onStatusChange(ArduinoCommManager.BluetoothStatus status) {
137 | switch (status) {
138 | case Enabled:
139 | case Disabled:
140 | layoutStatus.setVisibility(View.GONE);
141 | break;
142 | case Searching:
143 | showStatus("Searching...");
144 | break;
145 | case Error:
146 | bSearching = false;
147 | showError("Error!");
148 | BtFoundDevice sel = (BtFoundDevice) spnDevices.getSelectedItem();
149 | updateButtons();
150 | break;
151 | }
152 | }
153 |
154 | public void onCreate(ArduinoComm arduino) {
155 | try {
156 | InputStream in = MainActivity.this.getContentResolver().openInputStream(uriFirmware);
157 | if (spnMode.getSelectedItem().toString().equals("2.0")) {
158 | ((ArduinoCommBt)arduino).setPinCode(editPin.getText().toString());
159 | } else {
160 | ((ArduinoCommBle)arduino).setServiceId(editService.getText().toString());
161 | }
162 |
163 | updater = new ArduinoCommUpdater(MainActivity.this, arduino);
164 | updater.setOnStatus(new ArduinoCommUpdater.OnStatus() {
165 | @Override
166 | public void onError(ArduinoCommUpdater.ErrorCode code) {
167 | showError(code.toString());
168 | updater = null;
169 | bError = true;
170 | updateButtons();
171 | }
172 |
173 | @Override
174 | public void onStatus(ArduinoCommUpdater.StatusCode progress) {
175 | if (bError) {
176 | return;
177 | }
178 |
179 | if (progress == ArduinoCommUpdater.StatusCode.Disconnected) {
180 | showError("Disconnected");
181 | updater = null;
182 | updateButtons();
183 | } else if (progress == ArduinoCommUpdater.StatusCode.Complete) {
184 | showStatus("Upload Complete!");
185 | prgBusy.setVisibility(View.GONE);
186 | updater = null;
187 | updateButtons();
188 | } else {
189 | showStatus(progress.toString());
190 | }
191 | }
192 | });
193 |
194 | updater.upload(in);
195 | } catch (IOException e) {
196 | showError("Error reading firmware.");
197 | updater = null;
198 | }
199 | }
200 | };
201 |
202 | private void showError(String err) {
203 | layoutStatus.setVisibility(View.VISIBLE);
204 | prgBusy.setVisibility(View.GONE);
205 | imgError.setVisibility(View.VISIBLE);
206 | textStatus.setText(err);
207 | }
208 |
209 | private void showStatus(String msg) {
210 | layoutStatus.setVisibility(View.VISIBLE);
211 | prgBusy.setVisibility(View.VISIBLE);
212 | imgError.setVisibility(View.GONE);
213 | textStatus.setText(msg);
214 | }
215 |
216 |
217 | private void updateAfterBtModeChange(String mode) {
218 | if (mode.equals("2.0")) {
219 | if (Build.VERSION.SDK_INT < 23) {
220 | editPin.setVisibility(View.VISIBLE);
221 | textPin.setVisibility(View.VISIBLE);
222 | }
223 |
224 | editService.setVisibility(View.GONE);
225 | textService.setVisibility(View.GONE);
226 | } else {
227 | editPin.setVisibility(View.GONE);
228 | textPin.setVisibility(View.GONE);
229 |
230 | editService.setVisibility(View.VISIBLE);
231 | textService.setVisibility(View.VISIBLE);
232 | }
233 |
234 | if (!config.getBtMode().equals(mode)) {
235 | config.setBtMode(mode);
236 | spnDevicesAdapter.clear();
237 | config.setFavoriteBtDevice(null, null);
238 | device = null;
239 | }
240 | }
241 |
242 | private void loadValuesFromConfig() {
243 | if (config.getBtMode().equals("2.0")) {
244 | spnMode.setSelection(0);
245 | } else {
246 | spnMode.setSelection(1);
247 | }
248 |
249 | if (config.getFavoriteBtDeviceName() != null && config.getFavoriteBtDeviceAddr() != null) {
250 | String name = config.getFavoriteBtDeviceName();
251 | BluetoothDevice fav = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(config.getFavoriteBtDeviceAddr());
252 | device = fav;
253 | spnDevicesAdapter.add(new BtFoundDevice(name, fav));
254 | spnDevices.setSelection(0);
255 | }
256 |
257 | editService.setText(config.getBtServiceUuid());
258 | editPin.setText(config.getBtPinCode());
259 |
260 | updateButtons();
261 | }
262 |
263 | @Override
264 | protected void onCreate(Bundle savedInstanceState) {
265 | super.onCreate(savedInstanceState);
266 | setContentView(R.layout.activity_main);
267 |
268 | config = new ActivityConfig(this);
269 |
270 | layoutStatus = (LinearLayout) findViewById(R.id.layout_status);
271 | textStatus = (TextView) findViewById(R.id.bt_text_status);
272 | spnDevices = (Spinner) findViewById(R.id.bt_dropdown_device);
273 | imgError = (ImageView) findViewById(R.id.bt_error);
274 | prgBusy = (ProgressBar) findViewById(R.id.bt_progress);
275 | editService = (EditText) findViewById(R.id.bt_edit_service);
276 | btnSearchStart = (Button) findViewById(R.id.bt_button_find_start);
277 | btnSearchStop = (Button) findViewById(R.id.bt_button_find_stop);
278 | btnFileLookup = (ImageButton) findViewById(R.id.btn_file_lookup);
279 | btnUpload = (Button) findViewById(R.id.bt_button_upload);
280 | editPin = (EditText) findViewById(R.id.bt_edit_pin);
281 | textService = (TextView) findViewById(R.id.bt_text_service);
282 | textPin = (TextView) findViewById(R.id.bt_text_pin);
283 | spnMode = (Spinner) findViewById(R.id.bt_dropdown_mode);
284 | editFile = (EditText) findViewById(R.id.bt_edit_file);
285 |
286 | btnHelp = (Button) findViewById(R.id.bt_button_help);
287 | btnSourceCode = (Button) findViewById(R.id.bt_button_src);
288 | btnAbout = (Button) findViewById(R.id.bt_button_about);
289 |
290 | spnDevicesAdapter = new ArrayAdapter(MainActivity.this, android.R.layout.simple_spinner_item);
291 | spnDevicesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
292 |
293 | arduinoMgr = new ArduinoCommManagerAny(this, config.getBtMode());
294 | arduinoMgr.addOnManagerEvent(mgrEvent);
295 |
296 | spnMode.requestFocus();
297 | spnMode.setOnItemSelectedListener(btModeChange);
298 |
299 | spnDevices.setAdapter(spnDevicesAdapter);
300 | spnDevices.setOnItemSelectedListener(btDeviceChange);
301 |
302 | btnSearchStart.setOnClickListener(new View.OnClickListener() {
303 | @Override
304 | public void onClick(View v) {
305 | foundDevices.clear();
306 | spnDevicesAdapter.clear();
307 | config.setFavoriteBtDevice(null, null);
308 | device = null;
309 |
310 | arduinoMgr.setMode(spnMode.getSelectedItem().toString());
311 | arduinoMgr.find();
312 | bSearching = true;
313 | updateButtons();
314 | }
315 | });
316 |
317 | btnSearchStop.setOnClickListener(new View.OnClickListener() {
318 | @Override
319 | public void onClick(View v) {
320 | arduinoMgr.cancelFind();
321 | bSearching = false;
322 | updateButtons();
323 | }
324 | });
325 |
326 | btnUpload.setOnClickListener(new View.OnClickListener() {
327 | @Override
328 | public void onClick(View v) {
329 | arduinoMgr.cancelFind();
330 | bSearching = false;
331 | bError = false;
332 |
333 | BtFoundDevice sel = (BtFoundDevice) spnDevices.getSelectedItem();
334 | if (sel != null) {
335 | arduinoMgr.createArduinoComm(sel.getDevice());
336 | }
337 | updateButtons();
338 | }
339 | });
340 |
341 | btnFileLookup.setOnClickListener(new View.OnClickListener() {
342 | @Override
343 | public void onClick(View v) {
344 | performFileSearch();
345 | }
346 | });
347 |
348 | editPin.addTextChangedListener(new TextWatcher() {
349 | @Override
350 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
351 |
352 | }
353 |
354 | @Override
355 | public void onTextChanged(CharSequence s, int start, int before, int count) {
356 |
357 | }
358 |
359 | @Override
360 | public void afterTextChanged(Editable s) {
361 | config.setBtPinCode(s.toString());
362 | }
363 | });
364 |
365 | editService.addTextChangedListener(new TextWatcher() {
366 | @Override
367 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
368 |
369 | }
370 |
371 | @Override
372 | public void onTextChanged(CharSequence s, int start, int before, int count) {
373 |
374 | }
375 |
376 | @Override
377 | public void afterTextChanged(Editable s) {
378 | config.setBtServiceUuid(s.toString());
379 | }
380 | });
381 |
382 | btnSourceCode.setOnClickListener(new View.OnClickListener() {
383 | @Override
384 | public void onClick(View v) {
385 | String url = "https://github.com/e-regular-games/arduator";
386 | Intent i = new Intent(Intent.ACTION_VIEW);
387 | i.setData(Uri.parse(url));
388 | startActivity(i);
389 | }
390 | });
391 |
392 | btnHelp.setOnClickListener(new View.OnClickListener() {
393 | @Override
394 | public void onClick(View v) {
395 | Intent intent = new Intent(MainActivity.this, HelpActivity.class);
396 | startActivity(intent);
397 | }
398 | });
399 |
400 | btnAbout.setOnClickListener(new View.OnClickListener() {
401 | @Override
402 | public void onClick(View v) {
403 | Intent intent = new Intent(MainActivity.this, AboutActivity.class);
404 | startActivity(intent);
405 | }
406 | });
407 |
408 | loadValuesFromConfig();
409 | }
410 |
411 | private static final int READ_REQUEST_CODE = 42;
412 |
413 | private void performFileSearch() {
414 | Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
415 | intent.addCategory(Intent.CATEGORY_OPENABLE);
416 | intent.setType("*/*");
417 |
418 | startActivityForResult(intent, READ_REQUEST_CODE);
419 | }
420 |
421 | protected void onActivityResult(int requestCode, int responseCode, Intent data) {
422 | arduinoMgr.onActivityResult(requestCode, responseCode);
423 |
424 | if (requestCode == READ_REQUEST_CODE) {
425 | if (responseCode == RESULT_OK && data != null) {
426 | Uri uri = data.getData();
427 | fileDisplayName(uri);
428 | uriFirmware = uri;
429 | } else {
430 | uriFirmware = null;
431 | }
432 | updateButtons();
433 | }
434 | }
435 |
436 | @Override
437 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
438 | arduinoMgr.onRequestPermissionsResult(requestCode, permissions, grantResults);
439 | }
440 |
441 | private void fileDisplayName(Uri uri) {
442 | Cursor cursor = getContentResolver().query(uri, null, null, null, null, null);
443 |
444 | try {
445 | if (cursor != null && cursor.moveToFirst()) {
446 | String displayName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
447 | editFile.setText(displayName);
448 | }
449 | } finally {
450 | cursor.close();
451 | }
452 | }
453 | }
454 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoComm.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 | import android.bluetooth.BluetoothDevice;
5 |
6 | import java.lang.reflect.Method;
7 | import java.nio.charset.Charset;
8 | import java.util.ArrayList;
9 |
10 | /**
11 | * @author S. Ryan Edgar
12 | * A class to communicate with Arduino devices over bluetooth. Supports connect, send, recv,
13 | * disconnect. It should handle the pairing process as well without prompting the user. The API
14 | * for recving events is ArduinoComm.EventHandler. Multiple EventHandlers can be added to a single
15 | * ArduinoComm instance.
16 | *
17 | * Although any bluetooth devide implementing the serial port protocal will work, it need not only
18 | * be Arduino.
19 | */
20 | public abstract class ArduinoComm {
21 | public enum ErrorCode {Connect, IO, Send, Receive, Services, RemovePairing, PinRequired, ServiceIdRequired}
22 | public enum StatusCode {Connecting, Connected, Disconnected, Disconnecting}
23 |
24 | public ArduinoComm(Activity parent, BluetoothDevice device) {
25 | app = parent;
26 | this.device = device;
27 | }
28 |
29 | public static class EventHandler {
30 | public void onError(ArduinoComm self, ErrorCode code) {}
31 | public void onStatus(ArduinoComm self, StatusCode code) {}
32 | public void onContent(ArduinoComm self, int length, byte[] content) {}
33 | }
34 |
35 | public String getName() {
36 | return device.getName() != null ? device.getName() : device.getAddress();
37 | }
38 |
39 | public void addEventHandler(EventHandler handler) {
40 | if (handler != null) {
41 | onEvents.add(handler);
42 | }
43 | }
44 |
45 | public void removeEventHandler(EventHandler handler) {
46 | onEvents.remove(handler);
47 | }
48 |
49 | public abstract void connect();
50 | public abstract void disconnect();
51 |
52 | /**
53 | * @param packet data packet, only the lowest 8 bits of each integer will be sent. Integers are
54 | * used to avoid issues with negative numbers.
55 | */
56 | public abstract void send(int[] packet);
57 |
58 | protected Activity app;
59 | protected BluetoothDevice device;
60 | private ArrayList onEvents = new ArrayList<>();
61 |
62 | protected void onStatus(final ArduinoComm.StatusCode stat) {
63 | app.runOnUiThread(new Runnable() {
64 | @Override
65 | public void run() {
66 | for (EventHandler e : onEvents) {
67 | e.onStatus(ArduinoComm.this, stat);
68 | }
69 | }
70 | });
71 | }
72 |
73 | protected void onError(final ArduinoComm.ErrorCode err) {
74 | app.runOnUiThread(new Runnable() {
75 | @Override
76 | public void run() {
77 | for (EventHandler e : onEvents) {
78 | e.onError(ArduinoComm.this, err);
79 | }
80 | }
81 | });
82 | }
83 |
84 | protected void onContent(final int length, final byte[] content) {
85 | app.runOnUiThread(new Runnable() {
86 | @Override
87 | public void run() {
88 | for (EventHandler e : onEvents) {
89 | e.onContent(ArduinoComm.this, length, content);
90 | }
91 | }
92 | });
93 | }
94 |
95 | // https://stackoverflow.com/questions/38055699/programmatically-pairing-with-a-ble-device-on-android-4-4
96 | protected void deleteBondInformation() {
97 | try {
98 | // FFS Google, just unhide the method.
99 | Method m = device.getClass().getMethod("removeBond", (Class[]) null);
100 | m.invoke(device, (Object[]) null);
101 | } catch (Exception e) {
102 |
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoCommBle.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 | import android.bluetooth.BluetoothDevice;
5 | import android.bluetooth.BluetoothGatt;
6 | import android.bluetooth.BluetoothGattCallback;
7 | import android.bluetooth.BluetoothGattCharacteristic;
8 | import android.bluetooth.BluetoothGattDescriptor;
9 | import android.bluetooth.BluetoothGattService;
10 | import android.bluetooth.BluetoothProfile;
11 | import android.os.Handler;
12 | import android.os.Message;
13 |
14 | import java.util.ArrayList;
15 | import java.util.List;
16 | import java.util.Map;
17 | import java.util.Timer;
18 | import java.util.TimerTask;
19 | import java.util.concurrent.ConcurrentHashMap;
20 |
21 | /**
22 | * @author S. Ryan Edgar
23 | * Communicate with a Bluetooth 4.0 Low-Energy device. You must set the service id before
24 | * attempting to connect to the device.
25 | */
26 | public class ArduinoCommBle extends ArduinoComm {
27 |
28 | public ArduinoCommBle(Activity app, BluetoothDevice device) {
29 | super(app, device);
30 | }
31 |
32 | /**
33 | * @param uuid the last 4 hex characters of the UUID representing the Serial port service.
34 | */
35 | public void setServiceId(String uuid) {
36 | serviceId = uuid;
37 | }
38 |
39 | @Override
40 | public void connect() {
41 | if (!connected) {
42 | if (serviceId == null) {
43 | onError(ErrorCode.ServiceIdRequired);
44 | return;
45 | }
46 |
47 | if (device.getBondState() != BluetoothDevice.BOND_NONE) {
48 | deleteBondInformation();
49 | if (device.getBondState() != BluetoothDevice.BOND_NONE) {
50 | onError(ErrorCode.RemovePairing);
51 | }
52 | }
53 |
54 | btGatt = device.connectGatt(app, false, btgCallback);
55 | onStatus(StatusCode.Connecting);
56 |
57 | // will be canceled if connection is successful or explicitly fails.
58 | taskConnectTimeout = new TimerTask() {
59 | @Override
60 | public void run() {
61 | onError(ErrorCode.Connect);
62 | btGatt.disconnect();
63 | }
64 | };
65 | timer.schedule(taskConnectTimeout, 10000);
66 | }
67 | }
68 |
69 | @Override
70 | public void disconnect() {
71 | if (connected) {
72 | taskConnectTimeout.cancel();
73 | pendingWrite = false;
74 | toSend.clear();
75 | onStatus(StatusCode.Disconnecting);
76 | btGatt.disconnect();
77 | }
78 | }
79 |
80 | @Override
81 | public void send(int packet[]) {
82 | for (int i = 0; i < packet.length; i += 1) {
83 | toSend.add(packet[i]);
84 | }
85 |
86 | if (!pendingWrite) {
87 | pendingWrite = true;
88 | sendNextChunk();
89 | }
90 | }
91 |
92 | private BluetoothGatt btGatt;
93 | private ArrayList toSend = new ArrayList<>();
94 | private boolean pendingWrite = false;
95 |
96 | private Handler messenger = new Handler(new Handler.Callback() {
97 | @Override
98 | public boolean handleMessage(Message message) {
99 | switch (message.what) {
100 | case MessageConstants.ERROR:
101 | onError((ErrorCode) message.obj);
102 | break;
103 |
104 | case MessageConstants.STATUS:
105 | onStatus((StatusCode) message.obj);
106 | break;
107 |
108 | case MessageConstants.ACTION:
109 | doAction((ActionCode) message.obj);
110 | break;
111 |
112 | case MessageConstants.READ:
113 | byte[] content = (byte[]) message.obj;
114 | onContent(content.length, content);
115 | break;
116 | }
117 |
118 | return false;
119 | }
120 | });
121 | private BluetoothGattCallback btgCallback = new ArduinoGattCallback(messenger);
122 |
123 | private enum ActionCode {InitSerialService, WriteNext}
124 |
125 | private String serviceId;
126 | private BluetoothGattCharacteristic charSerial;
127 | private boolean connected = false;
128 |
129 | private Timer timer = new Timer();
130 | private TimerTask taskConnectTimeout;
131 |
132 | private interface MessageConstants {
133 | int READ = 1;
134 | int ERROR = 2;
135 | int STATUS = 4;
136 | int ACTION = 8;
137 | }
138 |
139 | private void doAction(ActionCode action) {
140 | switch (action) {
141 | case InitSerialService:
142 | initService();
143 | break;
144 | case WriteNext:
145 | sendNextChunk();
146 | break;
147 | }
148 | }
149 |
150 | /**
151 | * For BLE we can only send 20 bytes at a time.
152 | * Convert the integers from toSend and turn them into bytes.
153 | * Ensure btGatt is valid, because sendNextChunk can be called after a disconnect.
154 | */
155 | private void sendNextChunk() {
156 | if (toSend.size() > 0 && btGatt != null) {
157 | int chunkSize = toSend.size() > 20 ? 20 : toSend.size();
158 | byte chunk[] = new byte[chunkSize];
159 | for (int i = 0; i < chunkSize; i += 1) {
160 | chunk[i] = toSend.remove(0).byteValue();
161 | }
162 |
163 | charSerial.setValue(chunk);
164 | btGatt.writeCharacteristic(charSerial);
165 | } else {
166 | pendingWrite = false;
167 | }
168 | }
169 |
170 | private void initService() {
171 | List list = btGatt.getServices();
172 | BluetoothGattService service = null;
173 | for (BluetoothGattService s : list) {
174 | if (s.getUuid().toString().substring(4, 8).equalsIgnoreCase(serviceId)) {
175 | service = s;
176 | break;
177 | }
178 | }
179 |
180 | if (service == null) {
181 | onError(ErrorCode.IO);
182 | taskConnectTimeout.cancel();
183 | return;
184 | }
185 |
186 | BluetoothGattCharacteristic characteristic = null;
187 | List chars = service.getCharacteristics();
188 | for (BluetoothGattCharacteristic c : chars) {
189 | int props = c.getProperties();
190 | int desiredProps = BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE | BluetoothGattCharacteristic.PROPERTY_NOTIFY;
191 | if ((props & desiredProps) == desiredProps) {
192 | characteristic = c;
193 | break;
194 | }
195 | }
196 |
197 | if (characteristic == null) {
198 | onError(ErrorCode.IO);
199 | taskConnectTimeout.cancel();
200 | return;
201 | }
202 | charSerial = characteristic;
203 |
204 | BluetoothGattDescriptor clientConfig = null;
205 | List descs = charSerial.getDescriptors();
206 | for (BluetoothGattDescriptor d : descs) {
207 | if (d.getUuid().toString().substring(4, 8).equalsIgnoreCase("2902")) {
208 | clientConfig = d;
209 | break;
210 | }
211 | }
212 |
213 | if (clientConfig == null) {
214 | onError(ErrorCode.IO);
215 | taskConnectTimeout.cancel();
216 | return;
217 | }
218 |
219 | btGatt.setCharacteristicNotification(charSerial, true);
220 |
221 | clientConfig.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
222 | btGatt.writeDescriptor(clientConfig);
223 | }
224 |
225 | private class ArduinoGattCallback extends BluetoothGattCallback {
226 |
227 | private Handler messenger;
228 |
229 | public ArduinoGattCallback(Handler handler) {
230 | messenger = handler;
231 | }
232 |
233 | @Override
234 | public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
235 | super.onConnectionStateChange(gatt, status, newState);
236 |
237 | if (newState == BluetoothProfile.STATE_CONNECTED) {
238 | connected = true;
239 | gatt.discoverServices();
240 | } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
241 | connected = false;
242 | btGatt.close();
243 | btGatt = null;
244 | charSerial = null;
245 | messenger.obtainMessage(MessageConstants.STATUS, StatusCode.Disconnected).sendToTarget();
246 | } else {
247 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.Connect).sendToTarget();
248 | taskConnectTimeout.cancel();
249 | }
250 | }
251 |
252 | @Override
253 | public void onServicesDiscovered(BluetoothGatt gatt, int status) {
254 | super.onServicesDiscovered(gatt, status);
255 |
256 | if (status == BluetoothGatt.GATT_SUCCESS) {
257 | messenger.obtainMessage(MessageConstants.ACTION, ActionCode.InitSerialService).sendToTarget();
258 | } else {
259 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.Services).sendToTarget();
260 | taskConnectTimeout.cancel();
261 | }
262 | }
263 |
264 | @Override
265 | public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
266 | super.onCharacteristicRead(gatt, characteristic, status);
267 | }
268 |
269 | @Override
270 | public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
271 | super.onCharacteristicWrite(gatt, characteristic, status);
272 |
273 | if (characteristic.equals(charSerial) && status == BluetoothGatt.GATT_SUCCESS) {
274 | messenger.obtainMessage(MessageConstants.ACTION, ActionCode.WriteNext).sendToTarget();
275 | } else {
276 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.Send).sendToTarget();
277 | }
278 | }
279 |
280 | @Override
281 | public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
282 | super.onCharacteristicChanged(gatt, characteristic);
283 |
284 | if (characteristic.equals(charSerial)) {
285 | messenger.obtainMessage(MessageConstants.READ, characteristic.getValue()).sendToTarget();
286 | }
287 | }
288 |
289 | @Override
290 | public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
291 | super.onDescriptorWrite(gatt, descriptor, status);
292 |
293 | if (descriptor.getUuid().toString().substring(4, 8).equalsIgnoreCase("2902")) {
294 | if (status == BluetoothGatt.GATT_SUCCESS) {
295 | taskConnectTimeout.cancel();
296 | messenger.obtainMessage(MessageConstants.STATUS, StatusCode.Connected).sendToTarget();
297 | } else {
298 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.IO).sendToTarget();
299 | taskConnectTimeout.cancel();
300 | }
301 | }
302 | }
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoCommBt.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 | import android.bluetooth.BluetoothDevice;
5 | import android.bluetooth.BluetoothSocket;
6 | import android.content.BroadcastReceiver;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.content.IntentFilter;
10 | import android.os.Build;
11 | import android.os.Handler;
12 | import android.os.Message;
13 |
14 | import java.io.IOException;
15 | import java.io.InputStream;
16 | import java.io.OutputStream;
17 | import java.util.ArrayList;
18 | import java.util.HashMap;
19 | import java.util.Map;
20 | import java.util.Timer;
21 | import java.util.TimerTask;
22 | import java.util.UUID;
23 | import java.util.jar.Pack200;
24 |
25 | /**
26 | * @author S. Ryan Edgar
27 | */
28 | public class ArduinoCommBt extends ArduinoComm {
29 |
30 | public ArduinoCommBt(Activity app, BluetoothDevice device) {
31 | super(app, device);
32 | connThread = new ConnectionThread(device, messenger);
33 | }
34 |
35 | public void connect() {
36 | if (!connected) {
37 | if (pinCode == null) {
38 | onError(ErrorCode.PinRequired);
39 | return;
40 | }
41 |
42 | if (Build.VERSION.SDK_INT < 23) {
43 | if (device.getBondState() != BluetoothDevice.BOND_NONE) {
44 | deleteBondInformation();
45 | if (device.getBondState() != BluetoothDevice.BOND_NONE) {
46 | onError(ErrorCode.RemovePairing);
47 | return;
48 | }
49 | }
50 |
51 | IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
52 | app.registerReceiver(new BroadcastReceiver() {
53 | @Override
54 | public void onReceive(Context context, Intent intent) {
55 | String action = intent.getAction();
56 | if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(action)) {
57 | // Discovery has found a device. Get the BluetoothDevice
58 | // object and its info from the Intent.
59 | BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
60 | device.setPin(pinCode.getBytes());
61 | try {
62 | device.getClass().getMethod("cancelBondProcess", (Class[]) null).invoke(device, (Object[]) null);
63 | device.getClass().getMethod("cancelPairingUserInput", boolean.class).invoke(device);
64 | } catch (Exception e) {
65 | }
66 | app.unregisterReceiver(this);
67 | }
68 | }
69 | }, filter);
70 |
71 | device.setPin(pinCode.getBytes());
72 | }
73 |
74 | connThread.start();
75 | onStatus(StatusCode.Connecting);
76 | }
77 | }
78 |
79 | public void disconnect() {
80 | if (connected) {
81 | onStatus(StatusCode.Disconnecting);
82 | connThread.cancel();
83 | }
84 | }
85 |
86 | public void send(int[] packet) {
87 | connThread.write(packet);
88 | }
89 |
90 | public void setPinCode(String pin) {
91 | pinCode = pin;
92 | }
93 |
94 | private ConnectionThread connThread;
95 | private boolean connected = false;
96 | private String pinCode;
97 |
98 | private interface MessageConstants {
99 | int READ = 1;
100 | int ERROR = 2;
101 | int STATUS = 4;
102 | }
103 |
104 | private Handler messenger = new Handler(new Handler.Callback() {
105 | @Override
106 | public boolean handleMessage(Message message) {
107 | switch (message.what) {
108 | case MessageConstants.ERROR:
109 | onError((ErrorCode) message.obj);
110 | break;
111 |
112 | case MessageConstants.STATUS:
113 | onStatus((StatusCode) message.obj);
114 | break;
115 |
116 | case MessageConstants.READ:
117 | onContent(message.arg1, (byte[]) message.obj);
118 | break;
119 | }
120 |
121 | return false;
122 | }
123 | });
124 |
125 | private static UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");
126 |
127 | private class ConnectionThread extends Thread {
128 | private BluetoothDevice device;
129 | private BluetoothSocket socket;
130 | private boolean closed = false;
131 |
132 | private InputStream recvStream;
133 | private OutputStream sendStream;
134 | private byte[] buffer = new byte[1024];
135 | private Handler messenger;
136 |
137 | public ConnectionThread(BluetoothDevice device, Handler messenger) {
138 | this.device = device;
139 | this.messenger = messenger;
140 | }
141 |
142 | private boolean connect() {
143 | try {
144 | socket = device.createRfcommSocketToServiceRecord(SPP_UUID);
145 | socket.connect();
146 | closed = false;
147 |
148 | return true;
149 | } catch (IOException connE) {
150 | try {
151 | closed = true;
152 | socket.close();
153 | } catch (IOException closeE) {
154 | }
155 | return false;
156 | }
157 | }
158 |
159 | public void run() {
160 |
161 | messenger.obtainMessage(MessageConstants.STATUS, StatusCode.Connecting).sendToTarget();
162 | if (connect()) {
163 | messenger.obtainMessage(MessageConstants.STATUS, StatusCode.Connected).sendToTarget();
164 | connected = true;
165 | } else {
166 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.Connect).sendToTarget();
167 | return;
168 | }
169 |
170 |
171 | // Get the input and output streams; using temp objects because
172 | // member streams are final.
173 | try {
174 | recvStream = socket.getInputStream();
175 | sendStream = socket.getOutputStream();
176 | } catch (IOException e) {
177 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.IO).sendToTarget();
178 | }
179 |
180 | if (recvStream == null || sendStream == null) {
181 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.IO).sendToTarget();
182 | return;
183 | }
184 |
185 | int numBytes; // bytes returned from read()
186 |
187 | // Keep listening to the InputStream until an exception occurs.
188 | while (true) {
189 | try {
190 | // Read from the InputStream.
191 | numBytes = recvStream.read(buffer);
192 |
193 | if (numBytes > 0) {
194 | messenger.obtainMessage(MessageConstants.READ, numBytes, -1, buffer).sendToTarget();
195 | }
196 | } catch (IOException e) {
197 | if (!closed) {
198 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.Receive).sendToTarget();
199 | }
200 | break;
201 | }
202 | }
203 | }
204 |
205 | // Call this from the main activity to send data to the remote device.
206 | public void write(int[] bytes) {
207 | try {
208 | for (int i = 0; i < bytes.length; i += 1) {
209 | sendStream.write(bytes[i]);
210 | }
211 | sendStream.flush();
212 | } catch (IOException e) {
213 | if (!closed) {
214 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.Send).sendToTarget();
215 | }
216 | }
217 | }
218 |
219 | public void cancel() {
220 | try {
221 | closed = true;
222 | socket.close();
223 | connected = false;
224 | messenger.obtainMessage(MessageConstants.STATUS, StatusCode.Disconnected).sendToTarget();
225 | } catch (IOException closeE) {
226 |
227 | }
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoCommManager.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 | import android.bluetooth.BluetoothAdapter;
5 | import android.bluetooth.BluetoothDevice;
6 | import android.content.Intent;
7 |
8 | import java.util.ArrayList;
9 |
10 | import static android.app.Activity.RESULT_OK;
11 |
12 | /**
13 | * A class used to search for Bluetooth devices. Once found a ArduinoComm object can be
14 | * created from the Bluetooth device. This classes uses the ArduinoCommManager.ManagerEvent API
15 | * to indicate when events happen.
16 | *
17 | * This class is responsible for enabling Bluetooth and requesting appropriate permissions.
18 | *
19 | * ** IMPORTANT **
20 | * Your Activity must call onRequestPermissionResult and onActivityResult on ArduinoCommManager
21 | * when those functions are called on the Activity.
22 | */
23 | public abstract class ArduinoCommManager {
24 |
25 | public ArduinoCommManager(Activity parent) {
26 | app = parent;
27 | mBt = BluetoothAdapter.getDefaultAdapter();
28 | }
29 |
30 | public enum BluetoothStatus {Enabled, Disabled, Searching, Error}
31 |
32 | public static class ManagerEvent {
33 | public void onFind(BluetoothDevice device, boolean saved) {
34 | }
35 |
36 | public void onStatusChange(BluetoothStatus state) {
37 | }
38 |
39 | public void onCreate(ArduinoComm arduino) {
40 | }
41 | }
42 |
43 | public abstract void find();
44 |
45 | public abstract void cancelFind();
46 |
47 | public abstract void createArduinoComm(final BluetoothDevice device);
48 |
49 | public abstract void onRequestPermissionsResult(int requestCode, String permissions[], int grantResults[]);
50 |
51 | // must be called from the parent activity in the corresponding similarly name function.
52 | public void onActivityResult(int requestCode, int responseCode) {
53 | if (requestCode == REQUEST_ENABLE_BT) {
54 | if (responseCode == RESULT_OK) {
55 | btAvailable = true;
56 | onStatusChange(BluetoothStatus.Enabled);
57 |
58 | ArrayList copy = new ArrayList<>(afterEnable);
59 | afterEnable.clear();
60 | for (int i = 0; i < copy.size(); i += 1) {
61 | copy.get(i).after();
62 | }
63 | } else {
64 | onStatusChange(BluetoothStatus.Error);
65 | }
66 | }
67 | }
68 |
69 | public void addOnManagerEvent(ArduinoCommManagerBle.ManagerEvent onEvent) {
70 | if (onEvent != null) {
71 | onEvents.add(onEvent);
72 | }
73 | }
74 |
75 | public void removeOnManagerEvent(ManagerEvent onEvent) {
76 | onEvents.remove(onEvent);
77 | }
78 |
79 | protected Activity app;
80 | protected BluetoothAdapter mBt;
81 | protected boolean btAvailable;
82 | private ArrayList onEvents = new ArrayList<>();
83 | private static final int REQUEST_ENABLE_BT = 0x123;
84 |
85 | protected interface AfterEnable {
86 | void after();
87 | }
88 |
89 | private ArrayList afterEnable = new ArrayList<>();
90 |
91 | protected boolean enable(AfterEnable after) {
92 | if (afterEnable.size() > 0) {
93 | afterEnable.add(after);
94 | return false;
95 | }
96 |
97 | if (!mBt.isEnabled()) {
98 | afterEnable.add(after);
99 | Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
100 | app.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
101 | return false;
102 | }
103 |
104 | if (!btAvailable) {
105 | onStatusChange(BluetoothStatus.Enabled);
106 | }
107 |
108 | btAvailable = true;
109 | return true;
110 | }
111 |
112 | protected void onStatusChange(final BluetoothStatus status) {
113 | app.runOnUiThread(new Runnable() {
114 | @Override
115 | public void run() {
116 | for (ManagerEvent e : onEvents) {
117 | e.onStatusChange(status);
118 | }
119 | }
120 | });
121 | }
122 |
123 | protected void onFind(final BluetoothDevice device) {
124 | app.runOnUiThread(new Runnable() {
125 | @Override
126 | public void run() {
127 | for (ManagerEvent e : onEvents) {
128 | e.onFind(device, false);
129 | }
130 | }
131 | });
132 | }
133 |
134 | protected void onCreateStation(final ArduinoComm arduino) {
135 | app.runOnUiThread(new Runnable() {
136 | @Override
137 | public void run() {
138 | for (ManagerEvent e : onEvents) {
139 | e.onCreate(arduino);
140 | }
141 | }
142 | });
143 | }
144 | };
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoCommManagerAny.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 | import android.bluetooth.BluetoothDevice;
5 |
6 | import java.util.ArrayList;
7 |
8 | /**
9 | * A derived class of ArduinoCommManager which represents any Bluetooth mode (ie 2.0 or 4.0 LE).
10 | * This provides a way to switch managers while allow all other parts of the application to hold
11 | * on to the same object, ie an instance of ArduinoCommManagerAny.
12 | */
13 | public class ArduinoCommManagerAny extends ArduinoCommManager {
14 |
15 | /**
16 | * @param parent The parent Activity.
17 | * @param mode Either "2.0" or "4.0 LE".
18 | */
19 | public ArduinoCommManagerAny(Activity parent, String mode) {
20 | super(parent);
21 | this.parent = parent;
22 |
23 | setMode(mode);
24 | }
25 |
26 | /**
27 | * @param mode Either "2.0" or "4.0 LE".
28 | */
29 | public void setMode(String mode) {
30 | if (any != null) {
31 | any.cancelFind();
32 | }
33 |
34 | if (mode.equals("2.0")) {
35 | any = new ArduinoCommManagerBt(parent);
36 | } else if (mode.equals("4.0 LE")) {
37 | any = new ArduinoCommManagerBle(parent);
38 | } else {
39 | throw new Error("Invalid mode: " + mode);
40 | }
41 |
42 | for (ManagerEvent onEvent : onEvents) {
43 | any.addOnManagerEvent(onEvent);
44 | }
45 | }
46 |
47 | @Override
48 | public void find() {
49 | any.find();
50 | }
51 |
52 | @Override
53 | public void cancelFind() {
54 | any.cancelFind();
55 | }
56 |
57 | @Override
58 | public void createArduinoComm(BluetoothDevice device) {
59 | any.createArduinoComm(device);
60 | }
61 |
62 | @Override
63 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
64 | any.onRequestPermissionsResult(requestCode, permissions, grantResults);
65 | }
66 |
67 | @Override
68 | public void onActivityResult(int requestCode, int responseCode) {
69 | any.onActivityResult(requestCode, responseCode);
70 | }
71 |
72 | @Override
73 | public void addOnManagerEvent(ManagerEvent onEvent) {
74 | if (onEvent != null) {
75 | onEvents.add(onEvent);
76 | }
77 |
78 | any.addOnManagerEvent(onEvent);
79 | }
80 |
81 | @Override
82 | public void removeOnManagerEvent(ManagerEvent onEvent) {
83 | onEvents.remove(onEvent);
84 | any.removeOnManagerEvent(onEvent);
85 | }
86 |
87 | private ArduinoCommManager any;
88 | private Activity parent;
89 | private ArrayList onEvents = new ArrayList<>();
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoCommManagerBle.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.Manifest;
4 | import android.app.Activity;
5 | import android.app.AlertDialog;
6 | import android.bluetooth.BluetoothAdapter;
7 | import android.bluetooth.BluetoothDevice;
8 | import android.content.Context;
9 | import android.content.DialogInterface;
10 | import android.content.Intent;
11 | import android.content.pm.PackageManager;
12 | import android.location.LocationManager;
13 | import android.os.Build;
14 | import android.support.v4.app.ActivityCompat;
15 | import android.support.v4.content.ContextCompat;
16 |
17 | import java.util.ArrayList;
18 | import java.util.Timer;
19 | import java.util.TimerTask;
20 |
21 | import static android.app.Activity.RESULT_OK;
22 |
23 | /**
24 | * @author S. Ryan Edgar
25 | * A derived class of ArduinoCommManager specifically for finding and creatng ArduinoCommBle
26 | * devices, ie Bluetooth 4.0 LE devices.
27 | */
28 | public class ArduinoCommManagerBle extends ArduinoCommManager {
29 |
30 | public ArduinoCommManagerBle(Activity parent) {
31 | super(parent);
32 | }
33 |
34 | public void onRequestPermissionsResult(int requestCode, String permissions[], int grantResults[]) {
35 | if (requestCode == REQUEST_ENABLE_FIND) {
36 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
37 | if (enableFind()) {
38 | find();
39 | }
40 | } else {
41 | onStatusChange(BluetoothStatus.Error);
42 | }
43 | }
44 | }
45 |
46 | public void find() {
47 | AfterEnable afterEnableFind = new AfterEnable() {
48 | @Override
49 | public void after() {
50 | find();
51 | }
52 | };
53 |
54 | if (!enable(afterEnableFind)) {
55 | return;
56 | }
57 |
58 | if (finding || !enableFind()) {
59 | return;
60 | }
61 | onStatusChange(BluetoothStatus.Searching);
62 | finding = true;
63 |
64 | stopFindTask = new TimerTask() {
65 | @Override
66 | public void run() {
67 | mBt.stopLeScan(scanLeDevices);
68 | finding = false;
69 | stopFindTask = null;
70 | onStatusChange(BluetoothStatus.Enabled);
71 | }
72 | };
73 |
74 | // stop find after 2 minutes.
75 | stopFindTimer = new Timer();
76 | stopFindTimer.schedule(stopFindTask, 120 * 1000);
77 |
78 | mBt.startLeScan(scanLeDevices);
79 | }
80 |
81 | public void cancelFind() {
82 | if (finding && stopFindTask != null) {
83 | stopFindTimer.cancel();
84 | stopFindTimer.purge();
85 | stopFindTask.run();
86 | }
87 | }
88 |
89 | public void addOnManagerEvent(ManagerEvent onEvent) {
90 | if (onEvent != null) {
91 | super.addOnManagerEvent(onEvent);
92 |
93 | if (finding) {
94 | onEvent.onStatusChange(BluetoothStatus.Searching);
95 | } else if (btAvailable || mBt.isEnabled()) {
96 | onEvent.onStatusChange(BluetoothStatus.Enabled);
97 | } else {
98 | onEvent.onStatusChange(BluetoothStatus.Disabled);
99 | }
100 | }
101 | }
102 |
103 | @Override
104 | public void createArduinoComm(final BluetoothDevice device) {
105 | AfterEnable afterEnableCreate = new AfterEnable() {
106 | @Override
107 | public void after() {
108 | ArduinoComm station = new ArduinoCommBle(app, device);
109 | onCreateStation(station);
110 | }
111 | };
112 |
113 | if (!enable(afterEnableCreate)) {
114 | return;
115 | }
116 |
117 | if (finding) {
118 | onStatusChange(BluetoothStatus.Enabled);
119 | cancelFind();
120 | }
121 |
122 | ArduinoComm station = new ArduinoCommBle(app, device);
123 | onCreateStation(station);
124 | }
125 |
126 | private boolean finding = false;
127 | private Timer stopFindTimer;
128 | private TimerTask stopFindTask;
129 |
130 | private static final int REQUEST_ENABLE_FIND = 0x124;
131 | private static final int REQUEST_ENABLE_LOCATION = 0x125;
132 |
133 | private BluetoothAdapter.LeScanCallback scanLeDevices = new BluetoothAdapter.LeScanCallback() {
134 | @Override
135 | public void onLeScan(final BluetoothDevice device, int i, byte[] bytes) {
136 | if (device.getType() == BluetoothDevice.DEVICE_TYPE_LE) {
137 | onFind(device);
138 | }
139 | }
140 | };
141 |
142 | private boolean enableFind() {
143 | if (Build.VERSION.SDK_INT < 23) {
144 | return true;
145 | }
146 |
147 | final LocationManager manager = (LocationManager) app.getSystemService(Context.LOCATION_SERVICE);
148 | if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
149 | final AlertDialog.Builder builder = new AlertDialog.Builder(app);
150 | builder.setMessage("Your GPS seems to be disabled, do you want to enable it?")
151 | .setCancelable(false)
152 | .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
153 | public void onClick(final DialogInterface dialog, final int id) {
154 | Intent intent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
155 | app.startActivityForResult(intent, REQUEST_ENABLE_LOCATION);
156 | }
157 | })
158 | .setNegativeButton("No", new DialogInterface.OnClickListener() {
159 | public void onClick(final DialogInterface dialog, final int id) {
160 | onStatusChange(BluetoothStatus.Error);
161 | dialog.cancel();
162 | }
163 | });
164 | final AlertDialog alert = builder.create();
165 | alert.show();
166 | return false;
167 | } else if (ContextCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
168 | ActivityCompat.requestPermissions(app, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_ENABLE_FIND);
169 | return false;
170 | }
171 |
172 | return true;
173 | }
174 |
175 | // must be called from the parent activity in the corresponding similarly name function.
176 | public void onActivityResult(int requestCode, int responseCode) {
177 | super.onActivityResult(requestCode, responseCode);
178 |
179 | if (requestCode == REQUEST_ENABLE_LOCATION) {
180 | if (enableFind()) {
181 | find();
182 | }
183 | }
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoCommManagerBt.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 | import android.bluetooth.BluetoothDevice;
5 | import android.content.BroadcastReceiver;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.IntentFilter;
9 |
10 | import java.util.Timer;
11 | import java.util.TimerTask;
12 |
13 | /**
14 | * @author S. Ryan Edgar
15 | * Class used to discover and connect to Bluetooth 2.0 devices. If your intend to support
16 | * Bluetooth 2 and 4 it is suggested to use ArduinoCommManagerAny.
17 | */
18 | public class ArduinoCommManagerBt extends ArduinoCommManager {
19 |
20 | public ArduinoCommManagerBt(Activity app) {
21 | super(app);
22 | }
23 |
24 | @Override
25 | public void find() {
26 | AfterEnable afterEnableFind = new AfterEnable() {
27 | @Override
28 | public void after() {
29 | find();
30 | }
31 | };
32 |
33 | if (!enable(afterEnableFind)) {
34 | return;
35 | }
36 | if (findInProgress) {
37 | return;
38 | }
39 |
40 | onStatusChange(BluetoothStatus.Searching);
41 | stopFindTask = new TimerTask() {
42 | @Override
43 | public void run() {
44 | cancelFind();
45 | stopFindTask = null;
46 | }
47 | };
48 |
49 | // stop find after 2 minutes.
50 | stopFindTimer = new Timer();
51 | stopFindTimer.schedule(stopFindTask, 120 * 1000);
52 |
53 | // Register for broadcasts when a device is discovered.
54 | IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
55 | app.registerReceiver(mReceiver, filter);
56 |
57 | findInProgress = true;
58 | mBt.startDiscovery();
59 | }
60 |
61 | @Override
62 | public void cancelFind() {
63 | if (findInProgress) {
64 | app.unregisterReceiver(mReceiver);
65 | mBt.cancelDiscovery();
66 | findInProgress = false;
67 | onStatusChange(BluetoothStatus.Enabled);
68 | }
69 | }
70 |
71 | @Override
72 | public void createArduinoComm(final BluetoothDevice device) {
73 | AfterEnable afterEnableCreate = new AfterEnable() {
74 | @Override
75 | public void after() {
76 | ArduinoComm station = new ArduinoCommBt(app, device);
77 | onCreateStation(station);
78 | }
79 | };
80 |
81 | if (!enable(afterEnableCreate)) {
82 | return;
83 | }
84 |
85 | if (findInProgress) {
86 | onStatusChange(BluetoothStatus.Enabled);
87 | cancelFind();
88 | }
89 |
90 | ArduinoComm station = new ArduinoCommBt(app, device);
91 | onCreateStation(station);
92 | }
93 |
94 | @Override
95 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
96 |
97 | }
98 |
99 | private boolean findInProgress = false;
100 | private TimerTask stopFindTask;
101 | private Timer stopFindTimer;
102 |
103 | // Create a BroadcastReceiver for ACTION_FOUND.
104 | private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
105 | public void onReceive(Context context, Intent intent) {
106 | String action = intent.getAction();
107 | if (BluetoothDevice.ACTION_FOUND.equals(action)) {
108 | // Discovery has found a device. Get the BluetoothDevice
109 | // object and its info from the Intent.
110 | BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
111 | onFind(device);
112 | }
113 | }
114 | };
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/ArduinoCommUpdater.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 | import android.os.Handler;
5 | import android.os.Message;
6 |
7 | import java.io.InputStream;
8 | import java.util.ArrayList;
9 | import java.util.Timer;
10 | import java.util.TimerTask;
11 |
12 | /**
13 | * @author S. Ryan Edgar
14 | * Provided an ArduinoComm object, upload the specified Firmware to it. This relies on the
15 | * arduino power reseting when the ArduinoComm connects to it. The power reset triggers the
16 | * bootloader to watch for incoming commands from the serial port.
17 | *
18 | * Use ArduinoCommUpdater.OnStatus API to recieve status updates when each part of the upload
19 | * process completes, or if there is an error.
20 | */
21 | public class ArduinoCommUpdater {
22 | public enum ErrorCode { Connect, IO, Send, Receive, Services, RemovePairing, Sync, GetParams, SetProgParams, Program, VerifyProgram, Timeout, PinRequired, ServiceIdRequired, FW_FileName, FW_CheckSum, FW_StartCode, FW_ContiguousAddressing, Upload };
23 |
24 | public enum StatusCode {Connecting, FileCheck, Connected, Sync, GetParams, SetProgParams, Upload25, Upload50, Upload75, Upload100, Verifying, Complete, Disconnecting, Disconnected}
25 |
26 | public interface OnStatus {
27 | void onError(ErrorCode code);
28 | void onStatus(StatusCode progress);
29 | }
30 |
31 | public ArduinoCommUpdater(Activity app, ArduinoComm device) {
32 | this.app = app;
33 | this.device = device;
34 |
35 | device.addEventHandler(new ArduinoComm.EventHandler() {
36 | public void onError(ArduinoComm self, ArduinoComm.ErrorCode code) {
37 | ArduinoCommUpdater.this.onError(ErrorCode.valueOf(code.name()));
38 | }
39 |
40 | public void onStatus(ArduinoComm self, ArduinoComm.StatusCode code) {
41 | switch (code) {
42 | case Connected:
43 | if (inProgress == ActionCode.Wait) {
44 | Timer t = new Timer();
45 | t.schedule(new TimerTask() {
46 | @Override
47 | public void run() {
48 | doAction(ActionCode.Sync1);
49 | }
50 | }, 200);
51 | }
52 | break;
53 |
54 | case Disconnected:
55 | if (taskPendingResponse != null) {
56 | taskPendingResponse.cancel();
57 | }
58 | inProgress = ActionCode.Wait;
59 | break;
60 | }
61 |
62 | ArduinoCommUpdater.this.onStatus(StatusCode.valueOf(code.name()));
63 | }
64 |
65 | public void onContent(ArduinoComm self, int length, byte[] content) {
66 | for (int i = 0; i < length; i += 1) {
67 | toParse.add(content[i]);
68 | }
69 |
70 | parsePending(toParse);
71 | }
72 | });
73 | }
74 |
75 | public void setOnStatus(OnStatus status) {
76 | this.status = status;
77 | }
78 |
79 | public void upload(InputStream in) {
80 | completed = false;
81 | onStatus(StatusCode.FileCheck);
82 |
83 | firmware = new Firmware();
84 | if (!firmware.load(in)) {
85 | Firmware.ErrorCode ferr = firmware.getError();
86 | onError(ErrorCode.valueOf(ferr.name()));
87 | return;
88 | }
89 |
90 | fwBytes = firmware.getBytes();
91 | fwAddress = firmware.getStartAddress();
92 | fwBytesWritten = 0;
93 | fwBytesVerified = 0;
94 | timeouts = 0;
95 |
96 | device.connect();
97 | }
98 |
99 | public boolean success() {
100 | return completed;
101 | }
102 |
103 | private boolean completed = false;
104 | protected Activity app;
105 | protected ArduinoComm device;
106 | private OnStatus status;
107 | private Firmware firmware;
108 | private ArrayList fwBytes;
109 | private int fwBytesWritten = 0, fwAddress = 0, fwBytesVerified = 0;
110 | private ActionCode inProgress = ActionCode.Wait;
111 | private ArrayList toParse = new ArrayList<>();
112 | protected Timer timer = new Timer();
113 | private TimerTask taskPendingResponse;
114 | private int retries = 0;
115 | private static final int PAGE_LEN = 0x80;
116 |
117 | protected enum ActionCode {Wait, InitSerialService, Sync1, Sync2, Sync3, GetParam1, GetParam2, SetProgParams, SetExProgParams, EnterProgMode, ReadSignature, LoadAddressToWrite, LoadAddressToVerify, ExitProgMode, ReadPage, Program, WriteNext}
118 | protected interface MessageConstants {
119 | int ERROR = 2;
120 | }
121 |
122 | protected class OpTimeout extends TimerTask {
123 | private Handler messenger;
124 |
125 | public OpTimeout(Handler handler) {
126 | messenger = handler;
127 | }
128 |
129 | @Override
130 | public void run() {
131 | messenger.obtainMessage(MessageConstants.ERROR, ErrorCode.Timeout).sendToTarget();
132 | }
133 | }
134 |
135 | private int verifies = 0;
136 | private int timeouts = 0;
137 | protected Handler messenger = new Handler(new Handler.Callback() {
138 | @Override
139 | public boolean handleMessage(Message message) {
140 | if (message.what == MessageConstants.ERROR) {
141 | ErrorCode code = (ErrorCode) message.obj;
142 | if (code == ErrorCode.Timeout && timeouts < 5) {
143 | timeouts += 1;
144 | switch (inProgress) {
145 | case ReadPage:
146 | doAction(ActionCode.LoadAddressToVerify);
147 | break;
148 | case Program:
149 | doAction(ActionCode.LoadAddressToWrite);
150 | break;
151 | default:
152 | doAction(inProgress);
153 |
154 | }
155 | } else {
156 | onError(code);
157 | }
158 | }
159 |
160 | return false;
161 | }
162 | });
163 |
164 | private int getExpectedMessageLength() {
165 | switch (inProgress) {
166 | case Sync1:
167 | case Sync2:
168 | case Sync3:
169 | case SetProgParams:
170 | case SetExProgParams:
171 | case EnterProgMode:
172 | case LoadAddressToWrite:
173 | case LoadAddressToVerify:
174 | case Program:
175 | case ExitProgMode:
176 | return 2;
177 | case GetParam1:
178 | case GetParam2:
179 | return 3;
180 | case ReadSignature:
181 | return 5;
182 | case ReadPage:
183 | return 2 + PAGE_LEN;
184 | }
185 |
186 | return 0;
187 | }
188 |
189 | ;
190 |
191 | private void cleanPending(int len) {
192 | for (; len > 0; len--) {
193 | toParse.remove(0);
194 | }
195 | }
196 |
197 | private void parsePending(ArrayList toParse) {
198 | while (toParse.size() > 0 && toParse.get(0) != 0x14) {
199 | toParse.remove(0);
200 | }
201 |
202 | int msgLen = getExpectedMessageLength();
203 | if (msgLen == 0 || toParse.size() < msgLen) {
204 | return;
205 | }
206 |
207 | boolean validEnding = toParse.get(msgLen - 1) == 0x10;
208 | if (!validEnding) {
209 | cleanPending(msgLen);
210 | return;
211 | }
212 |
213 | if (taskPendingResponse != null) {
214 | taskPendingResponse.cancel();
215 | }
216 |
217 | timeouts = 0;
218 | switch (inProgress) {
219 | case Sync1:
220 | doAction(ActionCode.Sync2);
221 | break;
222 |
223 | case Sync2:
224 | doAction(ActionCode.Sync3);
225 |
226 | break;
227 |
228 | case Sync3:
229 | doAction(ActionCode.GetParam1);
230 | onStatus(StatusCode.Sync);
231 | break;
232 |
233 | case GetParam1:
234 | if (toParse.get(1) == 0x04 || toParse.get(1) == 0x01) {
235 | doAction(ActionCode.GetParam2);
236 | } else {
237 | onError(ErrorCode.GetParams);
238 | }
239 | break;
240 |
241 | case GetParam2:
242 | if (toParse.get(1) == 0x04 || toParse.get(1) == 0x10) {
243 | doAction(ActionCode.SetProgParams);
244 | onStatus(StatusCode.GetParams);
245 | } else {
246 | onError(ErrorCode.GetParams);
247 | }
248 | break;
249 |
250 | case SetProgParams:
251 | doAction(ActionCode.SetExProgParams);
252 | break;
253 |
254 | case SetExProgParams:
255 | doAction(ActionCode.EnterProgMode);
256 | onStatus(StatusCode.SetProgParams);
257 | break;
258 |
259 | case EnterProgMode:
260 | doAction(ActionCode.ReadSignature);
261 | break;
262 |
263 | case ReadSignature:
264 | if (toParse.get(1) == 0x1E && toParse.get(2) == (byte) 0x95 && toParse.get(3) == 0x0F) {
265 | fwAddress = firmware.getStartAddress();
266 | fwBytesWritten = 0;
267 | doAction(ActionCode.LoadAddressToWrite);
268 | } else {
269 | onError(ErrorCode.Program);
270 | }
271 | break;
272 |
273 | case Program:
274 | fwBytesWritten += PAGE_LEN;
275 | fwAddress += (PAGE_LEN / 2); // page_len is in bytes, address is in words.
276 |
277 | if (fwBytesWritten >= fwBytes.size()) {
278 | onStatus(StatusCode.Verifying);
279 | fwAddress = firmware.getStartAddress();
280 | fwBytesVerified = 0;
281 | doAction(ActionCode.LoadAddressToVerify);
282 | } else if (fwBytesWritten >= fwBytes.size() * 0.75) {
283 | onStatus(StatusCode.Upload75);
284 | doAction(ActionCode.LoadAddressToWrite);
285 | } else if (fwBytesWritten >= fwBytes.size() * 0.50) {
286 | onStatus(StatusCode.Upload50);
287 | doAction(ActionCode.LoadAddressToWrite);
288 | } else if (fwBytesWritten >= fwBytes.size() * 0.25) {
289 | onStatus(StatusCode.Upload25);
290 | doAction(ActionCode.LoadAddressToWrite);
291 | } else {
292 | doAction(ActionCode.LoadAddressToWrite);
293 | }
294 | break;
295 |
296 | case ReadPage:
297 | for (int i = 0; i < PAGE_LEN; i += 1) {
298 | int verify = fwBytesVerified + i < fwBytes.size() ? fwBytes.get(fwBytesVerified + i) : 0xFF;
299 | if (toParse.get(1 + i) != (byte) verify) {
300 | if (verifies < 10) {
301 | verifies += 1;
302 | cleanPending(msgLen);
303 | doAction(ActionCode.LoadAddressToVerify);
304 | } else {
305 | onError(ErrorCode.VerifyProgram);
306 | }
307 |
308 | return;
309 | }
310 | }
311 |
312 | verifies = 0;
313 | fwBytesVerified += PAGE_LEN;
314 |
315 | if (fwBytesVerified < fwBytes.size()) {
316 | fwAddress += (PAGE_LEN / 2); // page_len is in bytes, address is in words.
317 | doAction(ActionCode.LoadAddressToVerify);
318 | } else if (fwBytesVerified >= fwBytes.size()) {
319 | doAction(ActionCode.ExitProgMode);
320 | }
321 |
322 | break;
323 |
324 | case LoadAddressToWrite:
325 | doAction(ActionCode.Program);
326 | break;
327 |
328 | case LoadAddressToVerify:
329 | doAction(ActionCode.ReadPage);
330 | break;
331 |
332 | case ExitProgMode:
333 | onStatus(StatusCode.Complete); // must set status before completed!
334 | completed = true;
335 | device.disconnect();
336 | break;
337 | }
338 |
339 | cleanPending(msgLen);
340 | }
341 |
342 | private void handleError(ErrorCode err) {
343 | device.disconnect();
344 | }
345 |
346 | private StatusCode lastStatus;
347 |
348 | protected void onStatus(final StatusCode stat) {
349 | if (lastStatus == stat || completed) {
350 | return;
351 | }
352 |
353 | lastStatus = stat;
354 | app.runOnUiThread(new Runnable() {
355 | @Override
356 | public void run() {
357 | if (status != null) {
358 | status.onStatus(stat);
359 | }
360 | }
361 | });
362 | }
363 |
364 | protected void onError(final ErrorCode err) {
365 | app.runOnUiThread(new Runnable() {
366 | @Override
367 | public void run() {
368 | if (status != null) {
369 | status.onError(err);
370 | }
371 | }
372 | });
373 |
374 | handleError(err);
375 | }
376 |
377 | protected void doAction(ActionCode action) {
378 | inProgress = action;
379 |
380 | switch (action) {
381 | case Sync1:
382 | case Sync2:
383 | case Sync3:
384 | device.send(new int[]{0x30, 0x20});
385 | break;
386 | case GetParam1:
387 | device.send(new int[]{0x41, 0x81, 0x20});
388 | break;
389 | case GetParam2:
390 | device.send(new int[]{0x41, 0x82, 0x20});
391 | break;
392 | case SetProgParams:
393 | device.send(new int[]{0x42, 0x86, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x80, 0x04, 0x00, 0x00, 0x00, 0x80, 0x00, 0x20});
394 | break;
395 | case SetExProgParams:
396 | device.send(new int[]{0x45, 0x05, 0x04, 0xD7, 0xC2, 0x00, 0x20});
397 | break;
398 | case EnterProgMode:
399 | device.send(new int[]{0x50, 0x20});
400 | break;
401 | case ReadSignature:
402 | device.send(new int[]{0x75, 0x20});
403 | break;
404 | case LoadAddressToVerify:
405 | case LoadAddressToWrite:
406 | device.send(new int[]{0x55, 0xFF & fwAddress, 0xFF & (fwAddress >> 8), 0x20});
407 | break;
408 | case Program:
409 | int bytes[] = new int[5 + PAGE_LEN];
410 | bytes[0] = 0x64;
411 | bytes[1] = (PAGE_LEN >> 8) & 0xFF;
412 | bytes[2] = PAGE_LEN & 0xFF;
413 | bytes[3] = 0x46;
414 | for (int i = 0; i < PAGE_LEN; i += 1) {
415 | if (fwBytesWritten + i < fwBytes.size()) {
416 | bytes[4 + i] = fwBytes.get(fwBytesWritten + i);
417 | } else {
418 | bytes[4 + i] = 0xFF;
419 | }
420 | }
421 | bytes[4 + PAGE_LEN] = 0x20;
422 | device.send(bytes);
423 | break;
424 | case ReadPage:
425 | device.send(new int[]{0x74, (PAGE_LEN >> 8) & 0xFF, PAGE_LEN & 0xFF, 0x46, 0x20});
426 | break;
427 | case ExitProgMode:
428 | device.send(new int[]{0x51, 0x20});
429 | break;
430 | }
431 |
432 | taskPendingResponse = new OpTimeout(messenger);
433 | timer.schedule(taskPendingResponse, 1000);
434 | }
435 | }
436 |
--------------------------------------------------------------------------------
/app/src/main/java/com/e_regular_games/arduator/arduino/Firmware.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator.arduino;
2 |
3 | import android.app.Activity;
4 |
5 | import java.io.BufferedReader;
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.io.InputStreamReader;
9 | import java.util.ArrayList;
10 |
11 | /**
12 | * @author S. Ryan Edgar
13 | * Reads an input stream which provides intel based hex codes for Arduino Firmware. It verifies
14 | * the checksum of each line during load.
15 | *
16 | * https://en.wikipedia.org/wiki/Intel_HEX
17 | */
18 | public class Firmware {
19 | public Firmware() {}
20 |
21 | /**
22 | * Read the firmware from the provided stream and save it in this object.
23 | * @param in Stream containing the data of the firmware file.
24 | * @return true, if the file is valid, else false. If the file is invalid, getLastError will
25 | * indicate what went wrong.
26 | */
27 | public boolean load(InputStream in) {
28 | try {
29 | BufferedReader reader = new BufferedReader(new InputStreamReader(in));
30 |
31 | String line;
32 | boolean firstLine = true;
33 | int nextAddress = 0;
34 | while ((line = reader.readLine()) != null) {
35 | if (line.charAt(0) != ':') {
36 | lastError = ErrorCode.FW_StartCode;
37 | return false;
38 | }
39 |
40 | int dataLen = toInt(line.substring(1, 3));
41 | int lineLen = 2 * dataLen;
42 | int address = toInt(line.substring(3, 7));
43 | int checkSum = toInt(line.substring(1 + lineLen + 8, 1 + lineLen + 8 + 2));
44 | if (dataLen == 0 && address == 0 && toInt(line.substring(7, 8)) == 0) {
45 | //last line
46 | } else if (firstLine) {
47 | startAddress = address;
48 | } else if (address != nextAddress) {
49 | lastError = ErrorCode.FW_ContiguousAddressing;
50 | return false;
51 | }
52 |
53 | if (!verifyChecksum(line.substring(1, 1 + lineLen + 8), checkSum)) {
54 | lastError = ErrorCode.FW_CheckSum;
55 | return false;
56 | }
57 |
58 | for (int i = 0; i < dataLen; i += 1) {
59 | bytes.add(toInt(line.substring(9 + 2 * i, 9 + 2 * i + 2)));
60 | }
61 |
62 | firstLine = false;
63 | nextAddress = address + dataLen;
64 | }
65 |
66 | in.close();
67 | } catch (IOException e) {
68 | lastError = ErrorCode.FW_FileName;
69 | return false;
70 | }
71 |
72 | return true;
73 | }
74 |
75 | public enum ErrorCode {FW_FileName, FW_CheckSum, FW_StartCode, FW_ContiguousAddressing}
76 |
77 | public int getStartAddress() {
78 | return startAddress;
79 | }
80 |
81 | /**
82 | * @return an error code, if load returned false.
83 | */
84 | public ErrorCode getError() {
85 | return lastError;
86 | }
87 |
88 | /**
89 | * @return each byte of the program as an integer, to avoid negative number issues. Only the
90 | * lowest 8bits of each integer are valid, the rest should be ignored.
91 | */
92 | public ArrayList getBytes() {
93 | return bytes;
94 | }
95 |
96 | private int startAddress = 0;
97 | private ErrorCode lastError;
98 | private ArrayList bytes = new ArrayList<>();
99 |
100 | private boolean verifyChecksum(String lineWithoutSum, int sum) {
101 | if (lineWithoutSum.length() % 2 == 1) {
102 | return false;
103 | }
104 |
105 | int runningSum = 0;
106 | for (int i = 0; i < lineWithoutSum.length(); i += 2) {
107 | runningSum += toInt(lineWithoutSum.substring(i, i + 2));
108 | }
109 |
110 | return sum == (0xFF & (0x100 - runningSum));
111 | }
112 |
113 | private int toInt(String hex) {
114 | if ((hex.length() % 2) == 1) {
115 | return 0;
116 | }
117 |
118 | return Integer.parseInt(hex, 16);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_connections.xml:
--------------------------------------------------------------------------------
1 |
6 |
15 |
24 |
33 |
42 |
51 |
60 |
69 |
78 |
87 |
96 |
105 |
114 |
123 |
132 |
141 |
150 |
159 |
168 |
177 |
186 |
195 |
204 |
213 |
222 |
231 |
240 |
249 |
258 |
267 |
276 |
285 |
294 |
302 |
311 |
320 |
329 |
338 |
347 |
356 |
365 |
374 |
383 |
392 |
401 |
410 |
419 |
428 |
437 |
446 |
455 |
464 |
473 |
482 |
491 |
500 |
509 |
518 |
527 |
536 |
545 |
554 |
563 |
572 |
581 |
590 |
599 |
608 |
617 |
626 |
635 |
644 |
653 |
662 |
671 |
680 |
689 |
698 |
707 |
716 |
725 |
734 |
743 |
752 |
761 |
770 |
779 |
788 |
797 |
806 |
814 |
823 |
832 |
841 |
850 |
858 |
866 |
874 |
882 |
890 |
891 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_logo_negative.xml:
--------------------------------------------------------------------------------
1 |
6 |
15 |
24 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_about.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
23 |
24 |
25 |
31 |
32 |
40 |
41 |
48 |
49 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_help.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
23 |
24 |
25 |
26 |
30 |
31 |
36 |
37 |
43 |
44 |
51 |
52 |
58 |
59 |
66 |
67 |
75 |
76 |
82 |
83 |
90 |
91 |
98 |
99 |
106 |
107 |
114 |
115 |
122 |
123 |
129 |
130 |
137 |
138 |
145 |
146 |
153 |
154 |
162 |
163 |
170 |
171 |
178 |
179 |
186 |
187 |
193 |
194 |
199 |
200 |
208 |
209 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
17 |
18 |
22 |
23 |
27 |
28 |
32 |
33 |
39 |
40 |
46 |
47 |
53 |
54 |
55 |
65 |
66 |
84 |
85 |
97 |
98 |
104 |
105 |
112 |
113 |
114 |
124 |
125 |
134 |
135 |
145 |
146 |
158 |
159 |
170 |
171 |
184 |
185 |
195 |
196 |
205 |
206 |
221 |
222 |
227 |
228 |
229 |
230 |
241 |
242 |
243 |
244 |
245 |
246 |
253 |
254 |
260 |
261 |
273 |
274 |
285 |
286 |
297 |
298 |
299 |
300 |
301 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #00979c
4 | #025f63
5 | #0060ae
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Arduator
3 |
4 |
5 | - 2.0
6 | - 4.0 LE
7 |
8 | About
9 | Setup
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/test/java/com/e_regular_games/arduator/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.e_regular_games.arduator;
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() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/art/circuit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/art/circuit.png
--------------------------------------------------------------------------------
/art/company_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/art/company_logo.png
--------------------------------------------------------------------------------
/art/feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/art/feature.png
--------------------------------------------------------------------------------
/art/google-play-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/art/google-play-badge.png
--------------------------------------------------------------------------------
/art/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
90 |
--------------------------------------------------------------------------------
/art/logo_negative.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/art/logo_negative.png
--------------------------------------------------------------------------------
/art/logo_negative.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
79 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.3.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e-regular-games/arduator/896f06383e2a2748e2253b62e18a53140cb9aab1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jul 19 07:38:22 EDT 2017
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-3.3-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
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 Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------