├── .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 | [Google Play Store](https://play.google.com/store/apps/details?id=com.e_regular_games.arduator) 12 | 13 | ### The Programming Circuit 14 | 15 | Programming Circuit 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 | [E-Regular Games](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 |