From 163309c7eee44a8596fb3f0acf4fa0eadbb630d2 Mon Sep 17 00:00:00 2001 From: MGasztold Date: Fri, 29 Dec 2017 12:44:38 +0100 Subject: [PATCH] [RELEASE] v1.5.0 --- README.md | 162 +++++---- app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 3 - .../com/ubudu/iot/sample/MainActivity.java | 255 +++++--------- .../com/ubudu/iot/sample/MyBleDevice.java | 2 +- .../iot/sample/fragment/BaseFragment.java | 5 +- .../iot/sample/fragment/CommFragment.java | 132 +++++-- .../sample/fragment/PeripheralFragment.java | 6 + .../fragment/ScannedDevicesFragment.java | 14 +- .../ubudu/iot/sample/sound/ADPCMDecoder.java | 330 ------------------ app/src/main/res/layout/fragment_comm.xml | 98 ++++-- app/src/main/res/values/strings.xml | 19 +- 12 files changed, 384 insertions(+), 652 deletions(-) delete mode 100644 app/src/main/java/com/ubudu/iot/sample/sound/ADPCMDecoder.java diff --git a/README.md b/README.md index 0f5ede5..62d6a11 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Android-IoT-SDK -This SDK is dedicated for handling discovery, connection and two way communication with a BLE device. -SDK also provides API to make mobile device act like a connectable BLE device. +This SDK is dedicated for handling discovery, connection and two way communication with a Bluetooth LE device. +SDK also provides API to make mobile device act like a connectable Bluetooth LE device. ## Installing @@ -14,18 +14,16 @@ Add Ubudu nexus repository url to your `build.gradle` file: Then add the following dependency: - dependencies { - compile('com.ubudu.iot:iot-sdk:1.4.1@aar') - // ... - } + compile('com.ubudu.iot:iot-sdk:1.5.0@aar') ## How to use? -### BLE discovery +### Bluetooth LE discovery -Create an object implementing the `DongleFilter` interface. This interface is used to let the `DongleManager` class pick the desired device from all detectable devices nearby.: +Create an object implementing the `BleDeviceFilter` interface. This interface is used to let +the `DiscoveryManager` class pick the desired device from all devices being detected nearby.: - BleDeviceFilter dongleFilter = new BleDeviceFilter() { + BleDeviceFilter bleDeviceFilter = new BleDeviceFilter() { @Override public boolean isCorrect(BluetoothDevice device, int rssi, byte[] scanResponse) { // example implementation: @@ -33,16 +31,17 @@ Create an object implementing the `DongleFilter` interface. This interface is us } }; -**NOTE:** At this point on Android N `scanResponse` is always `null`. - -Then to discover a dongle: +Then to discover a Bluetooth LE device: - DongleManager.findDongle(mContext, 5000, dongleFilter, new DongleManager.DiscoveryListener() { + DiscoveryManager.discover(mContext, 5000, bleDeviceFilter, new DiscoveryManager.DiscoveryListener() { @Override - public boolean onDongleFound(Dongle dongle) { - // matching dongle found - mDongle = dongle; + public boolean onBleDeviceFound(BleDevice bleDevice) { + // ble device that matches the given bfound + mBleDevice = bleDevice; + // Returning true will stop the BLE device scanner immediately. + // Returning false will make scanning last according to + // given duration or until stop() is called. return true; } @@ -64,18 +63,18 @@ Then to discover a dongle: The flow of discovery is as follows: -- discovery lasts for the amount of milliseconds specified in the argument of `DongleManager.findDongle` method, +- discovery lasts for the amount of milliseconds specified in the argument of `DiscoveryManager.discover` method, -- during this time all detected BLE devices matching the given `BleDeviceFilter` implementation are returned in the `onDongleFound` method, +- during this time all detected Bluetooth LE devices matching the given `BleDeviceFilter` implementation are returned in the `onBleDeviceFound` callback, -- if at some point the `onDongleFound` returns `true`, the discovery will be stopped immediately. +- if at some point the `onBleDeviceFound` implementation returns `true`, the discovery will be stopped immediately. -### BLE connection +### Bluetooth LE connection Then to connect to the BLE device call the following: - mDongle.connect(mContext, new BleDevice.ConnectionListener() { + mBleDevice.connect(mContext, new BleDevice.ConnectionListener() { @Override public void onConnected() { // connected to dongle @@ -95,68 +94,81 @@ Then to connect to the BLE device call the following: To disconnect from the device please call: - mDongle.disconnect(); + mBleDevice.disconnect(); -### BLE communication +### Bluetooth LE communication -Before communicating with the BLE device its BLE services have to be discovered: +Before communicating with the device its Bluetooth GATT services have to be discovered: - mDongle.discoverServices(new BleDevice.ServicesDiscoveryListener() { - @Override - public void onServicesDiscovered(List services) { - // services found - } - - @Override - public void onError(Error error) { - // error - } - }); + mBleDevice.discoverServices(new BleDevice.ServicesDiscoveryListener() { + @Override + public void onServicesDiscovered(List services) { + // services found + } + + @Override + public void onError(Error error) { + // error + } + }); -With the BLE services of the device discovered it is possible to establish a 2 way communication channel. -In order to do this one GATT characteristic with WRITE permission and one with NOTIFICATIONS -permission have to be chosen. It is possible to set single GATT characteristic for both purposes: +With the Bluetooth LE services of the device discovered it is possible to establish a 2 way +communication channel: - mDongle.setGattCharacteristicForWriting(characteristic); - - dongle.registerForNotifications(characteristic, new BleDevice.RegisterForNotificationsListener() { - @Override - public void onRegistered(BluetoothGattCharacteristic characteristic) { - // success - } - - @Override - public void onError(Error error) { - // error - } - }); +1) In order to be able to receive data from Bluetooth LE device one of the GATT characteristics +available within one of the services should have `BluetoothGattCharacteristic.PROPERTY_NOTIFY` property. +2) In order to be able to send data to Bluetooth LE device one of the GATT characteristics +available within one of the services should have `BluetoothGattCharacteristic.PROPERTY_WRITE` or +`BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE` property. + +It is possible to use single GATT characteristic for both purposes if device provides such characteristic. + mBleDevice.registerForNotifications(characteristic, new BleDevice.RegisterForNotificationsListener() { + @Override + public void onRegistered(BluetoothGattCharacteristic gattCharacteristic) { + // success + } + + @Override + public void onError(Error error) { + // error + } + }); + +Data being received from the Bleutooth LE device is received by the listener interface that has to be set: - mDongle.setDataReceivedEventListener(new BleDevice.ReceiveDataEventListener() { - @Override - public void onDataReceived(byte[] data) { - // data received from dongle - } - }); + mBleDevice.setDataReceivedEventListener(new DataListener() { + @Override + public void onDataReceived(byte[] data) { + // data received from the device + } + }); -Now when 2 way communication is set up the app can send data to the BLE device: +The `DataListener` can be set any time before registering for notifications, e.g. right after successful connection. + + +To send data to the Bleutooth LE device: String message = "My message."; - mDongle.send(message.getBytes(), new BleDevice.SendDataEventListener() { - @Override - public void onDataSent(byte[] data) { - // data sent - } - - @Override - public void onCommunicationError(Error error) { - // communication error - } + mBleDevice.send(message.getBytes(), gattCharacteristic); + +To be notified about the result of sending the data a `DataSentListener` implementation must be set on the `BleDevice` instance: + + mBleDevice.setDataSentListener(new DataSentListener() { + @Override + public void onDataSent(byte[] data) { + // data sent successfuly + } + + @Override + public void onError(Error error) { + // error + } }); -### Make mobile device act as a connectable BLE device +### Make mobile device act as a connectable Bluetooth LE device -To make that happen the following code should be called to setup the `BluetoothGattServer`: +To make that happen the Bluetooth GATT server has to be opened. The following code configures it: peripheralManager = new PeripheralManager(mContext, new DeviceProfile() { @@ -207,6 +219,8 @@ To make that happen the following code should be called to setup the `BluetoothG } }); +To open the Bluetooth GATT server: + peripheralManager.openGattServer(); To stop the device from being a connectable peripheral: @@ -243,4 +257,10 @@ Start advertising as IBeacon: To stop advertising at any time call the following: - advertiser.stopAdvertising(); \ No newline at end of file + advertiser.stopAdvertising(); + +Foreign Bluetooth LE device can register for notifications to the read characteristic specified in `getReadCharacteristicUuid` method being a `DeviceProfile` implementation. If mobile device acting as peripheral wants to write some data to the foreign device then this read characteristic must be written. It is done when the following method is used: + + peripheralManager.writeData(data); + +The data received from the foreign device will trigger a `onCharacteristicWritten` callback of the given `PeripheralManager.PeripheralListener` instance. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index dbbc515..f2b4064 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,6 @@ buildscript { } } apply plugin: 'com.android.application' -apply plugin: 'io.fabric' apply plugin: 'android-apt' repositories { @@ -23,8 +22,8 @@ android { applicationId "com.ubudu.iot.sample" minSdkVersion 18 targetSdkVersion 26 - versionCode 24 - versionName "1.6" + versionCode 25 + versionName "1.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -49,15 +48,12 @@ dependencies { compile 'com.jakewharton:butterknife:8.5.1' apt 'com.jakewharton:butterknife-compiler:8.5.1' - compile('com.ubudu.iot:iot-sdk:1.4.1@aar') + compile('com.ubudu.iot:iot-sdk:1.5.0@aar') compile 'com.afollestad.material-dialogs:core:0.9.4.7' //slider compile 'org.adw.library:discrete-seekbar:1.0.1' - compile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { - transitive = true; - } compile 'com.wang.avi:library:2.1.3' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9eafcb4..cda35cb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,9 +23,6 @@ - \ No newline at end of file diff --git a/app/src/main/java/com/ubudu/iot/sample/MainActivity.java b/app/src/main/java/com/ubudu/iot/sample/MainActivity.java index af9eb21..08a9271 100644 --- a/app/src/main/java/com/ubudu/iot/sample/MainActivity.java +++ b/app/src/main/java/com/ubudu/iot/sample/MainActivity.java @@ -9,9 +9,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Typeface; -import android.media.AudioFormat; -import android.media.AudioManager; -import android.media.AudioTrack; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -41,11 +38,10 @@ import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; -import com.crashlytics.android.Crashlytics; import com.ubudu.iot.ConnectionListener; import com.ubudu.iot.DataListener; -import com.ubudu.iot.Iot; import com.ubudu.iot.DataSentListener; +import com.ubudu.iot.Iot; import com.ubudu.iot.ble.Advertiser; import com.ubudu.iot.ble.BleDevice; import com.ubudu.iot.ble.BleDeviceFilter; @@ -60,17 +56,13 @@ import com.ubudu.iot.sample.fragment.ScannedDevicesFragment; import com.ubudu.iot.sample.model.LogItem; import com.ubudu.iot.sample.model.Option; -import com.ubudu.iot.sample.sound.ADPCMDecoder; import com.ubudu.iot.sample.util.CustomTypefaceSpan; import com.ubudu.iot.sample.util.FragmentUtils; import com.ubudu.iot.sample.util.ToastUtil; import com.ubudu.iot.util.LongDataProtocol; import com.ubudu.iot.util.LongDataProtocolV2; -import io.fabric.sdk.android.Fabric; - import java.io.UnsupportedEncodingException; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -79,6 +71,7 @@ import butterknife.BindView; import butterknife.ButterKnife; +@RequiresApi(api = Build.VERSION_CODES.M) public class MainActivity extends AppCompatActivity implements BaseFragment.ViewController , DiscoveryManager.DiscoveryListener { @@ -88,6 +81,8 @@ public class MainActivity extends AppCompatActivity implements BaseFragment.View private static final int ASK_GEOLOCATION_PERMISSION_REQUEST_ON_SCAN = 1; private static final int ASK_GEOLOCATION_PERMISSION_REQUEST_ON_SEND_DATA = 2; + private Handler mEncodeAudioHandler; + private BleDevice mBleDevice; private BluetoothGattService selectedService; private Advertiser advertiser; @@ -96,78 +91,60 @@ public class MainActivity extends AppCompatActivity implements BaseFragment.View private PeripheralFragment peripheralFragment; private ScannedDevicesFragment scannedDevicesFragment; + private PeripheralManager peripheralManager; + + private BluetoothGattCharacteristic gattCharForWriting; + + private MenuItem scanMenuItem; + private DrawerLayout mRootView; @BindView(R.id.navigation_view) - android.support.design.widget.NavigationView navigationView; + NavigationView navigationView; @BindView(R.id.toolbar) Toolbar toolbar; private View headerLayout; private MaterialDialog progressDialog; - private int pcmSamplingFrequencyHz; - private AudioTrack mAudioTrack; - private ADPCMDecoder mAdpcmDecoder; + private SharedPreferences mSharedPref; private int rssiThreshold; - private Handler mAudioHandler; - private HandlerThread mAudioHandlerThread; - private LongDataProtocol longDataProtocol; - private DataListener mDataReceivedEventListener = new DataListener() { + private DataSentListener mDataSentListener = new DataSentListener() { @Override - public void onDataReceived(final byte[] data) { - Log.i(TAG, "Received data from ble device: " + String.format("%020x", new BigInteger(1, data))); - Log.i(TAG, "Received data len: " + data.length); - try { - if(data[0] != (byte)0xFA && commFragment!=null) { - Log.e(TAG,"normal data..."); - String msg = new String(data, "UTF-8"); - commFragment.onDataReceived(msg); - } else if(data[0] == (byte)0xFA && mAdpcmDecoder!=null){ - final byte[] audioBytes = Arrays.copyOfRange(data, 1, data.length); - Log.e(TAG,"audio bytes len: "+audioBytes.length); - playAudioBytes(audioBytes); + public void onDataSent(byte[] data) { + dismissProgressDialog(); + Log.i(TAG, "Data successfully sent: " + Arrays.toString(data) ); + } - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } + @Override + public void onError(final Error error) { + Log.e(TAG, error.getLocalizedMessage()); } }; - private void playAudioBytes(final byte[] audioBytes) { - mAudioHandler.post(new Runnable() { - @Override - public void run() { - if(mAdpcmDecoder==null) - return; - - if(audioBytes.length>ADPCMDecoder.FRAME_SIZE) { - int iterations = audioBytes.length/ADPCMDecoder.FRAME_SIZE; - byte[] block = new byte[ADPCMDecoder.FRAME_SIZE]; - for(int i=0;i131) { //Pre lollipop devices may not have the max mtu size hence the check - final byte[] newData = new byte[ADPCMDecoder.FRAME_SIZE]; - System.arraycopy(audioBytes, 0, newData, 0, ADPCMDecoder.FRAME_SIZE); - mAdpcmDecoder.add(newData); - } else { - mAdpcmDecoder.add(audioBytes); - } + Log.i(TAG, "Received data len: " + data.length); + if (commFragment != null) { + runOnUiThread(new Runnable() { + @Override + public void run() { + try { + commFragment.onDataReceived(new String(data, "UTF-8")); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + commFragment.onDataReceived(new String(data)); + } + } + }); } - }); - } + } + }; private BleDeviceFilter bleDeviceFilter = new BleDeviceFilter() { @Override @@ -197,7 +174,6 @@ public void onClick(View view) { scanMenuItem.setActionView(iv); } - private MenuItem scanMenuItem; @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); @@ -228,7 +204,6 @@ public void hideKeyboardRequested() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Fabric.with(this, new Crashlytics()); mRootView = (DrawerLayout) LayoutInflater.from(this).inflate(R.layout.activity_main, null); setContentView(mRootView); @@ -236,10 +211,9 @@ protected void onCreate(Bundle savedInstanceState) { longDataProtocol = new LongDataProtocolV2(); - Log.i(TAG,"Using Ubudu IOT-SDK v"+Iot.getVersion()+"("+Iot.getVersionCode()+")"); + Log.i(TAG,"Using Ubudu IOT-SDK v"+ Iot.getVersion()+"("+ Iot.getVersionCode()+")"); mSharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - pcmSamplingFrequencyHz = mSharedPref.getInt("pcm_audio_sampling_frequency_hz", 8000); rssiThreshold = mSharedPref.getInt("rssi_filter_value", -90); initNavigationDrawer(); @@ -254,24 +228,31 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onAdvertisingStarted() { Log.i(TAG, "Advertising started."); - peripheralFragment.onAdvertisingStarted(); + if(peripheralFragment!=null) + peripheralFragment.onAdvertisingStarted(); + + peripheralManager.openGattServer(); } @Override public void onAdvertisingStopped() { Log.i(TAG, "Advertising stopped."); - peripheralFragment.onAdvertisingStopped(); + if(peripheralFragment!=null) + peripheralFragment.onAdvertisingStopped(); + if(peripheralManager!=null) + peripheralManager.closeGattServer(); } @Override public void onAdvertisingError(Error error) { Log.e(TAG, error.getLocalizedMessage()); ToastUtil.showToast(getApplicationContext(), error.getLocalizedMessage()); - peripheralFragment.onAdvertisingError(error.getMessage()); + if(peripheralFragment!=null) + peripheralFragment.onAdvertisingError(error.getMessage()); } }); - PeripheralManager peripheralManager = new PeripheralManager(getApplicationContext(), new DeviceProfile() { + peripheralManager = new PeripheralManager(getApplicationContext(), new DeviceProfile() { @Override public String getServiceUuid() { return MyBleDevice.SERVICE_UUID; @@ -296,6 +277,15 @@ public void onPeripheralReady() { Log.i(TAG, msg); } + @Override + public void onPeripheralClosed() { + String msg = "Peripheral closed"; + if (peripheralFragment != null) + peripheralFragment.log(msg, LogItem.TYPE_MESSAGE); + Log.i(TAG, msg); + } + + private final Handler mHandler = new Handler(); @Override public void onConnectionStateChange(BluetoothDevice device, String stateDescription) { String msg = "Foreign device " + device.getName() + " connection to the peripheral manager state changed: " + stateDescription; @@ -305,16 +295,23 @@ public void onConnectionStateChange(BluetoothDevice device, String stateDescript } @Override - public void onCharacteristicWritten(UUID characteristicUUID, String value) { - String msg = "onDataWritten: " + value; - if (peripheralFragment != null) - peripheralFragment.log(msg, LogItem.TYPE_MESSAGE); + public void onCharacteristicWritten(UUID characteristicUUID, final byte[] value) { + final String msg = "onDataWritten: " + Arrays.toString(value); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (peripheralFragment != null) + peripheralFragment.log(msg, LogItem.TYPE_MESSAGE); + } + }); Log.d(TAG, msg); + + } @Override - public void onCharacteristicRead(UUID characteristicUUID, String value) { - String msg = "onDataRead: " + value; + public void onCharacteristicRead(UUID characteristicUUID, byte[] value) { + String msg = "onDataRead: " + new String(value); if (peripheralFragment != null) peripheralFragment.log(msg, LogItem.TYPE_MESSAGE); Log.d(TAG, msg); @@ -322,49 +319,18 @@ public void onCharacteristicRead(UUID characteristicUUID, String value) { @Override public void onPeripheralError(Error error) { - String msg = error.getLocalizedMessage(); - if (peripheralFragment != null) - peripheralFragment.log(msg, LogItem.TYPE_MESSAGE); + final String msg = error.getLocalizedMessage(); + runOnUiThread(new Runnable() { + @Override + public void run() { + if (peripheralFragment != null) + peripheralFragment.log(msg, LogItem.TYPE_MESSAGE); + } + }); Log.e(TAG, msg); ToastUtil.showToast(getApplicationContext(), msg); } }); - peripheralManager.openGattServer(); - } - } - - @Override - protected void onDestroy() { - stopPcmPlayback(); - super.onDestroy(); - } - - private void initPcmPlayback() { - - // init handler that plays the data - mAudioHandlerThread = new HandlerThread("AudioHandlerThread"); - mAudioHandlerThread.start(); - mAudioHandler = new Handler(mAudioHandlerThread.getLooper()); - - int bufferSize = AudioTrack.getMinBufferSize(pcmSamplingFrequencyHz, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT); - mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, pcmSamplingFrequencyHz - , AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM); - mAudioTrack.play(); - mAdpcmDecoder = new ADPCMDecoder(getApplicationContext(), false); - mAdpcmDecoder.setListener(new ADPCMDecoder.DecoderListener() { - @Override - public void onFrameDecoded(byte[] pcm, int frameNumber) { - if(mAudioTrack != null) - mAudioTrack.write(pcm, 0, pcm.length/*, AudioTrack.WRITE_NON_BLOCKING*/); - } - }); - } - - private void stopPcmPlayback() { - if(mAudioTrack!=null) { - mAudioTrack.stop(); - mAudioTrack = null; - mAdpcmDecoder = null; } } @@ -434,12 +400,6 @@ private void applyFontToMenuItem(MenuItem mi) { mNewTitle.setSpan(new CustomTypefaceSpan("", font), 0, mNewTitle.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); mi.setTitle(mNewTitle); } - - @Override - protected void onResume() { - super.onResume(); - } - @Override public void onBackPressed() { if(!isScannedDevicesFragmentVisible) @@ -570,7 +530,12 @@ public void onPeripheralFragmentResumed() { navigationView.setCheckedItem(R.id.peripheral_item); } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onPeripheralFragmentPaused() { + + } + + @RequiresApi(api = Build.VERSION_CODES.M) @Override public boolean isAdvertising() { return advertiser.isAdvertising(); @@ -584,17 +549,16 @@ public void onCommFragmentRequested() { @Override public void onCommFragmentResumed() { - initPcmPlayback(); + } @Override public void onCommFragmentPaused() { - stopPcmPlayback(); + } @Override public void onNameMacFilterChanged(String filter) { - Log.e(TAG,"saving filter: "+filter); mSharedPref.edit().putString("name_mac_filter_value",filter).apply(); } @@ -604,6 +568,7 @@ public void onConnectionRequested(BleDevice device) { showProgressDialog(getString(R.string.connecting)); this.mBleDevice = device; + this.mBleDevice.setDataSentListener(mDataSentListener); this.mBleDevice.setDataReceivedEventListener(mDataReceivedEventListener); // set long msg protocol for handling long data @@ -726,7 +691,7 @@ public void onOptionChosen(String option) { } } - mBleDevice.setGattCharacteristicForWriting(selected); + gattCharForWriting = selected; listener.onCompleted(); } @@ -802,13 +767,6 @@ public void onDisconnectRequested() { mBleDevice.disconnect(); } - @Override - public void onAudioSampleRateChanged(int sampleRateHz) { - pcmSamplingFrequencyHz = sampleRateHz; - stopPcmPlayback(); - initPcmPlayback(); - } - @Override public void onRssiFilterThresholdChanged(int rssiThreshold) { mSharedPref.edit().putInt("rssi_filter_value",rssiThreshold).apply(); @@ -826,7 +784,8 @@ public void onScannedDevicesFragmentRequested() { @Override public void onScannedDevicesFragmentPaused() { - scanMenuItem.setVisible(false); + if(scanMenuItem!=null) + scanMenuItem.setVisible(false); isScannedDevicesFragmentVisible = false; } @@ -897,34 +856,10 @@ public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) } @Override - public void onSendMessageRequested(String message) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M - && ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, ASK_GEOLOCATION_PERMISSION_REQUEST_ON_SEND_DATA); - return; + public void onSendMessageRequested(byte[] data) { + if(gattCharForWriting!=null && mBleDevice!=null && mBleDevice.isConnected()) { + mBleDevice.send(data, gattCharForWriting, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); } - - byte[] data = new byte[0]; - try { - data = (message).getBytes("UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - mBleDevice.send(data, new DataSentListener() { - @Override - public void onDataSent(byte[] data) { - dismissProgressDialog(); - Log.i(TAG, "Data successfully sent: " + String.format("%020x", new BigInteger(1, data))); - commFragment.onMessageSendingFinished(); - } - - @Override - public void onError(Error error) { - Log.e(TAG, error.getLocalizedMessage()); - ToastUtil.showToast(getApplicationContext(), error.getLocalizedMessage()); - commFragment.onMessageSendingFinished(); - } - }); } public interface ChooseListener { diff --git a/app/src/main/java/com/ubudu/iot/sample/MyBleDevice.java b/app/src/main/java/com/ubudu/iot/sample/MyBleDevice.java index 2781cf1..bc1e4f3 100644 --- a/app/src/main/java/com/ubudu/iot/sample/MyBleDevice.java +++ b/app/src/main/java/com/ubudu/iot/sample/MyBleDevice.java @@ -10,6 +10,6 @@ public class MyBleDevice { public static final String WRITE_CHARACTERISTIC_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb"; - public static final String READ_CHARACTERISTIC_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb"; + public static final String READ_CHARACTERISTIC_UUID = "0000ffe2-0000-1000-8000-00805f9b34fb"; } diff --git a/app/src/main/java/com/ubudu/iot/sample/fragment/BaseFragment.java b/app/src/main/java/com/ubudu/iot/sample/fragment/BaseFragment.java index 4ebbb56..182b4ef 100644 --- a/app/src/main/java/com/ubudu/iot/sample/fragment/BaseFragment.java +++ b/app/src/main/java/com/ubudu/iot/sample/fragment/BaseFragment.java @@ -56,11 +56,12 @@ public interface ViewController { void onScannedDevicesFragmentResumed(); void onAdvertiseRequested(); void onScanningRequested(); - void onSendMessageRequested(String message); + void onSendMessageRequested(byte[] data); void onScannedDevicesFragmentPaused(); void onPeripheralFragmentRequested(); void onPeripheralFragmentResumed(); + void onPeripheralFragmentPaused(); boolean isAdvertising(); @@ -72,8 +73,6 @@ public interface ViewController { void onDisconnectRequested(); - void onAudioSampleRateChanged(int sampleRateHz); - void onRssiFilterThresholdChanged(int rssiThreshold); void onCommFragmentPaused(); diff --git a/app/src/main/java/com/ubudu/iot/sample/fragment/CommFragment.java b/app/src/main/java/com/ubudu/iot/sample/fragment/CommFragment.java index 6f5ca34..85b71b9 100644 --- a/app/src/main/java/com/ubudu/iot/sample/fragment/CommFragment.java +++ b/app/src/main/java/com/ubudu/iot/sample/fragment/CommFragment.java @@ -2,10 +2,9 @@ import android.os.Bundle; import android.support.annotation.Nullable; -import android.support.v7.widget.PopupMenu; -import android.text.InputType; +import android.text.Editable; +import android.text.TextWatcher; import android.view.LayoutInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -19,6 +18,8 @@ import com.ubudu.iot.sample.R; import com.ubudu.iot.sample.util.ToastUtil; +import java.io.UnsupportedEncodingException; + import butterknife.BindView; import butterknife.ButterKnife; @@ -41,7 +42,7 @@ public class CommFragment extends BaseFragment { @BindView(R.id.loopback_mode) CheckBox loopBackModeCheckBox; @BindView(R.id.set_bytes_count_mode) - CheckBox byteCountMode; + CheckBox ascendingBytesModeCheckBox; @BindView(R.id.stream) CheckBox streamCheckBox; @BindView(R.id.stream_delay_edit_text) @@ -52,7 +53,12 @@ public class CommFragment extends BaseFragment { TextView iterationsTextView; @BindView(R.id.iteration_delay_text_view) TextView iterationDelayTextView; - + @BindView(R.id.message_text_view) + TextView messageTitleTextView; + @BindView(R.id.bytes_count_edit_text) + EditText bytesCountEditText; + @BindView(R.id.bytes_count_text_view) + TextView bytesCountTextView; @BindView(R.id.communication_layout) LinearLayout communicationLayout; @@ -96,27 +102,68 @@ public void onClick(View view) { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(isChecked){ + ascendingBytesModeCheckBox.setChecked(false); + streamCheckBox.setChecked(false); + ascendingBytesModeCheckBox.setChecked(false); + + ascendingBytesModeCheckBox.setEnabled(false); + streamCheckBox.setEnabled(false); + messageEditText.setText(""); messageEditText.setEnabled(false); sendDataButton.setVisibility(View.GONE); } else { messageEditText.setEnabled(true); sendDataButton.setVisibility(View.VISIBLE); + + ascendingBytesModeCheckBox.setEnabled(true); + streamCheckBox.setEnabled(true); } } }); - byteCountMode.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + ascendingBytesModeCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(isChecked){ - messageEditText.setText(""); - messageEditText.setHint("Type message size"); - messageEditText.setInputType(InputType.TYPE_CLASS_NUMBER); + bytesCountTextView.setVisibility(View.VISIBLE); + bytesCountEditText.setVisibility(View.VISIBLE); + messageEditText.setEnabled(false); + messageEditText.setText(getAscendingBytesDataString(Integer.parseInt(bytesCountEditText.getText().toString()))); + + loopBackModeCheckBox.setEnabled(false); + + bytesCountEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + try { + messageEditText.setText(getAscendingBytesDataString(Integer.parseInt(bytesCountEditText.getText().toString()))); + } catch(Exception e) { + messageEditText.setText(getAscendingBytesDataString(1)); + e.printStackTrace(); + } + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + } else { + if(!streamCheckBox.isChecked()) { + loopBackModeCheckBox.setEnabled(true); + } + bytesCountTextView.setVisibility(View.GONE); + bytesCountEditText.setVisibility(View.GONE); + messageEditText.setEnabled(true); + messageTitleTextView.setText(getResources().getString(R.string.string_message)); messageEditText.setText(""); - messageEditText.setHint("Type string message"); - messageEditText.setInputType(InputType.TYPE_CLASS_TEXT); } } }); @@ -125,11 +172,15 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(isChecked) { + loopBackModeCheckBox.setEnabled(false); streamDelayEditText.setVisibility(View.VISIBLE); streamIterationsEditText.setVisibility(View.VISIBLE); iterationsTextView.setVisibility(View.VISIBLE); iterationDelayTextView.setVisibility(View.VISIBLE); } else { + if(!ascendingBytesModeCheckBox.isChecked()) { + loopBackModeCheckBox.setEnabled(true); + } streamDelayEditText.setVisibility(View.GONE); streamIterationsEditText.setVisibility(View.GONE); iterationsTextView.setVisibility(View.GONE); @@ -148,18 +199,7 @@ public void onClick(View v) { } public void requestSend() { - sendDataButton.setEnabled(false); - String output = ""; - if(!byteCountMode.isChecked()) { - output = messageEditText.getText().toString(); - } else { - StringBuilder data = new StringBuilder(); - for(int i=0; i= rssiThreshold){ adapter.add(device); removedDevices.remove(i); i--; @@ -149,7 +147,7 @@ private void filterDevices(String filter) { if(!filter.isEmpty() && (device.getDevice().getName()==null || ( (device.getDevice().getName()!=null && !device.getDevice().getName().contains(filter)) - && !device.getDevice().getAddress().contains(filter)))) { + && !device.getDevice().getAddress().contains(filter))) || device.getRssi()> 3) - 0x02, 0x00, // Block alignment (2): blkalign = Number of channels * (Precision >> 3) - 0x10, 0x00, // Precision (Number of bits per sample) (16) - 0x00, 0x00, // Nothing? - // -- END OF FMT BLOCK -- - 0x64, 0x61, 0x74, 0x61, // 'DATA' - 0x00, 0x00, 0x00, 0x00 // Size of content in bytes (will be overwritten) - }; - - private DecoderListener mListener; - private byte[] mFrame; - private int mPositionInFrame; - private int mDecodedDataSize; - private int mReceivedDataSize; - private int mFramesCount; - private int mFramesLost; - private int mPacketsCount; - private int mInvalidPacketsCount; - private long mStartTime; - - private RandomAccessFile mOutputStream; - private File mTempFile; - - public static interface DecoderListener { - /** - * A full frame (13 packets) has been decoded. - * - * @param pcm - * the decoded frame in PCM format - * @param frameNumber - * the frame index (starting from 0) - */ - public void onFrameDecoded(final byte[] pcm, final int frameNumber); - } - - public ADPCMDecoder(final Context context, final boolean persistent) { - mFrame = new byte[FRAME_SIZE]; - mPositionInFrame = 0; - mDecodedDataSize = 0; - mReceivedDataSize = 0; - mPacketsCount = 0; - mInvalidPacketsCount = 0; - mFramesCount = 0; - mFramesLost = 0; - mStartTime = 0L; - - if (persistent) { - try { - final File tempDir = context.getCacheDir(); - final File wav = mTempFile = File.createTempFile("voice", ".wav", tempDir); - mOutputStream = new RandomAccessFile(wav, "rw"); - mOutputStream.write(WAV_HEADER); - } catch (final IOException e) { - Log.e(TAG, "Error while creating temporary file", e); - } - } - } - - /** - * Sets the decoder listener. - * - * @param listener - */ - public void setListener(final DecoderListener listener) { - mListener = listener; - } - - /** - * Adds the next frame packet. When the frame is completed it will be decoded and saved to the temporary file. - * you may register {@link DecoderListener} with {@link #setListener(DecoderListener)} to obtain each decoded frame for visualization. - * - * @param packet - * the packet from the Voice Input Module - */ - public void add(final byte[] packet) { - if (mStartTime == 0) - mStartTime = SystemClock.elapsedRealtime(); - - ++mPacketsCount; - mReceivedDataSize += packet.length; - - // Copy the packet to the frame buffer - System.arraycopy(packet, 0, mFrame, mPositionInFrame, packet.length); - mPositionInFrame += packet.length; - - // If frame is completed, decode it and save - if (mPositionInFrame == FRAME_SIZE) { - mPositionInFrame = 0; - ++mFramesCount; - - // Decode ADPCM -> PCM - final byte[] pcm = decode(mFrame); - - // Write data to the temporary file - try { - if (mOutputStream != null) - mOutputStream.write(pcm); - mDecodedDataSize += pcm.length; - } catch (final IOException e) { - Log.e(TAG, "Error while writing PCM data to file", e); - } - - // Notify the listener (if any) - if (mListener != null) - mListener.onFrameDecoded(pcm, mFramesCount - 1); - } - } - - /** - * Saves the PCM voice to the temporary file. - * - * @return the WAV file - */ - public File save() { - try { - // Write RIFF size - final ByteBuffer riffSize = ByteBuffer.allocate(4); - riffSize.order(ByteOrder.LITTLE_ENDIAN); - riffSize.putInt(mDecodedDataSize + 46); - mOutputStream.seek(4L); - mOutputStream.write(riffSize.array()); - - // Write data size - riffSize.putInt(0, mDecodedDataSize); - mOutputStream.seek(42L); - mOutputStream.write(riffSize.array()); - - mOutputStream.close(); - mOutputStream = null; - } catch (final IOException e) { - Log.e(TAG, "Error while closing temporary file", e); - } - return mTempFile; - } - - /** - * Returns true if no voice packets were decoded, false otherwise. - */ - public boolean isEmpty() { - return mPacketsCount == 0; - } - - /** - * Returns the total number of packets that were received, including invalid ones. - * - * @return the total number of received packets - */ - public int getPacketsCount() { - return mPacketsCount; - } - - /** - * Returns the number of frames decoded until now. - * - * @return the number of frames - */ - public int getFramesCount() { - return mFramesCount; - } - - /** - * Returns the number of invalid frames. Sometimes, due to the Android lag or bug, a packet is skipped and the frame may not be decoded. - * The decoder must discard some packets until a packet with size 19 bytes come (the last one in frame) when it starts decoding a new frame. - * - * @return the number of frames that were lost on Android size - */ - public int getInvalidFramesCount() { - return (mInvalidPacketsCount + 12) / 13; - } - - /** - * Returns the estimated number of lost frames. This number is calculated using by knowing the voice duration and expected frames number. - * - * @return the number of frames that were skipped by the Voice Input Module due to the slow connection. - */ - public int getFramesLost() { - final float duration = SystemClock.elapsedRealtime() - mStartTime; // transfer duration in milliseconds - /* - * The sample frequency is 16kHz = 16000 samples per second. There are 512 samples in each frame (13 packets), so about 31.25 frames are expected every second. - */ - final int expectedFrames = (int) (duration * 31.25f / 1000.0f); - if (expectedFrames - mFramesCount - mFramesLost > 0) - mFramesLost++; - return mFramesLost; - } - - /** - * Returns the size of the decoded voice (in PCM format) in bytes. - * - * @return PCM size in bytes - */ - public int getDataSize() { - return mDecodedDataSize; - } - - /** - * Returns the size of compressed voice (in ADPCM format) sent by the Voice Input Module, including invalid packets. - * - * @return ADPCM size in bytes - */ - public int getReceivedDataSize() { - return mReceivedDataSize; - } - - /** - * Decodes the ADPCM frame into PCM format - * - * @param adpcm - * the input in ADPCM format - * @return the PCM decoded array - */ - private byte[] decode(final byte[] adpcm) { - final byte[] pcm = new byte[512]; - - // The first 2 bytes of ADPCM frame are the predicted value - int valuePredicted = readShort(adpcm, 0); - // The 3rd byte is the index value - int index = adpcm[2]; - if (index < 0) - index = 0; - if (index > 88) - index = 88; - - int diff; /* Current change to valuePredicted */ - boolean bufferStep = false; - int inputBuffer = 0; - int delta = 0; - int sign = 0; - int step = STEP_SIZE_TABLE[index]; - - for (int in = 3, out = 0; in < adpcm.length; out += 2) { - /* Step 1 - get the delta value */ - if (bufferStep) { - delta = inputBuffer & 0x0F; - in++; - } else { - inputBuffer = adpcm[in]; - delta = (inputBuffer >> 4) & 0x0F; - } - bufferStep = !bufferStep; - - /* Step 2 - Find new index value (for later) */ - index += INDEX_TABLE[delta]; - if (index < 0) - index = 0; - if (index > 88) - index = 88; - - /* Step 3 - Separate sign and magnitude */ - sign = delta & 8; - delta = delta & 7; - - /* Step 4 - Compute difference and new predicted value */ - diff = step >> 3; - if ((delta & 4) > 0) - diff += step; - if ((delta & 2) > 0) - diff += step >> 1; - if ((delta & 1) > 0) - diff += step >> 2; - - if (sign > 0) - valuePredicted -= diff; - else - valuePredicted += diff; - - /* Step 5 - clamp output value */ - if (valuePredicted > 32767) - valuePredicted = 32767; - else if (valuePredicted < -32768) - valuePredicted = -32768; - - /* Step 6 - Update step value */ - step = STEP_SIZE_TABLE[index]; - - /* Step 7 - Output value */ - writeShort(pcm, out, valuePredicted); - } - return pcm; - } - - private static short readShort(final byte[] data, final int start) { - int b1 = data[start] & 0xff; - int b2 = data[start + 1] & 0xff; - - return (short) (b1 << 8 | b2 << 0); - } - - private static void writeShort(final byte[] data, final int start, final int value) { - data[start] = (byte) (value & 0xff); - data[start + 1] = (byte) ((value >>> 8) & 0xFF); - } - -} diff --git a/app/src/main/res/layout/fragment_comm.xml b/app/src/main/res/layout/fragment_comm.xml index 52b32de..9fa229e 100644 --- a/app/src/main/res/layout/fragment_comm.xml +++ b/app/src/main/res/layout/fragment_comm.xml @@ -18,12 +18,33 @@ app:layout_constraintVertical_bias="0.0" tools:context=".fragment.BaseFragment"> +