Palm

V1.0

🌴 Get alerted on excessive UV radiation with a backpack keychain 🗝

power LiPO
wireless BLE
sensor UV Index
mcu nRF52
ota? No
battery life 5 days
bom items 13
bom cost USD $50.47
vendors 4
status completed in July 2019

Features

  • Read UV Index with the on-board sensor VEML6075 and the battery level
  • Display the UV Index color code with the on-board RGB LED
  • Power on/off the entire device with the on-board power switch
  • Start or stop BLE advertising with the on-board BLE switch
  • Connect via the USB cable to flash the firmware or charge the LiPo
  • Hang it like as keychain using the M6 drill hole
  • Display UV Index and Battery level as a GATT service and characteristic on a BLE client such as the iPhone or Web BLE on Chrome browser

Firmware

Download code
#include <bluefruit.h>
#include "Adafruit_VEML6075.h"

int redPin = PIN_A1;
int greenPin = PIN_A2;
int bluePin = PIN_A3;

const int ENSensorPin = 15;
Adafruit_VEML6075 uv = Adafruit_VEML6075();
uint8_t  uvindexvalue = 0x42;
float  readUVIndexValue = 0.0;

const int ENBLEPin = 16;
bool isBLEON = false;

/* GATT Services https://www.bluetooth.com/specifications/gatt/services/
    Name: Environmental Sensing
    Uniform Type Identifier: org.bluetooth.service.environmental_sensing
    Assigned Number: 0x181A
    Specification: GSS
*/
#define UUID16_SVC_ENVIRONMENTAL_SENSING 0x181A

/* GATT Characteristics https://www.bluetooth.com/specifications/gatt/characteristics/
    Name: UV Index
    Uniform Type Identifier: org.bluetooth.characteristic.uv_index
    Assigned Number: 0x2A76
    Specification: GSS
*/
#define UUID16_CHR_UV_INDEX 0x2A76

BLEService environmental_sensing_service = BLEService(UUID16_SVC_ENVIRONMENTAL_SENSING);
BLECharacteristic uv_index_characteristic = BLECharacteristic(UUID16_CHR_UV_INDEX);

BLEDis bledis;    // DIS (Device Information Service) helper class instance

// Advanced function prototypes
void startAdv(void);
void setupESService(void);
void setupBattService(void);
void connect_callback(uint16_t conn_handle);
void disconnect_callback(uint16_t conn_handle, uint8_t reason);

// Battery
#define VBAT_PIN          (A7)
#define VBAT_MV_PER_LSB   (0.73242188F)
#define VBAT_DIVIDER      (0.71275837F)
#define VBAT_DIVIDER_COMP (1.403F)
int vbat_raw;
uint8_t vbat_per;
float vbat_mv;

/* GATT Services https://www.bluetooth.com/specifications/gatt/services/
    Name: Battery Service
    Uniform Type Identifier: org.bluetooth.service.battery_service
    Assigned Number: 0x180F
    Specification: GSS
*/
#define UUID16_SVC_BATTERY 0x180F

/* GATT Characteristics https://www.bluetooth.com/specifications/gatt/characteristics/
    Name: Battery Level
    Abstract: The current charge level of a battery.
          100% represents fully charged while 0% represents fully discharged.
    Uniform Type Identifier: org.bluetooth.characteristic.battery_level
    Assigned Number: 0x2A19
    Format: uint8
    Specification: GSS
*/
#define UUID16_CHR_BATTERY_LEVEL 0x2A19

BLEService        battery_service = BLEService(UUID16_SVC_BATTERY);
BLECharacteristic battery_level_characteristic = BLECharacteristic(UUID16_CHR_BATTERY_LEVEL);

void setup() {
  Serial.begin(115200);
  delay(500);

  pinMode(ENSensorPin, OUTPUT);
  digitalWrite(ENSensorPin, LOW);
  pinMode(ENBLEPin, INPUT);
  pinMode(LED_RED, OUTPUT);
  Bluefruit.autoConnLed(false);  // Turn off BLUE LED

  // Battery
  analogReference(AR_INTERNAL_3_0);
  analogReadResolution(12);  // Can be 8, 10, 12 or 14

  Serial.println("[email protected] - UV Index sensor");
  Serial.println("-------------------------------------\n");

  // Enable UV sensor
  int returnValue = uv.begin();
  if (!returnValue) {
    Serial.println("[ERROR] Sensor VEML6075 cannot be found");
  }
  Serial.println("[INFO] Sensor VEML6075 enabled");

  // TODO: Turn on/off BLE based on the ENBLEPin
  Bluefruit.begin();
  Bluefruit.setName("[email protected]");

  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  bledis.setManufacturer("Adafruit Industries");
  bledis.setModel("Bluefruit Feather52");
  bledis.begin();;
  setupESService();
  setupBattService();
  startAdv();

  Serial.println("[INFO] Start advertising...");
}

void loop() {
  // Battery
  vbat_raw = analogRead(VBAT_PIN);
  vbat_per = mvToPercent(vbat_raw * VBAT_MV_PER_LSB);
  vbat_mv = (float)vbat_raw * VBAT_MV_PER_LSB * VBAT_DIVIDER_COMP;

  // UV Index
  readUVIndexValue = uv.readUVI();
  uvindexvalue = round(abs(readUVIndexValue));
  displayLED(uvindexvalue);

  // Turn on BLE
  if (digitalRead(ENBLEPin) == 1) {
    isBLEON = true;
    digitalToggle(LED_RED);  // Blinking RED LED indicated BLE is ON
  } else {
    isBLEON = false;
    digitalWrite(LED_RED, LOW);
  }

  if (isBLEON && Bluefruit.connected()) {
    // Note: We use .indicate instead of .write!
    // If it is connected but CCCD is not enabled
    // The characteristic's value is still updated although indicate is not sent
    if (uv_index_characteristic.indicate(&uvindexvalue, sizeof(uvindexvalue))) {
      Serial.print("[INFO] Updated UV Index: ");
      Serial.println(uvindexvalue);
    } else {
      Serial.println("[ERROR] UV Index Indicate not set in the CCCD or not connected");
    }

    if (battery_level_characteristic.indicate(&vbat_per, sizeof(vbat_per))) {
      Serial.print("[INFO] Updated Battery level: ");
      Serial.print(vbat_per);
      Serial.println("%");
    } else {
      Serial.println("[ERROR] Battery level Indicate not set in the CCCD or not connected");
    }
  }

  Serial.print("[INFO] UV Index: ");
  Serial.println(uvindexvalue);
  Serial.print("[INFO] Battery level: ");
  Serial.print(vbat_per);
  Serial.println("%");
  Serial.println("");

  delay(4000);
}

void displayLEDColor(int red, int green, int blue) {
  analogWrite(redPin, red);
  analogWrite(greenPin, green);
  analogWrite(bluePin, blue);
}

void displayLED(uint8_t uvindexvalue) {
  if (uvindexvalue <= 2) {
    // UV Index <= 2, display GREEN or no color alerts
    displayLEDColor(0, 255, 0);
  } else if (uvindexvalue >= 3 && uvindexvalue <= 5) {
    // UV Index is from 3 to 5, display YELLOW
    displayLEDColor(255, 255, 0);
  } else if (uvindexvalue >= 6 && uvindexvalue <= 7) {
    // UV Index is from 6 to 7, display ORANGE
    displayLEDColor(255, 50, 0);
  } else if (uvindexvalue >= 8 && uvindexvalue <= 10) {
    // UV Index is from 8 to 10, display RED
    displayLEDColor(255, 0, 0);
  } else {
    // UV Index is above 11, display PURPLE
    displayLEDColor(255, 0, 255);
  }
}

void startAdv(void) {
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();

  // Include HTM Service UUID
  Bluefruit.Advertising.addService(environmental_sensing_service);
  Bluefruit.Advertising.addService(battery_service);

  // Include Name
  Bluefruit.Advertising.addName();

  /* Start Advertising
   * - Enable auto advertising if disconnected
   * - Interval:  fast mode = 20 ms, slow mode = 152.5 ms
   * - Timeout for fast mode is 30 seconds
   * - Start(timeout) with timeout = 0 will advertise forever (until connected)
   *
   * For recommended advertising interval
   * https://developer.apple.com/library/content/qa/qa1931/_index.html
   */
  Bluefruit.Advertising.restartOnDisconnect(true);

  // in unit of 0.625 ms
  Bluefruit.Advertising.setInterval(32, 244);

  // number of seconds in fast mode
  Bluefruit.Advertising.setFastTimeout(30);

  // 0 = Don't stop advertising after n seconds
  Bluefruit.Advertising.start(0);
}

void setupESService(void) {
  // Configure the Environmental Sensing service
  // See: https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.uv_index.xml
  // Supported Characteristics:
  // Name                         UUID    Requirement Properties
  // ---------------------------- ------  ----------- ----------
  // UV Index                     0x2A76  Mandatory   Read
  environmental_sensing_service.begin();

  // Note: You must call .begin() on the BLEService before calling .begin() on
  // any characteristic(s) within that service definition.. Calling .begin() on
  // a BLECharacteristic will cause it to be added to the last BLEService that
  // was 'begin()'ed!

  // Configure the UV Index characteristic
  // See:https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.uv_index.xml
  // Properties = Indicate
  // Min Len    = 1
  // Max Len    = 1
  // B0         = UINT8 - UV Index measurement unitless
  uv_index_characteristic.setProperties(CHR_PROPS_INDICATE);
  uv_index_characteristic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
  uv_index_characteristic.setFixedLen(1);
  uv_index_characteristic.setCccdWriteCallback(cccd_callback);  // Optionally capture CCCD updates
  uv_index_characteristic.begin();
  uv_index_characteristic.write(&uvindexvalue, sizeof(uvindexvalue));
}

void setupBattService(void) {
  battery_service.begin();

  battery_level_characteristic.setProperties(CHR_PROPS_INDICATE);
  battery_level_characteristic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
  battery_level_characteristic.setFixedLen(1);
  battery_level_characteristic.setCccdWriteCallback(cccd_callback);  // Optionally capture CCCD updates
  battery_level_characteristic.begin();
  battery_level_characteristic.write(&vbat_per, sizeof(vbat_per));
}

void connect_callback(uint16_t conn_handle) {
  // Get the reference to current connection
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));

  Serial.print("[INFO] Connected to ");
  Serial.println(central_name);
}

/**
 * Callback invoked when a connection is dropped
 * @param conn_handle connection where this event happens
 * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
 */
void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
  (void) conn_handle;
  (void) reason;

  Serial.println("[INFO] Disconnected");
  Serial.println("[INFO] Advertising!");
}

void cccd_callback(uint16_t conn_hdl,
  BLECharacteristic* chr, uint16_t cccd_value) {
    // Display the raw request packet
    Serial.print("[INFO] CCCD Updated: ");

    // Serial.printBuffer(request->data, request->len);
    Serial.print(cccd_value);
    Serial.println("");

    // Check the characteristic this CCCD update is associated with in case
    // this handler is used for multiple CCCD records.
    if (chr->uuid == uv_index_characteristic.uuid) {
        if (chr->indicateEnabled(conn_hdl)) {
            Serial.println("[INFO] UV Index Measurement 'Indicate' enabled");
        } else {
            Serial.println("[INFO] UV Index Measurement 'Indicate' disabled");
        }
    } else if (chr->uuid == battery_level_characteristic.uuid) {
        if (chr->indicateEnabled(conn_hdl)) {
            Serial.println("[INFO] Battery level 'Indicate' enabled");
        } else {
            Serial.println("[INFO] Battery level 'Indicate' disabled");
        }
    }
}

uint8_t mvToPercent(float mvolts) {
  uint8_t battery_level;

  if (mvolts >= 3000) {
    battery_level = 100;
  } else if (mvolts > 2900) {
    battery_level = 100 - ((3000 - mvolts) * 58) / 100;
  } else if (mvolts > 2740) {
    battery_level = 42 - ((2900 - mvolts) * 24) / 160;
  } else if (mvolts > 2440) {
    battery_level = 18 - ((2740 - mvolts) * 12) / 300;
  } else if (mvolts > 2100) {
    battery_level = 6 - ((2440 - mvolts) * 6) / 340;
  } else {
    battery_level = 0;
  }

  return battery_level;
}

Makefile

BOARD?=adafruit:nrf52:feather52832
PORT := $(shell ls /dev/tty.SLAB_USBtoUART)
BAUD_RATE = 115200

.PHONY: lint compile upload clean

default: lint compile upload clean

lint:
	cpplint --extensions=ino --filter=-legal/copyright,-readability/todo,-readability/casting,-whitespace/line_length *.ino

compile:
	arduino-cli compile --fqbn $(BOARD) ./

upload:
	adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application ./.*.hex dfu-package.zip
	adafruit-nrfutil dfu serial --package dfu-package.zip -p $(PORT) -b $(BAUD_RATE)

clean:
	rm -f .*.elf
	rm -f .*.hex
	rm -f *.zip

Serial console

Firmware serial console

Test

Download test code
#include <bluefruit.h>
#include "Adafruit_VEML6075.h"

int redPin = PIN_A1;
int greenPin = PIN_A2;
int bluePin = PIN_A3;

const int ENSensorPin = 15;
Adafruit_VEML6075 uv = Adafruit_VEML6075();
uint8_t  uvindexvalue = 0x42;
float  readUVIndexValue = 0.0;

/* GATT Services https://www.bluetooth.com/specifications/gatt/services/
    Name: Environmental Sensing
    Uniform Type Identifier: org.bluetooth.service.environmental_sensing
    Assigned Number: 0x181A
    Specification: GSS
*/
#define UUID16_SVC_ENVIRONMENTAL_SENSING 0x181A

/* GATT Characteristics https://www.bluetooth.com/specifications/gatt/characteristics/
    Name: UV Index
    Uniform Type Identifier: org.bluetooth.characteristic.uv_index
    Assigned Number: 0x2A76
    Specification: GSS
*/
#define UUID16_CHR_UV_INDEX 0x2A76

BLEService        ess = BLEService(UUID16_SVC_ENVIRONMENTAL_SENSING);
BLECharacteristic uvic = BLECharacteristic(UUID16_CHR_UV_INDEX);

BLEDis bledis;    // DIS (Device Information Service) helper class instance

// Advanced function prototypes
void startAdv(void);
void setupESService(void);
void connect_callback(uint16_t conn_handle);
void disconnect_callback(uint16_t conn_handle, uint8_t reason);

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);
  pinMode(ENSensorPin, OUTPUT);
  digitalWrite(ENSensorPin, LOW);

  Serial.println("Starting Palm design verification test");
  Serial.println("-------------------------------------\n");

  Serial.println("Test 1/14: It expects display GREEN LED");
  displayLEDColor(0, 255, 0);
  delay(1000);

  Serial.println("Test 2/14: It expects display YELLOW LED");
  displayLEDColor(255, 255, 0);
  delay(1000);

  Serial.println("Test 3/14: It expects display ORANGE LED");
  displayLEDColor(255, 50, 0);
  delay(1000);

  Serial.println("Test 4/14: It expects display RED LED");
  displayLEDColor(255, 0, 0);
  delay(1000);

  Serial.println("Test 5/14: It expects display PURPLE LED");
  displayLEDColor(255, 0, 255);
  delay(1000);

  Serial.println("Test 6/14: It expects display no color on the LED");
  displayLEDColor(0, 0, 0);
  delay(1000);

  digitalWrite(ENSensorPin, HIGH);
  Serial.println("Test 7/14: It expects the Sensor VEML6075 to be disabled");
  delay(1000);

  digitalWrite(ENSensorPin, LOW);
  delay(100);

  int returnValue = uv.begin();
  if (!returnValue) {
    Serial.println("Test 8/14: It expects to error if sensor is not found");
  }
  Serial.println("Test 9/14: It expects the Sensor VEML6075 to be enabled");

  Serial.println("Test 10/14: It expects to initialise the nRF52 module");
  Bluefruit.begin();
  Bluefruit.setName("[email protected]");

  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  bledis.setManufacturer("Adafruit Industries");
  bledis.setModel("Bluefruit Feather52");
  bledis.begin();;
  setupESService();
  startAdv();

  Serial.println("Test 11/14: It expects to start advertising");
}

void loop() {
  digitalToggle(LED_RED);  // blinking RED LED indicates reading UV sensor

  readUVIndexValue = uv.readUVI();
  uvindexvalue = round(abs(readUVIndexValue));

  Serial.print("Test 12/14: It expects to read UV Index value: ");
  Serial.println(uvindexvalue);
  displayLED(uvindexvalue);

  if (Bluefruit.connected()) {
    // Note: We use .indicate instead of .write!
    // If it is connected but CCCD is not enabled
    // The characteristic's value is still updated although indicate is not sent
    if (uvic.indicate(&uvindexvalue, sizeof(uvindexvalue))) {
      Serial.print("Test 13/14: It expects to update UV Index value: ");
      Serial.println(uvindexvalue);
    } else {
      Serial.println("Test 14/14: It errors when indicate is not set");
    }
  }

  delay(2000);
}

void displayLEDColor(int red, int green, int blue) {
  analogWrite(redPin, red);
  analogWrite(greenPin, green);
  analogWrite(bluePin, blue);
}

void displayLED(uint8_t uvindexvalue) {
  if (uvindexvalue <= 2) {
    // UV Index <= 2, display GREEN or no color alerts
    displayLEDColor(0, 255, 0);
  } else if (uvindexvalue >= 3 && uvindexvalue <= 5) {
    // UV Index is from 3 to 5, display YELLOW
    displayLEDColor(255, 255, 0);
  } else if (uvindexvalue >= 6 && uvindexvalue <= 7) {
    // UV Index is from 6 to 7, display ORANGE
    displayLEDColor(255, 50, 0);
  } else if (uvindexvalue >= 8 && uvindexvalue <= 10) {
    // UV Index is from 8 to 10, display RED
    displayLEDColor(255, 0, 0);
  } else {
    // UV Index is above 11, display PURPLE
    displayLEDColor(255, 0, 255);
  }
}

void startAdv(void) {
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();

  // Include HTM Service UUID
  Bluefruit.Advertising.addService(ess);

  // Include Name
  Bluefruit.Advertising.addName();

  /* Start Advertising
   * - Enable auto advertising if disconnected
   * - Interval:  fast mode = 20 ms, slow mode = 152.5 ms
   * - Timeout for fast mode is 30 seconds
   * - Start(timeout) with timeout = 0 will advertise forever (until connected)
   *
   * For recommended advertising interval
   * https://developer.apple.com/library/content/qa/qa1931/_index.html
   */
  Bluefruit.Advertising.restartOnDisconnect(true);

  // in unit of 0.625 ms
  Bluefruit.Advertising.setInterval(32, 244);

  // number of seconds in fast mode
  Bluefruit.Advertising.setFastTimeout(30);

  // 0 = Don't stop advertising after n seconds
  Bluefruit.Advertising.start(0);
}

void setupESService(void) {
  // Configure the Environmental Sensing service
  // See: https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.uv_index.xml
  // Supported Characteristics:
  // Name                         UUID    Requirement Properties
  // ---------------------------- ------  ----------- ----------
  // UV Index                     0x2A76  Mandatory   Read
  ess.begin();

  // Note: You must call .begin() on the BLEService before calling .begin() on
  // any characteristic(s) within that service definition.. Calling .begin() on
  // a BLECharacteristic will cause it to be added to the last BLEService that
  // was 'begin()'ed!

  // Configure the UV Index characteristic
  // See:https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Characteristics/org.bluetooth.characteristic.uv_index.xml
  // Properties = Indicate
  // Min Len    = 1
  // Max Len    = 1
  // B0         = UINT8 - UV Index measurement unitless
  uvic.setProperties(CHR_PROPS_INDICATE);
  uvic.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
  uvic.setFixedLen(1);
  uvic.setCccdWriteCallback(cccd_callback);  // Optionally capture CCCD updates
  uvic.begin();
  uvic.write(&uvindexvalue, sizeof(uvindexvalue));
}

void connect_callback(uint16_t conn_handle) {
  // Get the reference to current connection
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));

  Serial.print("Connected to ");
  Serial.println(central_name);
}

/**
 * Callback invoked when a connection is dropped
 * @param conn_handle connection where this event happens
 * @param reason is a BLE_HCI_STATUS_CODE which can be found in ble_hci.h
 */
void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
  (void) conn_handle;
  (void) reason;

  Serial.println("Disconnected");
  Serial.println("Advertising!");
}

void cccd_callback(uint16_t conn_hdl,
  BLECharacteristic* chr, uint16_t cccd_value) {
    // Display the raw request packet
    Serial.print("CCCD Updated: ");

    // Serial.printBuffer(request->data, request->len);
    Serial.print(cccd_value);
    Serial.println("");

    // Check the characteristic this CCCD update is associated with in case
    // this handler is used for multiple CCCD records.
    if (chr->uuid == uvic.uuid) {
        if (chr->indicateEnabled(conn_hdl)) {
            Serial.println("UV Index Measurement 'Indicate' enabled");
        } else {
            Serial.println("UV Index Measurement 'Indicate' disabled");
        }
    }
}

Makefile

.PHONY: lint compile upload clean

lint:
	cpplint --extensions=ino --filter=-legal/copyright *.ino

compile:
	arduino-cli compile --fqbn adafruit:nrf52:feather52832 ./

upload:
	adafruit-nrfutil dfu genpkg --dev-type 0x0052 --application ./.*.hex dfu-package.zip
	adafruit-nrfutil dfu serial --package dfu-package.zip -p /dev/tty.SLAB_USBtoUART -b 115200

clean:
	rm -f .*.elf
	rm -f .*.hex
	rm -f *.zip

flash: lint compile upload clean

Serial console

Firmware serial console

iOS Client

Use the nRF Connect iPhone app to view the exact UV Index levels.

iOS detect BLE device
iOS detect GATT services
iOS GATT Characteristics Battery level
iOS GATT Characteristics UV Index

Web BLE

Web BLE on Chrome browser is used to display the UV Index and Battery level on laptop.

Download code View demo
<!doctype html>
<html>
<head>
  <title>Web BLE</title>
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css">
</head>
<body>
<section class="hero is-small is-light">
  <div class="hero-body">
    <div class="container">
      <h2 class="subtitle is-1">Web Bluetooth: Read GATT Values Changing</h2>
    </div>
  </div>
</section>

<section class="section is-small">
  <div class="container">
    <div class="columns is-desktop">
      <div class="column">
        <p class="title is-2">Get UV Index</p>

        <button class="button is-primary" id="read-env">Read UV Index</button>
        <button class="button is-info" id="start-env" disabled>Start</button>
        <button class="button is-info" id="stop-env" disabled>Stop</button>
        <button class="button is-dark" id="reset-env">Reset device</button>

        <p class="subtitle is-3 is-spaced" id="env-value"></p>
      </div>
      <div class="column">
        <p class="title is-2">Get Battery level</p>

        <button class="button is-primary" id="read-batt">Read Battery level</button>
        <button class="button is-info" id="start-batt" disabled>Start</button>
        <button class="button is-info" id="stop-batt" disabled>Stop</button>
        <button class="button is-dark" id="reset-batt">Reset device</button>

        <p class="subtitle is-3 is-spaced" id="batt-value"></p>
        <progress class="progress is-primary" value="0" max="100" id="batt-bar"></progress>
      </div>
    </div>
  </div>
</section>

<section class="section is-small" id="uv-index"> </section>

<script>
  var bluetoothDevice;
  var gattCharacteristic;
  var currentGatt;
  var gatt = {
    env: {
      serviceName: "UV Index",
      shortName: "env",
      service: "environmental_sensing",
      characteristic: "uv_index",
      unit: ""
    },
    batt: {
      serviceName: "Battery level",
      shortName: "batt",
      service: "battery_service",
      characteristic: "battery_level",
      unit: "%"
    }
  }

  // User actions read, start, stop, reset
  document.querySelector('#read-env').addEventListener('click', function() {
    if (isWebBluetoothEnabled()) { currentGatt = 'env'; read() }
  })

  document.querySelector('#start-env').addEventListener('click', function(event) {
    if (isWebBluetoothEnabled()) { start() }
  })

  document.querySelector('#stop-env').addEventListener('click', function(event) {
    if (isWebBluetoothEnabled()) { stop() }
  })

  document.querySelector('#reset-env').addEventListener('click', function(event) {
    if (isWebBluetoothEnabled()) { currentGatt = ''; reset() }
  })

  document.querySelector('#read-batt').addEventListener('click', function() {
    if (isWebBluetoothEnabled()) { currentGatt = 'batt'; read() }
  })

  document.querySelector('#start-batt').addEventListener('click', function(event) {
    if (isWebBluetoothEnabled()) { start() }
  })

  document.querySelector('#stop-batt').addEventListener('click', function(event) {
    if (isWebBluetoothEnabled()) { stop() }
  })

  document.querySelector('#reset-batt').addEventListener('click', function(event) {
    if (isWebBluetoothEnabled()) { currentGatt = ''; reset() }
  })

  function isWebBluetoothEnabled() {
    if (navigator.bluetooth) {
      return true;
    } else {
      console.log('Web Bluetooth API is not available.')
      console.log('Please make sure the "Experimental Web Platform features" flag is enabled.')
      return false
    }
  }

  function read() {
    return (bluetoothDevice ? Promise.resolve() : requestDevice())
    .then(connectGATT)
    .then(_ => {
      console.log('Reading ' + gatt[currentGatt].serviceName + '...');
      return gattCharacteristic.readValue();
    })
    .catch(error => {
      console.log('[ERROR] Read ' + gatt[currentGatt].serviceName + ' on click: ' + error);
    });
  }

  function start() {
    console.log('Starting Notifications...');
    gattCharacteristic.startNotifications()
    .then(_ => {
      console.log('> Notifications started');
      document.querySelector('#start-' + gatt[currentGatt].shortName).disabled = true;
      document.querySelector('#stop-' + gatt[currentGatt].shortName).disabled = false;
    })
    .catch(error => {
      console.log('[ERROR] Start notifications: ' + error);
    });
  }

  function stop() {
    console.log('Stopping Notifications...');
    gattCharacteristic.stopNotifications()
    .then(_ => {
      console.log('> Notifications stopped');
      document.querySelector('#start-' + gatt[currentGatt].shortName).disabled = false;
      document.querySelector('#stop-' + gatt[currentGatt].shortName).disabled = true;
    })
    .catch(error => {
      console.log('[ERROR] Stop notifications: ' + error);
    });
  }

  function reset() {
    if (gattCharacteristic) {
      gattCharacteristic.removeEventListener('characteristicvaluechanged',
          handleChangedValue);
      gattCharacteristic = null;
    }

    bluetoothDevice = null;
    document.querySelector('#start-batt').disabled = true;
    document.querySelector('#stop-batt').disabled = true;
    document.querySelector('#start-env').disabled = true;
    document.querySelector('#stop-env').disabled = true;
    document.getElementById("uv-index").style.background = "";

    console.log('> Bluetooth Device reset');
  }

  function connectGATT() {
    if (bluetoothDevice.gatt.connected && gattCharacteristic) {
      return Promise.resolve();
    }

    console.log('Connecting to GATT Server...');
    return bluetoothDevice.gatt.connect()
    .then(server => {
      console.log('Getting GATT Service...');
      return server.getPrimaryService(gatt[currentGatt].service);
    })
    .then(service => {
      console.log('Getting GATT Characteristic...');
      return service.getCharacteristic(gatt[currentGatt].characteristic);
    })
    .then(characteristic => {
      gattCharacteristic = characteristic;
      gattCharacteristic.addEventListener('characteristicvaluechanged',
          handleChangedValue);
      document.querySelector('#start-' + gatt[currentGatt].shortName).disabled = false;
      document.querySelector('#stop-' + gatt[currentGatt].shortName).disabled = true;
    });
  }

  function requestDevice() {
    console.log('Requesting any Bluetooth Device...');
    return navigator.bluetooth.requestDevice({
      filters: [ { "namePrefix": "Palm" } ],
      optionalServices: [gatt[currentGatt].service]})
    .then(device => {
      bluetoothDevice = device;
      bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected);
    });
  }

  function handleChangedValue(event) {
    let value = event.target.value.getUint8(0);
    var now = new Date()
    console.log('> ' + now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds() + ' ' + gatt[currentGatt].serviceName + ' is ' + value + gatt[currentGatt].unit);

    document.getElementById(gatt[currentGatt].shortName + '-value').innerText = gatt[currentGatt].serviceName + ' is ' + value + gatt[currentGatt].unit

    if (currentGatt == 'env') { changeBackground(value) }
    if (currentGatt == 'batt') { changeProgressBar(value) }
  }

  function onDisconnected() {
    console.log('> Bluetooth Device disconnected');
    connectGATT()
    connectGATTBatt()
    .catch(error => {
      console.log('[ERROR] on disconnected:  ' + error);
    })
  }

  function changeBackground(value) {
    var uvIndexStyle = document.getElementById("uv-index").style

    if (value <= 2) {
      uvIndexStyle.background = "yellowgreen"
    } else if (value <= 5) {
      uvIndexStyle.background = "lemonchiffon"
    } else if (value <= 7) {
      uvIndexStyle.background = "tomato"
    } else if (value <= 10) {
      uvIndexStyle.background = "indianred"
    } else {
      uvIndexStyle.background = "plum"
    }
  }

  function changeProgressBar(value) {
    document.getElementById("batt-bar").value = value
  }
</script>

</body>
</html>