DIY Smartwatch Walkthrough

Note: The Android app code for this section of the tutorial is available here: https://github.com/calebh/cwatch/tree/clockonly/android/CWatch

The built APK for this app can be downloaded in Github releases here: https://github.com/calebh/cwatch/releases

Android App

Image of watch displaying time and date.

Screenshot of the Android app used to synchronize time with the CLUE board.

In this tutorial we will be creating a basic Android application for sending the date and time to the CLUE board/DIY smartwatch. The app will consist of scan button, which when pressed scans for nearby BLE devices that are advertising the existence. A list of devices will appear on the screen, which the user can scroll through. When the CLUE board is on the "CWatch" name should appear in the list of devices. The user then taps the name to synchronize the date/time and day of week with the CLUE board.

Since Android apps are very complex and require quite a bit of boilerplate code, we will focus on the core functionality for writing to the BLE characteristics. To view the full implementation, see the MainActivity.java file.

public class MainActivity extends AppCompatActivity implements MyRecyclerViewAdapter.ItemClickListener {
    // ...

    private final UUID rawTimeServiceUuid = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb");
    private final UUID dayDateTimeChrUuid = UUID.fromString("00002A0A-0000-1000-8000-00805f9b34fb");
    private final UUID dayOfWeekChrUuid = UUID.fromString("00002A09-0000-1000-8000-00805f9b34fb");

    private BluetoothGatt remoteGatt;

    private void writeCharacteristic(BluetoothGattCharacteristic characteristic, byte[] payload) {
        characteristic.setValue(payload);
        if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == BluetoothGattCharacteristic.PROPERTY_WRITE) {
            characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
        } else if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) {
            characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
        }
        remoteGatt.writeCharacteristic(characteristic);
    }

    private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (characteristic.getUuid().equals(dayDateTimeChrUuid)) {
                    // We have succeeded in writing the day/date/time characteristic, now
                    // write the day of week
                    ByteBuffer dayOfWeek = ByteBuffer.allocate(1);
                    Calendar calendar = Calendar.getInstance();
                    dayOfWeek.put((byte) (calendar.get(Calendar.DAY_OF_WEEK) - 1));
                    BluetoothGattCharacteristic dayOfWeekChr = gatt.getService(rawTimeServiceUuid).getCharacteristic(dayOfWeekChrUuid);
                    writeCharacteristic(dayOfWeekChr, dayOfWeek.array());
                }
                Log.i("BluetoothGattCallback", "Wrote to characteristic $uuid | value: ${value.toHexString()}");
            } else if (status == BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH) {
                Log.e("BluetoothGattCallback", "Write exceeded connection ATT MTU!");
            } else if (status == BluetoothGatt.GATT_WRITE_NOT_PERMITTED) {
                Log.e("BluetoothGattCallback", "Write not permitted for $uuid!");
            } else {
                Log.e("BluetoothGattCallback", "Characteristic write failed for $uuid, error: $status");
            }
        }

        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);

            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    remoteGatt = gatt;
                    gatt.discoverServices();
                } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                    gatt.close();
                }
            } else {
                gatt.close();
            }
        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);

            // First write the day date time
            // Once this has succeeded, we then write the day of the week
            // in the onCharacteristicWrite callback.
            // Android BLE is not good at queueing multiple writes, so
            // we have to do the next write only after the first write has
            // succeeded
            BluetoothGattCharacteristic dayDateTimeChr = gatt.getService(rawTimeServiceUuid).getCharacteristic(dayDateTimeChrUuid);
            ByteBuffer dayDateTime = ByteBuffer.allocate(9);
            Calendar calendar = Calendar.getInstance();
            dayDateTime.put((byte) (calendar.get(Calendar.MONTH)));
            dayDateTime.put((byte) (calendar.get(Calendar.DAY_OF_MONTH)));
            dayDateTime.putInt((int) (calendar.get(Calendar.YEAR)));
            dayDateTime.put((byte) (calendar.get(Calendar.HOUR_OF_DAY)));
            dayDateTime.put((byte) (calendar.get(Calendar.MINUTE)));
            dayDateTime.put((byte) (calendar.get(Calendar.SECOND)));
            writeCharacteristic(dayDateTimeChr, dayDateTime.array());
        }
    };

    @Override
    public void onItemClick(View view, int position) {
        BluetoothDevice device = mLeDevices.get(position);
        if (device.getName().equals("CWatch")) {
            device.connectGatt(getApplicationContext(), false, gattCallback);
        }
    }

    // ...
}

The core logic is quite simple and consists of basic binary writes. When the user taps on a BLE device in the list, the onItemClick method is called. We check if this name is "CWatch" and if so connect to it and attach our callback handler. When this handler has its onServicesDiscovered callback called, we write the day/date/time to the correct characteristic. At some point this write completes, and the onCharacteristicWrite handler is called. When this is called we start the second characteristic write for the day of the week. The order and size of the buffers that we write to the characteristics are identical to what is given in the packed struct in the Juniper program. The UUIDs are those given in the BLE specification and are identical to what is used on the CLUE board.