├── .gitignore
├── README.md
├── android
└── serial-ble-rc
│ └── SimpleBluetoothLeTerminal
│ ├── .gitignore
│ ├── LICENSE.txt
│ ├── README.md
│ ├── app
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── de
│ │ │ └── kai_morich
│ │ │ └── simple_bluetooth_le_terminal
│ │ │ ├── Constants.java
│ │ │ ├── MainActivity.java
│ │ │ ├── SerialListener.java
│ │ │ ├── SerialService.java
│ │ │ ├── SerialSocket.java
│ │ │ ├── TextUtil.java
│ │ │ ├── fragments
│ │ │ ├── DevicesFragment.java
│ │ │ ├── JoystickFragment.java
│ │ │ ├── PIDSettingsFragment.java
│ │ │ ├── SerialEnabledFragment.java
│ │ │ └── TerminalFragment.java
│ │ │ └── views
│ │ │ └── JoystickView.java
│ │ └── res
│ │ ├── drawable-hdpi
│ │ ├── ic_clear_white_24dp.png
│ │ └── ic_notification.png
│ │ ├── drawable-mdpi
│ │ ├── ic_clear_white_24dp.png
│ │ └── ic_notification.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_clear_white_24dp.png
│ │ └── ic_notification.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_clear_white_24dp.png
│ │ └── ic_notification.png
│ │ ├── drawable-xxxhdpi
│ │ ├── ic_clear_white_24dp.png
│ │ └── ic_notification.png
│ │ ├── drawable
│ │ ├── ic_delete_white_24dp.xml
│ │ └── ic_send_white_24dp.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── device_list_header.xml
│ │ ├── device_list_item.xml
│ │ ├── fragment_joystick.xml
│ │ ├── fragment_pid_settings.xml
│ │ └── fragment_terminal.xml
│ │ ├── menu
│ │ ├── menu_devices.xml
│ │ ├── menu_joystick.xml
│ │ └── menu_terminal.xml
│ │ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ └── values
│ │ ├── arrays.xml
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradlew
│ ├── gradlew.bat
│ └── settings.gradle
├── docs
└── stepper_frequency_estimation.py
├── drawings
└── v3
│ ├── bottom.dxf
│ ├── middle.dxf
│ ├── side.dxf
│ └── top-mount.dxf
├── esp32
├── README.md
├── balancing-robot
│ ├── .gitignore
│ ├── .vscode
│ │ ├── extensions.json
│ │ └── settings.json
│ ├── README.md
│ ├── include
│ │ └── README
│ ├── lib
│ │ └── README
│ ├── platformio.ini
│ ├── src
│ │ ├── indicator
│ │ │ ├── Indicator.cpp
│ │ │ └── Indicator.h
│ │ ├── main.cpp
│ │ ├── pid
│ │ │ ├── PID.cpp
│ │ │ └── PID.h
│ │ └── stepper
│ │ │ ├── Stepper.cpp
│ │ │ └── Stepper.h
│ └── test
│ │ └── README
└── docs
│ ├── test_bluetooth_classic.md
│ ├── test_imu_sketch.md
│ ├── test_rgb_led_sketch.md
│ └── test_steppers_sketch.md
├── nano
└── twbr-nano
│ ├── .gitignore
│ ├── .vscode
│ └── extensions.json
│ ├── include
│ └── README
│ ├── lib
│ └── README
│ ├── platformio.ini
│ ├── src
│ ├── main.cpp
│ └── pid
│ │ ├── PID.cpp
│ │ └── PID.h
│ └── test
│ └── README
└── r-pi
├── README.md
├── configure_wifi.sh
├── docs
├── install_deps.md
└── rpi_shield.md
├── mpu_stream_client
├── README.md
├── mpu_client.html
├── requirements.txt
└── src
│ └── mpu_stream_client.py
├── requirements.txt
├── run.sh
└── src
├── __init__.py
├── balance.py
├── control_stepper_with_mpu.py
├── motor
├── __init__.py
└── stepper.py
├── mpu_test.py
├── recv_mpu_data.py
├── roll_pitch_test.py
├── sensor
├── __init__.py
└── mpu.py
├── stepper_test.py
├── stream_mpu_data.py
└── util
├── __init__.py
├── pid.py
└── timed_task.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | __pycache__/
3 |
4 | .DS_Store
5 | sftp-config.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Self-balancing robot
2 |
3 | BLE-controlled self-balancing robot
4 |
5 | - [Android BLE Joystick](/android/serial-ble-rc)
6 | - [Arduino Code](/nano/twbr-nano)
7 |
8 | ## BOM
9 |
10 | - Keywish BLE Nano x1
11 | - A4988 Stepper Driver x2
12 | - Stepper motor Nema17 x2
13 | - MPU6050 x1
14 |
15 | ## References
16 |
17 | - [Hackaday](https://hackaday.io/project/180126-self-balancing-robot-for-humans/)
18 | - [Arduino Project](https://create.arduino.cc/projecthub/zjor/self-balancing-robot-with-arduino-nano-and-steppers-9bf019)
19 | - [](https://youtu.be/_VPTOirKccM)
20 | - [Habr](https://habr.com/ru/post/575662/)
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | /.gradle/
3 | /.idea/
4 | /local.properties
5 | /app/build/
6 | /build/
7 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Kai Morich
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 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.codacy.com/manual/kai-morich/SimpleBluetoothLeTerminal?utm_source=github.com&utm_medium=referral&utm_content=kai-morich/SimpleBluetoothLeTerminal&utm_campaign=Badge_Grade)
2 |
3 | # SimpleBluetoothLeTerminal
4 |
5 | This Android app provides a line-oriented terminal / console for Bluetooth LE (4.x) devices implementing a custom serial profile
6 |
7 | For an overview on Android BLE communication see
8 | [Android Bluetooth LE Overview](https://developer.android.com/guide/topics/connectivity/bluetooth/ble-overview).
9 |
10 | In contrast to classic Bluetooth, there is no predifined serial profile for Bluetooth LE,
11 | so each vendor uses GATT services with different service and characteristic UUIDs.
12 |
13 | This app includes UUIDs for widely used serial profiles:
14 | - Nordic Semiconductor nRF51822
15 | - Texas Instruments CC254x
16 | - Microchip RN4870/1
17 | - Telit Bluemod
18 |
19 | ## Motivation
20 |
21 | I got various requests asking for help with Android development or source code for my
22 | [Serial Bluetooth Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal) app.
23 | Here you find a simplified version of my app.
24 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 29
5 | defaultConfig {
6 | targetSdkVersion 29
7 | minSdkVersion 18
8 | vectorDrawables.useSupportLibrary true
9 |
10 | applicationId "de.kai_morich.simple_bluetooth_le_terminal"
11 | versionCode 1
12 | versionName "1.0"
13 | }
14 | compileOptions {
15 | sourceCompatibility JavaVersion.VERSION_1_8
16 | targetCompatibility JavaVersion.VERSION_1_8
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | }
25 |
26 | dependencies {
27 | implementation 'androidx.appcompat:appcompat:1.2.0'
28 | implementation 'com.google.android.material:material:1.3.0'
29 | implementation 'androidx.preference:preference:1.1.1'
30 | implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
31 | implementation 'io.reactivex.rxjava3:rxjava:3.0.0'
32 | }
33 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/src/main/java/de/kai_morich/simple_bluetooth_le_terminal/Constants.java:
--------------------------------------------------------------------------------
1 | package de.kai_morich.simple_bluetooth_le_terminal;
2 |
3 | class Constants {
4 |
5 | // values have to be globally unique
6 | static final String INTENT_ACTION_DISCONNECT = BuildConfig.APPLICATION_ID + ".Disconnect";
7 | static final String NOTIFICATION_CHANNEL = BuildConfig.APPLICATION_ID + ".Channel";
8 | static final String INTENT_CLASS_MAIN_ACTIVITY = BuildConfig.APPLICATION_ID + ".MainActivity";
9 |
10 | // values have to be unique within each app
11 | static final int NOTIFY_MANAGER_START_FOREGROUND_SERVICE = 1001;
12 |
13 | private Constants() {}
14 | }
15 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/src/main/java/de/kai_morich/simple_bluetooth_le_terminal/MainActivity.java:
--------------------------------------------------------------------------------
1 | package de.kai_morich.simple_bluetooth_le_terminal;
2 |
3 | import android.os.Bundle;
4 |
5 | import androidx.appcompat.app.AppCompatActivity;
6 | import androidx.appcompat.widget.Toolbar;
7 | import androidx.fragment.app.FragmentManager;
8 |
9 | import de.kai_morich.simple_bluetooth_le_terminal.fragments.DevicesFragment;
10 |
11 | public class MainActivity extends AppCompatActivity implements FragmentManager.OnBackStackChangedListener {
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_main);
17 | Toolbar toolbar = findViewById(R.id.toolbar);
18 | setSupportActionBar(toolbar);
19 | getSupportFragmentManager().addOnBackStackChangedListener(this);
20 | if (savedInstanceState == null)
21 | getSupportFragmentManager().beginTransaction().add(R.id.fragment, new DevicesFragment(), "devices").commit();
22 | else
23 | onBackStackChanged();
24 | }
25 |
26 | @Override
27 | public void onBackStackChanged() {
28 | getSupportActionBar().setDisplayHomeAsUpEnabled(getSupportFragmentManager().getBackStackEntryCount()>0);
29 | }
30 |
31 | @Override
32 | public boolean onSupportNavigateUp() {
33 | onBackPressed();
34 | return true;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/src/main/java/de/kai_morich/simple_bluetooth_le_terminal/SerialListener.java:
--------------------------------------------------------------------------------
1 | package de.kai_morich.simple_bluetooth_le_terminal;
2 |
3 | public interface SerialListener {
4 | void onSerialConnect();
5 |
6 | void onSerialConnectError(Exception e);
7 |
8 | void onSerialRead(byte[] data);
9 |
10 | void onSerialIoError(Exception e);
11 | }
12 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/src/main/java/de/kai_morich/simple_bluetooth_le_terminal/SerialService.java:
--------------------------------------------------------------------------------
1 | package de.kai_morich.simple_bluetooth_le_terminal;
2 |
3 | import android.app.Notification;
4 | import android.app.NotificationChannel;
5 | import android.app.NotificationManager;
6 | import android.app.PendingIntent;
7 | import android.app.Service;
8 | import android.content.Context;
9 | import android.content.Intent;
10 | import android.os.Binder;
11 | import android.os.Build;
12 | import android.os.Handler;
13 | import android.os.IBinder;
14 | import android.os.Looper;
15 |
16 | import androidx.annotation.Nullable;
17 | import androidx.core.app.NotificationCompat;
18 |
19 | import java.io.IOException;
20 | import java.util.LinkedList;
21 | import java.util.Queue;
22 |
23 | /**
24 | * create notification and queue serial data while activity is not in the foreground
25 | * use listener chain: SerialSocket -> SerialService -> UI fragment
26 | */
27 | public class SerialService extends Service implements SerialListener {
28 |
29 | public class SerialBinder extends Binder {
30 | public SerialService getService() { return SerialService.this; }
31 | }
32 |
33 | private enum QueueType {Connect, ConnectError, Read, IoError}
34 |
35 | private static class QueueItem {
36 | QueueType type;
37 | byte[] data;
38 | Exception e;
39 |
40 | QueueItem(QueueType type, byte[] data, Exception e) { this.type=type; this.data=data; this.e=e; }
41 | }
42 |
43 | private final Handler mainLooper;
44 | private final IBinder binder;
45 | private final Queue queue1, queue2;
46 |
47 | private SerialSocket socket;
48 | private SerialListener listener;
49 | private boolean connected;
50 |
51 | /**
52 | * Lifecylce
53 | */
54 | public SerialService() {
55 | mainLooper = new Handler(Looper.getMainLooper());
56 | binder = new SerialBinder();
57 | queue1 = new LinkedList<>();
58 | queue2 = new LinkedList<>();
59 | }
60 |
61 | @Override
62 | public void onDestroy() {
63 | cancelNotification();
64 | disconnect();
65 | super.onDestroy();
66 | }
67 |
68 | @Nullable
69 | @Override
70 | public IBinder onBind(Intent intent) {
71 | return binder;
72 | }
73 |
74 | /**
75 | * Api
76 | */
77 | public void connect(SerialSocket socket) throws IOException {
78 | socket.connect(this);
79 | this.socket = socket;
80 | connected = true;
81 | }
82 |
83 | public void disconnect() {
84 | connected = false; // ignore data,errors while disconnecting
85 | cancelNotification();
86 | if(socket != null) {
87 | socket.disconnect();
88 | socket = null;
89 | }
90 | }
91 |
92 | public void write(byte[] data) throws IOException {
93 | if(!connected)
94 | throw new IOException("not connected");
95 | socket.write(data);
96 | }
97 |
98 | public void attach(SerialListener listener) {
99 | if(Looper.getMainLooper().getThread() != Thread.currentThread())
100 | throw new IllegalArgumentException("not in main thread");
101 | cancelNotification();
102 | // use synchronized() to prevent new items in queue2
103 | // new items will not be added to queue1 because mainLooper.post and attach() run in main thread
104 | synchronized (this) {
105 | this.listener = listener;
106 | }
107 | for(QueueItem item : queue1) {
108 | switch(item.type) {
109 | case Connect: listener.onSerialConnect (); break;
110 | case ConnectError: listener.onSerialConnectError (item.e); break;
111 | case Read: listener.onSerialRead (item.data); break;
112 | case IoError: listener.onSerialIoError (item.e); break;
113 | }
114 | }
115 | for(QueueItem item : queue2) {
116 | switch(item.type) {
117 | case Connect: listener.onSerialConnect (); break;
118 | case ConnectError: listener.onSerialConnectError (item.e); break;
119 | case Read: listener.onSerialRead (item.data); break;
120 | case IoError: listener.onSerialIoError (item.e); break;
121 | }
122 | }
123 | queue1.clear();
124 | queue2.clear();
125 | }
126 |
127 | public void detach() {
128 | if(connected)
129 | createNotification();
130 | // items already in event queue (posted before detach() to mainLooper) will end up in queue1
131 | // items occurring later, will be moved directly to queue2
132 | // detach() and mainLooper.post run in the main thread, so all items are caught
133 | listener = null;
134 | }
135 |
136 | private void createNotification() {
137 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
138 | NotificationChannel nc = new NotificationChannel(Constants.NOTIFICATION_CHANNEL, "Background service", NotificationManager.IMPORTANCE_LOW);
139 | nc.setShowBadge(false);
140 | NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
141 | nm.createNotificationChannel(nc);
142 | }
143 | Intent disconnectIntent = new Intent()
144 | .setAction(Constants.INTENT_ACTION_DISCONNECT);
145 | Intent restartIntent = new Intent()
146 | .setClassName(this, Constants.INTENT_CLASS_MAIN_ACTIVITY)
147 | .setAction(Intent.ACTION_MAIN)
148 | .addCategory(Intent.CATEGORY_LAUNCHER);
149 | PendingIntent disconnectPendingIntent = PendingIntent.getBroadcast(this, 1, disconnectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
150 | PendingIntent restartPendingIntent = PendingIntent.getActivity(this, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT);
151 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL)
152 | .setSmallIcon(R.drawable.ic_notification)
153 | .setColor(getResources().getColor(R.color.colorPrimary))
154 | .setContentTitle(getResources().getString(R.string.app_name))
155 | .setContentText(socket != null ? "Connected to "+socket.getName() : "Background Service")
156 | .setContentIntent(restartPendingIntent)
157 | .setOngoing(true)
158 | .addAction(new NotificationCompat.Action(R.drawable.ic_clear_white_24dp, "Disconnect", disconnectPendingIntent));
159 | // @drawable/ic_notification created with Android Studio -> New -> Image Asset using @color/colorPrimaryDark as background color
160 | // Android < API 21 does not support vectorDrawables in notifications, so both drawables used here, are created as .png instead of .xml
161 | Notification notification = builder.build();
162 | startForeground(Constants.NOTIFY_MANAGER_START_FOREGROUND_SERVICE, notification);
163 | }
164 |
165 | private void cancelNotification() {
166 | stopForeground(true);
167 | }
168 |
169 | /**
170 | * SerialListener
171 | */
172 | public void onSerialConnect() {
173 | if(connected) {
174 | synchronized (this) {
175 | if (listener != null) {
176 | mainLooper.post(() -> {
177 | if (listener != null) {
178 | listener.onSerialConnect();
179 | } else {
180 | queue1.add(new QueueItem(QueueType.Connect, null, null));
181 | }
182 | });
183 | } else {
184 | queue2.add(new QueueItem(QueueType.Connect, null, null));
185 | }
186 | }
187 | }
188 | }
189 |
190 | public void onSerialConnectError(Exception e) {
191 | if(connected) {
192 | synchronized (this) {
193 | if (listener != null) {
194 | mainLooper.post(() -> {
195 | if (listener != null) {
196 | listener.onSerialConnectError(e);
197 | } else {
198 | queue1.add(new QueueItem(QueueType.ConnectError, null, e));
199 | cancelNotification();
200 | disconnect();
201 | }
202 | });
203 | } else {
204 | queue2.add(new QueueItem(QueueType.ConnectError, null, e));
205 | cancelNotification();
206 | disconnect();
207 | }
208 | }
209 | }
210 | }
211 |
212 | public void onSerialRead(byte[] data) {
213 | if(connected) {
214 | synchronized (this) {
215 | if (listener != null) {
216 | mainLooper.post(() -> {
217 | if (listener != null) {
218 | listener.onSerialRead(data);
219 | } else {
220 | queue1.add(new QueueItem(QueueType.Read, data, null));
221 | }
222 | });
223 | } else {
224 | queue2.add(new QueueItem(QueueType.Read, data, null));
225 | }
226 | }
227 | }
228 | }
229 |
230 | public void onSerialIoError(Exception e) {
231 | if(connected) {
232 | synchronized (this) {
233 | if (listener != null) {
234 | mainLooper.post(() -> {
235 | if (listener != null) {
236 | listener.onSerialIoError(e);
237 | } else {
238 | queue1.add(new QueueItem(QueueType.IoError, null, e));
239 | cancelNotification();
240 | disconnect();
241 | }
242 | });
243 | } else {
244 | queue2.add(new QueueItem(QueueType.IoError, null, e));
245 | cancelNotification();
246 | disconnect();
247 | }
248 | }
249 | }
250 | }
251 |
252 | }
253 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/src/main/java/de/kai_morich/simple_bluetooth_le_terminal/SerialSocket.java:
--------------------------------------------------------------------------------
1 | package de.kai_morich.simple_bluetooth_le_terminal;
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.content.BroadcastReceiver;
12 | import android.content.Context;
13 | import android.content.Intent;
14 | import android.content.IntentFilter;
15 | import android.os.Build;
16 | import android.util.Log;
17 |
18 | import java.io.IOException;
19 | import java.security.InvalidParameterException;
20 | import java.util.ArrayList;
21 | import java.util.Arrays;
22 | import java.util.UUID;
23 |
24 | /**
25 | * wrap BLE communication into socket like class
26 | * - connect, disconnect and write as methods,
27 | * - read + status is returned by SerialListener
28 | */
29 | public class SerialSocket extends BluetoothGattCallback {
30 |
31 | /**
32 | * delegate device specific behaviour to inner class
33 | */
34 | private static class DeviceDelegate {
35 | boolean connectCharacteristics(BluetoothGattService s) { return true; }
36 | // following methods only overwritten for Telit devices
37 | void onDescriptorWrite(BluetoothGatt g, BluetoothGattDescriptor d, int status) { /*nop*/ }
38 | void onCharacteristicChanged(BluetoothGatt g, BluetoothGattCharacteristic c) {/*nop*/ }
39 | void onCharacteristicWrite(BluetoothGatt g, BluetoothGattCharacteristic c, int status) { /*nop*/ }
40 | boolean canWrite() { return true; }
41 | void disconnect() {/*nop*/ }
42 | }
43 |
44 | private static final UUID BLUETOOTH_LE_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
45 | private static final UUID BLUETOOTH_LE_CC254X_SERVICE = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb");
46 | private static final UUID BLUETOOTH_LE_CC254X_CHAR_RW = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb");
47 | private static final UUID BLUETOOTH_LE_NRF_SERVICE = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
48 | private static final UUID BLUETOOTH_LE_NRF_CHAR_RW2 = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e"); // read on microbit, write on adafruit
49 | private static final UUID BLUETOOTH_LE_NRF_CHAR_RW3 = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
50 | private static final UUID BLUETOOTH_LE_RN4870_SERVICE = UUID.fromString("49535343-FE7D-4AE5-8FA9-9FAFD205E455");
51 | private static final UUID BLUETOOTH_LE_RN4870_CHAR_RW = UUID.fromString("49535343-1E4D-4BD9-BA61-23C647249616");
52 |
53 | // https://play.google.com/store/apps/details?id=com.telit.tiosample
54 | // https://www.telit.com/wp-content/uploads/2017/09/TIO_Implementation_Guide_r6.pdf
55 | private static final UUID BLUETOOTH_LE_TIO_SERVICE = UUID.fromString("0000FEFB-0000-1000-8000-00805F9B34FB");
56 | private static final UUID BLUETOOTH_LE_TIO_CHAR_TX = UUID.fromString("00000001-0000-1000-8000-008025000000"); // WNR
57 | private static final UUID BLUETOOTH_LE_TIO_CHAR_RX = UUID.fromString("00000002-0000-1000-8000-008025000000"); // N
58 | private static final UUID BLUETOOTH_LE_TIO_CHAR_TX_CREDITS = UUID.fromString("00000003-0000-1000-8000-008025000000"); // W
59 | private static final UUID BLUETOOTH_LE_TIO_CHAR_RX_CREDITS = UUID.fromString("00000004-0000-1000-8000-008025000000"); // I
60 |
61 | private static final int MAX_MTU = 512; // BLE standard does not limit, some BLE 4.2 devices support 251, various source say that Android has max 512
62 | private static final int DEFAULT_MTU = 23;
63 | private static final String TAG = "SerialSocket";
64 |
65 | private final ArrayList writeBuffer;
66 | private final IntentFilter pairingIntentFilter;
67 | private final BroadcastReceiver pairingBroadcastReceiver;
68 | private final BroadcastReceiver disconnectBroadcastReceiver;
69 |
70 | private final Context context;
71 | private SerialListener listener;
72 | private DeviceDelegate delegate;
73 | private BluetoothDevice device;
74 | private BluetoothGatt gatt;
75 | private BluetoothGattCharacteristic readCharacteristic, writeCharacteristic;
76 |
77 | private boolean writePending;
78 | private boolean canceled;
79 | private boolean connected;
80 | private int payloadSize = DEFAULT_MTU-3;
81 |
82 | public SerialSocket(Context context, BluetoothDevice device) {
83 | if(context instanceof Activity)
84 | throw new InvalidParameterException("expected non UI context");
85 | this.context = context;
86 | this.device = device;
87 | writeBuffer = new ArrayList<>();
88 | pairingIntentFilter = new IntentFilter();
89 | pairingIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
90 | pairingIntentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
91 | pairingBroadcastReceiver = new BroadcastReceiver() {
92 | @Override
93 | public void onReceive(Context context, Intent intent) {
94 | onPairingBroadcastReceive(context, intent);
95 | }
96 | };
97 | disconnectBroadcastReceiver = new BroadcastReceiver() {
98 | @Override
99 | public void onReceive(Context context, Intent intent) {
100 | if(listener != null)
101 | listener.onSerialIoError(new IOException("background disconnect"));
102 | disconnect(); // disconnect now, else would be queued until UI re-attached
103 | }
104 | };
105 | }
106 |
107 | String getName() {
108 | return device.getName() != null ? device.getName() : device.getAddress();
109 | }
110 |
111 | void disconnect() {
112 | Log.d(TAG, "disconnect");
113 | listener = null; // ignore remaining data and errors
114 | device = null;
115 | canceled = true;
116 | synchronized (writeBuffer) {
117 | writePending = false;
118 | writeBuffer.clear();
119 | }
120 | readCharacteristic = null;
121 | writeCharacteristic = null;
122 | if(delegate != null)
123 | delegate.disconnect();
124 | if (gatt != null) {
125 | Log.d(TAG, "gatt.disconnect");
126 | gatt.disconnect();
127 | Log.d(TAG, "gatt.close");
128 | try {
129 | gatt.close();
130 | } catch (Exception ignored) {}
131 | gatt = null;
132 | connected = false;
133 | }
134 | try {
135 | context.unregisterReceiver(pairingBroadcastReceiver);
136 | } catch (Exception ignored) {
137 | }
138 | try {
139 | context.unregisterReceiver(disconnectBroadcastReceiver);
140 | } catch (Exception ignored) {
141 | }
142 | }
143 |
144 | /**
145 | * connect-success and most connect-errors are returned asynchronously to listener
146 | */
147 | void connect(SerialListener listener) throws IOException {
148 | if(connected || gatt != null)
149 | throw new IOException("already connected");
150 | canceled = false;
151 | this.listener = listener;
152 | context.registerReceiver(disconnectBroadcastReceiver, new IntentFilter(Constants.INTENT_ACTION_DISCONNECT));
153 | Log.d(TAG, "connect "+device);
154 | context.registerReceiver(pairingBroadcastReceiver, pairingIntentFilter);
155 | if (Build.VERSION.SDK_INT < 23) {
156 | Log.d(TAG, "connectGatt");
157 | gatt = device.connectGatt(context, false, this);
158 | } else {
159 | Log.d(TAG, "connectGatt,LE");
160 | gatt = device.connectGatt(context, false, this, BluetoothDevice.TRANSPORT_LE);
161 | }
162 | if (gatt == null)
163 | throw new IOException("connectGatt failed");
164 | // continues asynchronously in onPairingBroadcastReceive() and onConnectionStateChange()
165 | }
166 |
167 | private void onPairingBroadcastReceive(Context context, Intent intent) {
168 | // for ARM Mbed, Microbit, ... use pairing from Android bluetooth settings
169 | // for HM10-clone, ... pairing is initiated here
170 | BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
171 | if(device==null || !device.equals(this.device))
172 | return;
173 | switch (intent.getAction()) {
174 | case BluetoothDevice.ACTION_PAIRING_REQUEST:
175 | final int pairingVariant = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, -1);
176 | Log.d(TAG, "pairing request " + pairingVariant);
177 | onSerialConnectError(new IOException(context.getString(R.string.pairing_request)));
178 | // pairing dialog brings app to background (onPause), but it is still partly visible (no onStop), so there is no automatic disconnect()
179 | break;
180 | case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
181 | final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
182 | final int previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1);
183 | Log.d(TAG, "bond state " + previousBondState + "->" + bondState);
184 | break;
185 | default:
186 | Log.d(TAG, "unknown broadcast " + intent.getAction());
187 | break;
188 | }
189 | }
190 |
191 | @Override
192 | public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
193 | // status directly taken from gat_api.h, e.g. 133=0x85=GATT_ERROR ~= timeout
194 | if (newState == BluetoothProfile.STATE_CONNECTED) {
195 | Log.d(TAG,"connect status "+status+", discoverServices");
196 | if (!gatt.discoverServices())
197 | onSerialConnectError(new IOException("discoverServices failed"));
198 | } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
199 | if (connected)
200 | onSerialIoError (new IOException("gatt status " + status));
201 | else
202 | onSerialConnectError(new IOException("gatt status " + status));
203 | } else {
204 | Log.d(TAG, "unknown connect state "+newState+" "+status);
205 | }
206 | // continues asynchronously in onServicesDiscovered()
207 | }
208 |
209 | @Override
210 | public void onServicesDiscovered(BluetoothGatt gatt, int status) {
211 | Log.d(TAG, "servicesDiscovered, status " + status);
212 | if (canceled)
213 | return;
214 | connectCharacteristics1(gatt);
215 | }
216 |
217 | private void connectCharacteristics1(BluetoothGatt gatt) {
218 | boolean sync = true;
219 | writePending = false;
220 | for (BluetoothGattService gattService : gatt.getServices()) {
221 | if (gattService.getUuid().equals(BLUETOOTH_LE_CC254X_SERVICE))
222 | delegate = new Cc245XDelegate();
223 | if (gattService.getUuid().equals(BLUETOOTH_LE_RN4870_SERVICE))
224 | delegate = new Rn4870Delegate();
225 | if (gattService.getUuid().equals(BLUETOOTH_LE_NRF_SERVICE))
226 | delegate = new NrfDelegate();
227 | if (gattService.getUuid().equals(BLUETOOTH_LE_TIO_SERVICE))
228 | delegate = new TelitDelegate();
229 |
230 | if(delegate != null) {
231 | sync = delegate.connectCharacteristics(gattService);
232 | break;
233 | }
234 | }
235 | if(canceled)
236 | return;
237 | if(delegate==null || readCharacteristic==null || writeCharacteristic==null) {
238 | for (BluetoothGattService gattService : gatt.getServices()) {
239 | Log.d(TAG, "service "+gattService.getUuid());
240 | for(BluetoothGattCharacteristic characteristic : gattService.getCharacteristics())
241 | Log.d(TAG, "characteristic "+characteristic.getUuid());
242 | }
243 | onSerialConnectError(new IOException("no serial profile found"));
244 | return;
245 | }
246 | if(sync)
247 | connectCharacteristics2(gatt);
248 | }
249 |
250 | private void connectCharacteristics2(BluetoothGatt gatt) {
251 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
252 | Log.d(TAG, "request max MTU");
253 | if (!gatt.requestMtu(MAX_MTU))
254 | onSerialConnectError(new IOException("request MTU failed"));
255 | // continues asynchronously in onMtuChanged
256 | } else {
257 | connectCharacteristics3(gatt);
258 | }
259 | }
260 |
261 | @Override
262 | public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
263 | Log.d(TAG,"mtu size "+mtu+", status="+status);
264 | if(status == BluetoothGatt.GATT_SUCCESS) {
265 | payloadSize = mtu - 3;
266 | Log.d(TAG, "payload size "+payloadSize);
267 | }
268 | connectCharacteristics3(gatt);
269 | }
270 |
271 | private void connectCharacteristics3(BluetoothGatt gatt) {
272 | int writeProperties = writeCharacteristic.getProperties();
273 | if((writeProperties & (BluetoothGattCharacteristic.PROPERTY_WRITE + // Microbit,HM10-clone have WRITE
274 | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) ==0) { // HM10,TI uart,Telit have only WRITE_NO_RESPONSE
275 | onSerialConnectError(new IOException("write characteristic not writable"));
276 | return;
277 | }
278 | if(!gatt.setCharacteristicNotification(readCharacteristic,true)) {
279 | onSerialConnectError(new IOException("no notification for read characteristic"));
280 | return;
281 | }
282 | BluetoothGattDescriptor readDescriptor = readCharacteristic.getDescriptor(BLUETOOTH_LE_CCCD);
283 | if(readDescriptor == null) {
284 | onSerialConnectError(new IOException("no CCCD descriptor for read characteristic"));
285 | return;
286 | }
287 | int readProperties = readCharacteristic.getProperties();
288 | if((readProperties & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
289 | Log.d(TAG, "enable read indication");
290 | readDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
291 | }else if((readProperties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
292 | Log.d(TAG, "enable read notification");
293 | readDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
294 | } else {
295 | onSerialConnectError(new IOException("no indication/notification for read characteristic ("+readProperties+")"));
296 | return;
297 | }
298 | Log.d(TAG,"writing read characteristic descriptor");
299 | if(!gatt.writeDescriptor(readDescriptor)) {
300 | onSerialConnectError(new IOException("read characteristic CCCD descriptor not writable"));
301 | }
302 | // continues asynchronously in onDescriptorWrite()
303 | }
304 |
305 | @Override
306 | public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
307 | delegate.onDescriptorWrite(gatt, descriptor, status);
308 | if(canceled)
309 | return;
310 | if(descriptor.getCharacteristic() == readCharacteristic) {
311 | Log.d(TAG,"writing read characteristic descriptor finished, status="+status);
312 | if (status != BluetoothGatt.GATT_SUCCESS) {
313 | onSerialConnectError(new IOException("write descriptor failed"));
314 | } else {
315 | // onCharacteristicChanged with incoming data can happen after writeDescriptor(ENABLE_INDICATION/NOTIFICATION)
316 | // before confirmed by this method, so receive data can be shown before device is shown as 'Connected'.
317 | onSerialConnect();
318 | connected = true;
319 | Log.d(TAG, "connected");
320 | }
321 | }
322 | }
323 |
324 | /*
325 | * read
326 | */
327 | @Override
328 | public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
329 | if(canceled)
330 | return;
331 | delegate.onCharacteristicChanged(gatt, characteristic);
332 | if(canceled)
333 | return;
334 | if(characteristic == readCharacteristic) { // NOPMD - test object identity
335 | byte[] data = readCharacteristic.getValue();
336 | onSerialRead(data);
337 | Log.d(TAG,"read, len="+data.length);
338 | }
339 | }
340 |
341 | /*
342 | * write
343 | */
344 | void write(byte[] data) throws IOException {
345 | if(canceled || !connected || writeCharacteristic == null)
346 | throw new IOException("not connected");
347 | byte[] data0;
348 | synchronized (writeBuffer) {
349 | if(data.length <= payloadSize) {
350 | data0 = data;
351 | } else {
352 | data0 = Arrays.copyOfRange(data, 0, payloadSize);
353 | }
354 | if(!writePending && writeBuffer.isEmpty() && delegate.canWrite()) {
355 | writePending = true;
356 | } else {
357 | writeBuffer.add(data0);
358 | Log.d(TAG,"write queued, len="+data0.length);
359 | data0 = null;
360 | }
361 | if(data.length > payloadSize) {
362 | for(int i=1; i<(data.length+payloadSize-1)/payloadSize; i++) {
363 | int from = i*payloadSize;
364 | int to = Math.min(from+payloadSize, data.length);
365 | writeBuffer.add(Arrays.copyOfRange(data, from, to));
366 | Log.d(TAG,"write queued, len="+(to-from));
367 | }
368 | }
369 | }
370 | if(data0 != null) {
371 | writeCharacteristic.setValue(data0);
372 | if (!gatt.writeCharacteristic(writeCharacteristic)) {
373 | onSerialIoError(new IOException("write failed"));
374 | } else {
375 | Log.d(TAG,"write started, len="+data0.length);
376 | }
377 | }
378 | // continues asynchronously in onCharacteristicWrite()
379 | }
380 |
381 | @Override
382 | public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
383 | if(canceled || !connected || writeCharacteristic == null)
384 | return;
385 | if(status != BluetoothGatt.GATT_SUCCESS) {
386 | onSerialIoError(new IOException("write failed"));
387 | return;
388 | }
389 | delegate.onCharacteristicWrite(gatt, characteristic, status);
390 | if(canceled)
391 | return;
392 | if(characteristic == writeCharacteristic) { // NOPMD - test object identity
393 | Log.d(TAG,"write finished, status="+status);
394 | writeNext();
395 | }
396 | }
397 |
398 | private void writeNext() {
399 | final byte[] data;
400 | synchronized (writeBuffer) {
401 | if (!writeBuffer.isEmpty() && delegate.canWrite()) {
402 | writePending = true;
403 | data = writeBuffer.remove(0);
404 | } else {
405 | writePending = false;
406 | data = null;
407 | }
408 | }
409 | if(data != null) {
410 | writeCharacteristic.setValue(data);
411 | if (!gatt.writeCharacteristic(writeCharacteristic)) {
412 | onSerialIoError(new IOException("write failed"));
413 | } else {
414 | Log.d(TAG,"write started, len="+data.length);
415 | }
416 | }
417 | }
418 |
419 | /**
420 | * SerialListener
421 | */
422 | private void onSerialConnect() {
423 | if (listener != null)
424 | listener.onSerialConnect();
425 | }
426 |
427 | private void onSerialConnectError(Exception e) {
428 | canceled = true;
429 | if (listener != null)
430 | listener.onSerialConnectError(e);
431 | }
432 |
433 | private void onSerialRead(byte[] data) {
434 | if (listener != null)
435 | listener.onSerialRead(data);
436 | }
437 |
438 | private void onSerialIoError(Exception e) {
439 | writePending = false;
440 | canceled = true;
441 | if (listener != null)
442 | listener.onSerialIoError(e);
443 | }
444 |
445 | /**
446 | * device delegates
447 | */
448 |
449 | private class Cc245XDelegate extends DeviceDelegate {
450 | @Override
451 | boolean connectCharacteristics(BluetoothGattService gattService) {
452 | Log.d(TAG, "service cc254x uart");
453 | readCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_CC254X_CHAR_RW);
454 | writeCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_CC254X_CHAR_RW);
455 | return true;
456 | }
457 | }
458 |
459 | private class Rn4870Delegate extends DeviceDelegate {
460 | @Override
461 | boolean connectCharacteristics(BluetoothGattService gattService) {
462 | Log.d(TAG, "service rn4870 uart");
463 | readCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_RN4870_CHAR_RW);
464 | writeCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_RN4870_CHAR_RW);
465 | return true;
466 | }
467 | }
468 |
469 | private class NrfDelegate extends DeviceDelegate {
470 | @Override
471 | boolean connectCharacteristics(BluetoothGattService gattService) {
472 | Log.d(TAG, "service nrf uart");
473 | BluetoothGattCharacteristic rw2 = gattService.getCharacteristic(BLUETOOTH_LE_NRF_CHAR_RW2);
474 | BluetoothGattCharacteristic rw3 = gattService.getCharacteristic(BLUETOOTH_LE_NRF_CHAR_RW3);
475 | if (rw2 != null && rw3 != null) {
476 | int rw2prop = rw2.getProperties();
477 | int rw3prop = rw3.getProperties();
478 | boolean rw2write = (rw2prop & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0;
479 | boolean rw3write = (rw3prop & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0;
480 | Log.d(TAG, "characteristic properties " + rw2prop + "/" + rw3prop);
481 | if (rw2write && rw3write) {
482 | onSerialConnectError(new IOException("multiple write characteristics (" + rw2prop + "/" + rw3prop + ")"));
483 | } else if (rw2write) {
484 | writeCharacteristic = rw2;
485 | readCharacteristic = rw3;
486 | } else if (rw3write) {
487 | writeCharacteristic = rw3;
488 | readCharacteristic = rw2;
489 | } else {
490 | onSerialConnectError(new IOException("no write characteristic (" + rw2prop + "/" + rw3prop + ")"));
491 | }
492 | }
493 | return true;
494 | }
495 | }
496 |
497 | private class TelitDelegate extends DeviceDelegate {
498 | private BluetoothGattCharacteristic readCreditsCharacteristic, writeCreditsCharacteristic;
499 | private int readCredits, writeCredits;
500 |
501 | @Override
502 | boolean connectCharacteristics(BluetoothGattService gattService) {
503 | Log.d(TAG, "service telit tio 2.0");
504 | readCredits = 0;
505 | writeCredits = 0;
506 | readCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_TIO_CHAR_RX);
507 | writeCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_TIO_CHAR_TX);
508 | readCreditsCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_TIO_CHAR_RX_CREDITS);
509 | writeCreditsCharacteristic = gattService.getCharacteristic(BLUETOOTH_LE_TIO_CHAR_TX_CREDITS);
510 | if (readCharacteristic == null) {
511 | onSerialConnectError(new IOException("read characteristic not found"));
512 | return false;
513 | }
514 | if (writeCharacteristic == null) {
515 | onSerialConnectError(new IOException("write characteristic not found"));
516 | return false;
517 | }
518 | if (readCreditsCharacteristic == null) {
519 | onSerialConnectError(new IOException("read credits characteristic not found"));
520 | return false;
521 | }
522 | if (writeCreditsCharacteristic == null) {
523 | onSerialConnectError(new IOException("write credits characteristic not found"));
524 | return false;
525 | }
526 | if (!gatt.setCharacteristicNotification(readCreditsCharacteristic, true)) {
527 | onSerialConnectError(new IOException("no notification for read credits characteristic"));
528 | return false;
529 | }
530 | BluetoothGattDescriptor readCreditsDescriptor = readCreditsCharacteristic.getDescriptor(BLUETOOTH_LE_CCCD);
531 | if (readCreditsDescriptor == null) {
532 | onSerialConnectError(new IOException("no CCCD descriptor for read credits characteristic"));
533 | return false;
534 | }
535 | readCreditsDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
536 | Log.d(TAG,"writing read credits characteristic descriptor");
537 | if (!gatt.writeDescriptor(readCreditsDescriptor)) {
538 | onSerialConnectError(new IOException("read credits characteristic CCCD descriptor not writable"));
539 | return false;
540 | }
541 | Log.d(TAG, "writing read credits characteristic descriptor");
542 | return false;
543 | // continues asynchronously in connectCharacteristics2
544 | }
545 |
546 | @Override
547 | void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
548 | if(descriptor.getCharacteristic() == readCreditsCharacteristic) {
549 | Log.d(TAG, "writing read credits characteristic descriptor finished, status=" + status);
550 | if (status != BluetoothGatt.GATT_SUCCESS) {
551 | onSerialConnectError(new IOException("write credits descriptor failed"));
552 | } else {
553 | connectCharacteristics2(gatt);
554 | }
555 | }
556 | if(descriptor.getCharacteristic() == readCharacteristic) {
557 | Log.d(TAG, "writing read characteristic descriptor finished, status=" + status);
558 | if (status == BluetoothGatt.GATT_SUCCESS) {
559 | readCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
560 | writeCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
561 | grantReadCredits();
562 | // grantReadCredits includes gatt.writeCharacteristic(writeCreditsCharacteristic)
563 | // but we do not have to wait for confirmation, as it is the last write of connect phase.
564 | }
565 | }
566 | }
567 |
568 | @Override
569 | void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
570 | if(characteristic == readCreditsCharacteristic) { // NOPMD - test object identity
571 | int newCredits = readCreditsCharacteristic.getValue()[0];
572 | synchronized (writeBuffer) {
573 | writeCredits += newCredits;
574 | }
575 | Log.d(TAG, "got write credits +"+newCredits+" ="+writeCredits);
576 |
577 | if (!writePending && !writeBuffer.isEmpty()) {
578 | Log.d(TAG, "resume blocked write");
579 | writeNext();
580 | }
581 | }
582 | if(characteristic == readCharacteristic) { // NOPMD - test object identity
583 | grantReadCredits();
584 | Log.d(TAG, "read, credits=" + readCredits);
585 | }
586 | }
587 |
588 | @Override
589 | void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
590 | if(characteristic == writeCharacteristic) { // NOPMD - test object identity
591 | synchronized (writeBuffer) {
592 | if (writeCredits > 0)
593 | writeCredits -= 1;
594 | }
595 | Log.d(TAG, "write finished, credits=" + writeCredits);
596 | }
597 | if(characteristic == writeCreditsCharacteristic) { // NOPMD - test object identity
598 | Log.d(TAG,"write credits finished, status="+status);
599 | }
600 | }
601 |
602 | @Override
603 | boolean canWrite() {
604 | if(writeCredits > 0)
605 | return true;
606 | Log.d(TAG, "no write credits");
607 | return false;
608 | }
609 |
610 | @Override
611 | void disconnect() {
612 | readCreditsCharacteristic = null;
613 | writeCreditsCharacteristic = null;
614 | }
615 |
616 | private void grantReadCredits() {
617 | final int minReadCredits = 16;
618 | final int maxReadCredits = 64;
619 | if(readCredits > 0)
620 | readCredits -= 1;
621 | if(readCredits <= minReadCredits) {
622 | int newCredits = maxReadCredits - readCredits;
623 | readCredits += newCredits;
624 | byte[] data = new byte[] {(byte)newCredits};
625 | Log.d(TAG, "grant read credits +"+newCredits+" ="+readCredits);
626 | writeCreditsCharacteristic.setValue(data);
627 | if (!gatt.writeCharacteristic(writeCreditsCharacteristic)) {
628 | if(connected)
629 | onSerialIoError(new IOException("write read credits failed"));
630 | else
631 | onSerialConnectError(new IOException("write read credits failed"));
632 | }
633 | }
634 | }
635 |
636 | }
637 |
638 | }
639 |
--------------------------------------------------------------------------------
/android/serial-ble-rc/SimpleBluetoothLeTerminal/app/src/main/java/de/kai_morich/simple_bluetooth_le_terminal/TextUtil.java:
--------------------------------------------------------------------------------
1 | package de.kai_morich.simple_bluetooth_le_terminal;
2 |
3 | import android.text.Editable;
4 | import android.text.InputType;
5 | import android.text.Spannable;
6 | import android.text.SpannableStringBuilder;
7 | import android.text.TextWatcher;
8 | import android.text.style.BackgroundColorSpan;
9 | import android.widget.TextView;
10 |
11 | import androidx.annotation.ColorInt;
12 |
13 | import java.io.ByteArrayOutputStream;
14 |
15 | public final class TextUtil {
16 |
17 | @ColorInt static int caretBackground = 0xff666666;
18 |
19 | public final static String newline_crlf = "\r\n";
20 | public final static String newline_lf = "\n";
21 |
22 | public static byte[] fromHexString(final CharSequence s) {
23 | ByteArrayOutputStream buf = new ByteArrayOutputStream();
24 | byte b = 0;
25 | int nibble = 0;
26 | for(int pos = 0; pos='0' && c<='9') { nibble++; b *= 16; b += c-'0'; }
34 | if(c>='A' && c<='F') { nibble++; b *= 16; b += c-'A'+10; }
35 | if(c>='a' && c<='f') { nibble++; b *= 16; b += c-'a'+10; }
36 | }
37 | if(nibble>0)
38 | buf.write(b);
39 | return buf.toByteArray();
40 | }
41 |
42 | public static String toHexString(final byte[] buf) {
43 | return toHexString(buf, 0, buf.length);
44 | }
45 |
46 | public static String toHexString(final byte[] buf, int begin, int end) {
47 | StringBuilder sb = new StringBuilder(3*(end-begin));
48 | toHexString(sb, buf, begin, end);
49 | return sb.toString();
50 | }
51 |
52 | public static void toHexString(StringBuilder sb, final byte[] buf) {
53 | toHexString(sb, buf, 0, buf.length);
54 | }
55 |
56 | public static void toHexString(StringBuilder sb, final byte[] buf, int begin, int end) {
57 | for(int pos=begin; pos0)
59 | sb.append(' ');
60 | int c;
61 | c = (buf[pos]&0xff) / 16;
62 | if(c >= 10) c += 'A'-10;
63 | else c += '0';
64 | sb.append((char)c);
65 | c = (buf[pos]&0xff) % 16;
66 | if(c >= 10) c += 'A'-10;
67 | else c += '0';
68 | sb.append((char)c);
69 | }
70 | }
71 |
72 | /**
73 | * use https://en.wikipedia.org/wiki/Caret_notation to avoid invisible control characters
74 | */
75 | public static CharSequence toCaretString(CharSequence s, boolean keepNewline) {
76 | return toCaretString(s, keepNewline, s.length());
77 | }
78 |
79 | public static CharSequence toCaretString(CharSequence s, boolean keepNewline, int length) {
80 | boolean found = false;
81 | for (int pos = 0; pos < length; pos++) {
82 | if (s.charAt(pos) < 32 && (!keepNewline ||s.charAt(pos)!='\n')) {
83 | found = true;
84 | break;
85 | }
86 | }
87 | if(!found)
88 | return s;
89 | SpannableStringBuilder sb = new SpannableStringBuilder();
90 | for(int pos=0; pos= '0' && c <= '9') sb.append(c);
140 | if(c >= 'A' && c <= 'F') sb.append(c);
141 | if(c >= 'a' && c <= 'f') sb.append((char)(c+'A'-'a'));
142 | }
143 | for(i=2; i listItems = new ArrayList<>();
53 | private ArrayAdapter listAdapter;
54 |
55 | public DevicesFragment() {
56 | leScanCallback = (device, rssi, scanRecord) -> {
57 | if(device != null && getActivity() != null) {
58 | getActivity().runOnUiThread(() -> { updateScan(device); });
59 | }
60 | };
61 | discoveryBroadcastReceiver = new BroadcastReceiver() {
62 | @Override
63 | public void onReceive(Context context, Intent intent) {
64 | if(BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
65 | BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
66 | if(device.getType() != BluetoothDevice.DEVICE_TYPE_CLASSIC && getActivity() != null) {
67 | getActivity().runOnUiThread(() -> updateScan(device));
68 | }
69 | }
70 | if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
71 | scanState = ScanState.DISCOVERY_FINISHED; // don't cancel again
72 | stopScan();
73 | }
74 | }
75 | };
76 | discoveryIntentFilter = new IntentFilter();
77 | discoveryIntentFilter.addAction(BluetoothDevice.ACTION_FOUND);
78 | discoveryIntentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
79 | }
80 |
81 | @Override
82 | public void onCreate(Bundle savedInstanceState) {
83 | super.onCreate(savedInstanceState);
84 | setHasOptionsMenu(true);
85 | if(getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH))
86 | bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
87 | listAdapter = new ArrayAdapter(getActivity(), 0, listItems) {
88 | @NonNull
89 | @Override
90 | public View getView(int position, View view, @NonNull ViewGroup parent) {
91 | BluetoothDevice device = listItems.get(position);
92 | if (view == null)
93 | view = getActivity().getLayoutInflater().inflate(R.layout.device_list_item, parent, false);
94 | TextView text1 = view.findViewById(R.id.text1);
95 | TextView text2 = view.findViewById(R.id.text2);
96 | if(device.getName() == null || device.getName().isEmpty())
97 | text1.setText("");
98 | else
99 | text1.setText(device.getName());
100 | text2.setText(device.getAddress());
101 | return view;
102 | }
103 | };
104 | }
105 |
106 | @Override
107 | public void onActivityCreated(Bundle savedInstanceState) {
108 | super.onActivityCreated(savedInstanceState);
109 | setListAdapter(null);
110 | View header = getActivity().getLayoutInflater().inflate(R.layout.device_list_header, null, false);
111 | getListView().addHeaderView(header, null, false);
112 | setEmptyText("initializing...");
113 | ((TextView) getListView().getEmptyView()).setTextSize(18);
114 | setListAdapter(listAdapter);
115 | }
116 |
117 | @Override
118 | public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
119 | inflater.inflate(R.menu.menu_devices, menu);
120 | this.menu = menu;
121 | if (bluetoothAdapter == null) {
122 | menu.findItem(R.id.bt_settings).setEnabled(false);
123 | menu.findItem(R.id.ble_scan).setEnabled(false);
124 | } else if(!bluetoothAdapter.isEnabled()) {
125 | menu.findItem(R.id.ble_scan).setEnabled(false);
126 | }
127 | }
128 |
129 | @Override
130 | public void onResume() {
131 | super.onResume();
132 |
133 | getActivity().registerReceiver(discoveryBroadcastReceiver, discoveryIntentFilter);
134 | getActivity().setTitle(R.string.devices_title);
135 |
136 | if(bluetoothAdapter == null) {
137 | setEmptyText("");
138 | } else if(!bluetoothAdapter.isEnabled()) {
139 | setEmptyText("");
140 | if (menu != null) {
141 | listItems.clear();
142 | listAdapter.notifyDataSetChanged();
143 | menu.findItem(R.id.ble_scan).setEnabled(false);
144 | }
145 | } else {
146 | setEmptyText("