Created
October 7, 2017 21:30
-
-
Save ntoto/47904a141546473f418cb6e78a3ab6c7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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