Skip to content

Instantly share code, notes, and snippets.

@ntoto
Created October 7, 2017 21:30
Show Gist options
  • Save ntoto/47904a141546473f418cb6e78a3ab6c7 to your computer and use it in GitHub Desktop.
Save ntoto/47904a141546473f418cb6e78a3ab6c7 to your computer and use it in GitHub Desktop.
package org.toto.lmlm;
import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.ParcelUuid;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@TargetApi(21)
public class MainScreen extends AppCompatActivity {
private BluetoothAdapter mBluetoothAdapter;
private int REQUEST_ENABLE_BT = 1;
private Handler mHandler;
private static final long SCAN_PERIOD = 10000;
private BluetoothLeScanner mLEScanner;
private ScanSettings settings;
private List<ScanFilter> filters;
private BluetoothGatt mGatt;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Scale scale;
private String SCALE_SERVICE_UUID = "00001820-0000-1000-8000-00805f9b34fb";
private String SCALE_CHARACTERISTIC_UUID = "00002a80-0000-1000-8000-00805f9b34fb";
private static final int STATE_HEADER = 0;
private static final int STATE_DATA = 1;
final private byte HEADER1 = (byte)0xef;
final private byte HEADER2 = (byte)0xdd;
private static final int MSG_SYSTEM = 0;
private static final int MSG_TARE = 4;
private static final int MSG_INFO = 7;
private static final int MSG_STATUS = 8;
private static final int MSG_IDENTIFY = 11;
private static final int MSG_EVENT = 12;
private static final int MSG_TIMER = 13;
private static final int EVENT_WEIGHT = 5;
private static final int EVENT_BATTERY = 6;
private static final int EVENT_TIMER = 7;
private static final int EVENT_KEY = 8;
private static final int EVENT_ACK = 11;
private static final int EVENT_WEIGHT_LEN = 6;
private static final int EVENT_BATTERY_LEN = 1;
private static final int EVENT_TIMER_LEN = 3;
private static final int EVENT_KEY_LEN = 1;
private static final int EVENT_ACK_LEN = 2;
private static final int TIMER_START = 0;
private static final int TIMER_STOP = 1;
private static final int TIMER_PAUSE = 2;
private static final int TIMER_STATE_STOPPED = 0;
private static final int TIMER_STATE_STARTED = 1;
private static final int TIMER_STATE_PAUSED = 2;
private class Scale {
private int state;
private byte msgType;
private int battery;
private boolean notificationInfoSent;
private boolean ready;
private long lastHeartbeat;
private float weight;
private boolean weightHasChanged;
private int minutes;
private int seconds;
private int mseconds;
private void sendMessage(int type, byte[] payload) {
int cksum1 = 0;
int cksum2 = 0;
byte[] bytes = new byte[payload.length + 5];
bytes[0] = HEADER1;
bytes[1] = HEADER2;
bytes[2] = (byte) type;
for (int i = 0; i < payload.length; i++) {
bytes[i + 3] = payload[i];
if (i % 2 == 0) {
cksum1 = (cksum1 + payload[i]) & 0xFF;
} else {
cksum2 = (cksum2 + payload[i]) & 0xFF;
}
}
bytes[payload.length + 3] = (byte) cksum1;
bytes[payload.length + 4] = (byte) cksum2;
BluetoothGattCharacteristic characteristic = mGatt.getService(UUID.fromString(SCALE_SERVICE_UUID)).getCharacteristic(UUID.fromString(SCALE_CHARACTERISTIC_UUID));
characteristic.setValue(bytes);
mGatt.writeCharacteristic(characteristic);
}
void sendEvent(byte[] payload) {
byte[] bytes = new byte[payload.length + 1];
bytes[0] = (byte)(payload.length + 1);
for (int i = 0; i < payload.length; i++) {
bytes[i+1] = (byte)(payload[i] & 0xff);
}
sendMessage(MSG_EVENT, bytes);
}
void sendHeartbeat() {
long now = System.currentTimeMillis();
if (lastHeartbeat + 3000 > now) {
return;
}
byte[] payload = {0x02,0x00};
sendMessage(MSG_SYSTEM, payload);
lastHeartbeat = now;
}
void sendTare() {
byte[] payload = {0x00};
sendMessage(MSG_TARE, payload);
}
void sendTimerCommand(int command) {
byte[] payload = {0x00, (byte)command};
sendMessage(MSG_TIMER, payload);
}
void sendId() {
byte[] payload = {0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d,0x2d};
sendMessage(MSG_IDENTIFY, payload);
}
void sendNotificationRequest() {
byte[] payload = {
0, // weight
1, // weight argument
1, // battery
2, // battery argument
2, // timer
5, // timer argument
3, // key
4 // setting
};
sendEvent(payload);
ready = true;
notificationInfoSent = true;
}
void dump(String msg, byte[] payload) {
StringBuilder sb = new StringBuilder();
for (byte b : payload) {
sb.append(String.format("%02X ", b));
}
Log.e("scaleDump", msg + sb.toString());
}
int parseWeightEvent(byte[] payload) {
if (payload.length < EVENT_WEIGHT_LEN) {
dump("Invalid weight event: ", payload);
return -1;
}
float value = (((payload[1] & 0xff) << 8) + (payload[0] & 0xff));
int unit = payload[4] & 0xFF;
if (unit == 1) {
value /= 10;
}
else if (unit == 2) {
value /= 100;
}
else if (unit == 3) {
value /= 1000;
}
else if (unit == 4) {
value /= 10000;
}
if ((payload[5] & 0x02) == 0x02) {
value *= -1;
}
weight = value;
weightHasChanged = true;
return EVENT_WEIGHT_LEN;
}
int parseAckEvent(byte[] payload) {
if (payload.length < EVENT_ACK_LEN) {
dump("Invalid ack event: ", payload);
return -1;
}
// ignore ack
return EVENT_ACK_LEN;
}
int parseKeyEvent(byte[] payload) {
if (payload.length < EVENT_KEY_LEN) {
dump("Invalid ack event length: ", payload);
return -1;
}
// ignore key event
return EVENT_KEY_LEN;
}
int parseBatteryEvent(byte[] payload) {
if (payload.length < EVENT_BATTERY_LEN) {
dump("Invalid battery event length: ", payload);
return -1;
}
battery = payload[0];
return EVENT_BATTERY_LEN;
}
int parseTimerEvent(byte[] payload) {
if (payload.length < EVENT_TIMER_LEN) {
dump("Invalid timer event length: ", payload);
return -1;
}
minutes = payload[0];
seconds = payload[1];
mseconds = payload[2];
return EVENT_TIMER_LEN;
}
// returns last position in payload
int parseScaleEvent(byte[] payload) {
int event = payload[0];
int val;
byte[] bytes = Arrays.copyOfRange(payload, 1, payload.length);
switch(event) {
case EVENT_WEIGHT:
val = parseWeightEvent(bytes);
break;
case EVENT_BATTERY:
val = parseBatteryEvent(bytes);
break;
case EVENT_TIMER:
val = parseTimerEvent(bytes);
break;
case EVENT_ACK:
val = parseAckEvent(bytes);
break;
case EVENT_KEY:
val = parseKeyEvent(bytes);
break;
default:
dump("Unknown event: ", payload);
return -1;
}
if (val < 0) {
return -1;
}
return val + 1;
}
int parseScaleEvents(byte[] payload) {
int lastPos = 0;
while (lastPos < payload.length) {
byte[] bytes = Arrays.copyOfRange(payload, lastPos, payload.length);
int pos = parseScaleEvent(bytes);
if (pos < 0) {
return -1;
}
lastPos += pos;
}
return 0;
}
int parseInfo(byte[] payload) {
battery = payload[4];
// TODO parse other infos
return 0;
}
private int parseScaleData(byte[] data) {
int ret = 0;
switch(msgType) {
case MSG_INFO:
ret = parseInfo(data);
sendId();
break;
case MSG_STATUS:
if (!notificationInfoSent) {
sendNotificationRequest();
}
break;
case MSG_EVENT:
ret = parseScaleEvents(data);
break;
default:
break;
}
return ret;
}
private void processData(byte[] data) {
if (state == STATE_HEADER) {
if (data.length != 3) {
Log.e("Invalid header length", data.toString());
return;
}
if (data[0] != HEADER1 || data[1] != HEADER2) {
Log.e("Invalid header: ", data.toString());
return;
}
state = STATE_DATA;
msgType = data[2];
} else {
int len;
int offset = 0;
if (msgType == MSG_STATUS || msgType == MSG_EVENT || msgType == MSG_INFO) {
len = data[0];
if (len == 0) {
len = 1;
}
offset = 1;
} else {
switch (msgType) {
case 0:
len = 2;
break;
default:
len = 0;
}
}
if (data.length < len + 2) {
Log.e("Invalid data length", data.toString());
}
parseScaleData(Arrays.copyOfRange(data, offset, offset + len));
state = STATE_HEADER;
}
}
public float getWeight() {
return weight;
}
public int getBattery() {
return battery;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
scale = new Scale();
setContentView(R.layout.activity_main_screen);
mHandler = new Handler();
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "BLE Not Supported", Toast.LENGTH_SHORT).show();
finish();
}
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
}
@Override
protected void onResume() {
super.onResume();
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
else {
if (Build.VERSION.SDK_INT >= 21) {
mLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
filters = new ArrayList<>();
filters.add(
new ScanFilter
.Builder()
.setServiceUuid(ParcelUuid.fromString(SCALE_SERVICE_UUID))
.build()
);
}
scanLeDevice(true);
}
}
@Override
protected void onPause() {
super.onPause();
if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) {
scanLeDevice(false);
}
}
@Override
protected void onDestroy() {
if (mGatt == null) {
return;
}
mGatt.close();
mGatt = null;
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == Activity.RESULT_CANCELED) {
//Bluetooth not enabled.
finish();
return;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private void scanLeDevice(final boolean enable) {
if (enable) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT < 21) {
mBluetoothAdapter.stopLeScan(mLeScanCallback);
} else {
mLEScanner.stopScan(mScanCallback);
}
}
}, SCAN_PERIOD);
if (Build.VERSION.SDK_INT < 21) {
mBluetoothAdapter.startLeScan(mLeScanCallback);
} else {
mLEScanner.startScan(filters, settings, mScanCallback);
}
}
else {
if (Build.VERSION.SDK_INT < 21) {
mBluetoothAdapter.stopLeScan(mLeScanCallback);
} else {
mLEScanner.stopScan(mScanCallback);
}
}
}
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
Log.i("callbackType", String.valueOf(callbackType));
Log.i("result", result.toString());
BluetoothDevice btDevice = result.getDevice();
connectToDevice(btDevice);
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult sr : results) {
Log.i("ScanResult - Results", sr.toString());
}
}
@Override
public void onScanFailed(int errorCode) {
Log.e("Scan Failed", "Error Code: " + errorCode);
}
};
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi,
byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.i("onLeScan", device.toString());
connectToDevice(device);
}
});
}
};
public void connectToDevice(BluetoothDevice device) {
if (mGatt == null) {
mGatt = device.connectGatt(this, false, gattCallback);
// will stop after first device detection
scanLeDevice(false);
}
}
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
Log.i("onConnectionStateChange", "Status: " + status);
switch (newState) {
case BluetoothProfile.STATE_CONNECTED:
Log.i("gattCallback", "STATE_CONNECTED");
gatt.discoverServices();
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.e("gattCallback", "STATE_DISCONNECTED");
break;
default:
Log.e("gattCallback", "STATE_OTHER");
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
BluetoothGattService service = gatt.getService(UUID.fromString(SCALE_SERVICE_UUID));
Log.i("onServicesDiscovered", service.toString());
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(SCALE_CHARACTERISTIC_UUID));
gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptors().get(0);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
// Run heartbeat every 3 seconds to keep connection alive
scheduler.scheduleAtFixedRate(scale::sendHeartbeat, 2, 3, TimeUnit.SECONDS);
}
@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
//StringBuilder sb = new StringBuilder();
//for (byte b : characteristic.getValue()) {
// sb.append(String.format("%02X ", b));
//}
//Log.i("onCharacteristicRead", sb.toString());
scale.processData(characteristic.getValue());
Log.i("onCharacteristicRead", "battery: " + scale.getBattery() + ", weight: " + String.valueOf(scale.getWeight()));
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
Log.i("onCharacteristicRead", characteristic.toString());
gatt.disconnect();
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment