Sitemap / Advertise

Introduction

Log UV & weather data on an SD card to train an Edge Impulse model. Then, run it to get informed of sun damage over BLE via an Android app.


Tags

Share

BLE AI-driven Smartwatch Detecting Potential Sun Damage w/ Edge Impulse

Advertisement:


read_later

Read Later



read_later

Read Later

Introduction

Log UV & weather data on an SD card to train an Edge Impulse model. Then, run it to get informed of sun damage over BLE via an Android app.

Tags

Share





Advertisement

Advertisement




    Components :
  • [1]XIAO BLE nRF52840
  • [1]XIAO Expansion Board
  • [1]Grove - UV Sensor
  • [1]BMP180 Precision Sensor
  • [1]Keyes 10mm RGB LED Module (140C05)
  • [1]Creality CR-6 SE 3D Printer
  • [1]MicroSD Card
  • [1]3.7V LiPo Battery
  • [1]15mm Wide Yellow Velcro
  • [4]10mm M3 Male-Female Brass Hex Spacer Standoff
  • [4]M3 Screw and Hex Nut
  • [1]Jumper Wires

Description

Although many of us enjoy a sunny day in our leisure time, the glaring sun can affect our health detrimentally, especially for the elderly, children, and people with light skin. Excess sun exposure can cause minor health conditions such as sunburn, dehydration, hyponatremia, heatstroke, etc. In more severe cases, excess sun exposure can engender photoaging, DNA damage, skin cancer, immunosuppression, and eye damage, such as cataracts[1]. Therefore, it is crucial to detect sun damage risk levels so as to get prescient warnings regarding potential health risks to mitigate the brunt of inadvertent excess sun exposure.

The sun emits energy over a broad spectrum of wavelengths: visible light, infrared radiation generating heat, and UV (ultraviolet) radiation hidden from our senses. Although UV radiation affects our health positively in moderation, such as instigating the production of vitamin D, sun damage is mostly engendered by overexposure to UV radiation since it has a higher frequency and lower wavelength than visible light.

The UV region covers the wavelength range of 100-400 nm and is divided into three bands:

After perusing recent research papers on UV radiation, I decided to utilize UV index (UVI), temperature, pressure, and altitude measurements denoting the amount of UV radiation from the sun so as to create a budget-friendly BLE smartwatch to forecast sun damage risk levels in the hope of prewarning the user, especially risk groups, of potential sun damage risk to avert severe health conditions related to excess sun exposure, such as immune system damage and melanoma (skin cancer).

The ultraviolet index (UV index) is an international standard measurement of the strength of the sunburn-producing UV (ultraviolet) radiation. In other words, the UV index forecasts the strength of the sun’s harmful rays. Since it is designed as an open-ended linear scale, directly proportional to the intensity of UV radiation, the higher the number, the greater the chance of sun damage. An increase in the UV index value corresponds to a constant decrease in time to sunburn. Therefore, higher values represent a greater risk of sunburn and excess UV radiation exposure.

Even though UV index, temperature, pressure, and altitude measurements provide insight into detecting sun damage, it is not possible to extrapolate and construe sun damage risk levels precisely by merely employing limited data without applying complex algorithms since sun damage risk levels fluctuate according to various phenomena, some of which are not fully fathomed yet. Hence, I decided to build and train an artificial neural network model by utilizing the empirically assigned sun damage risk classes to forecast sun damage risk levels based on UV index, temperature, pressure, and altitude measurements.

Since XIAO BLE (nRF52840) is an ultra-small size Bluetooth development board that can easily collect data and run my neural netwrok model after being trained to forecast sun damage risk levels, I decided to employ XIAO BLE in this wearable smartwatch project. To obtain the required measurements to train my model, I utilized a UV sensor (Grove) and a BMP180 precision sensor. Since the XIAO expansion board provides various prototyping options and built-in peripherals such as an SSD1306 OLED display and a MicroSD card module, I use the expansion board to make rigid connections between XIAO BLE and the sensors.

Since the expansion board supports reading and writing information from/to files on an SD card, I stored the collected data in a CSV file on the SD card to create a data set. In this regard, I was able to save data packets via XIAO BLE without requiring any additional procedures.

After completing my data set, I built my artificial neural network model (ANN) with Edge Impulse to make predictions on sun damage risk levels (classes) based on UV index, temperature, pressure, and altitude measurements. Since Edge Impulse is nearly compatible with all microcontrollers and development boards, I had not encountered any issues while uploading and running my model on XIAO BLE. As labels, I employed the empirically assigned sun damage risk classes for each data record while collecting data outdoors:

After training and testing my neural network model, I deployed and uploaded the model on XIAO BLE. Therefore, the smartwatch is capable of detecting precise sun damage risk levels (classes) by running the model independently. Also, after running the model successfully, I employed XIAO BLE to transmit (advertise) the prediction (detection) result and the recently collected data over BLE.

Then, I developed a complementing Android application from scratch to obtain the transmitted (advertised) information over BLE from the smartwatch so as to inform the user of potential sun damage risk.

Lastly, to make the smartwatch as sturdy and robust as possible while enduring harsh conditions outdoors, I designed an Ultimatrix-inspired smartwatch case with a sliding (removable) top cover (3D printable).

So, this is my project in a nutshell 😃

In the following steps, you can find more detailed information on coding, logging data on the SD card, transmitting data packets over BLE, building a neural network model with Edge Impulse, and running it on XIAO BLE.

🎁🎨 If you want to replicate or modify this project, you can get XIAO BLE with free shipping worldwide as of now. Click here for more information.

project-image

🎁🎨 Huge thanks to Seeed Studio for sponsoring these products:

⭐ XIAO BLE nRF52840 | Inspect

⭐ XIAO Expansion Board | Inspect

⭐ Grove - UV Sensor | Inspect

🎁🎨 Also, huge thanks to Creality3D for sponsoring a Creality CR-6 SE 3D Printer.

🎁🎨 If you want to purchase some products from Creality3D, you can use my 10% discount coupon (Aktar10) even for their new and most popular printers: CR-10 Smart, CR-30 3DPrintMill, Ender-3 Pro, and Ender-3 V2. You can also use the coupon for Creality filaments.

project-image
Figure - 78.1


project-image
Figure - 78.2


project-image
Figure - 78.3


project-image
Figure - 78.4


project-image
Figure - 78.5


project-image
Figure - 78.6


project-image
Figure - 78.7


project-image
Figure - 78.8

Step 1: Designing and printing an Ultimatrix-inspired smartwatch case

Since I am a huge Ben 10 fan, I got inspired by the Ultimatrix in Ben 10: Ultimate Alien animated series to design a smartwatch case so as to create a robust and sturdy device flawlessly operating while enduring harsh conditions outdoors. To make the XIAO expansion board accessible while logging the collected data on the SD card, I added a sliding (removable) top cover. Also, I inscribed the well-known Omnitrix symbol on the top cover to emphasize the Ben 10 theme gloriously :)

project-image
Figure - 78.9

I designed the smartwatch case and its sliding (removable) top cover in Autodesk Fusion 360. You can download their STL files below.

project-image
Figure - 78.10


project-image
Figure - 78.11


project-image
Figure - 78.12


project-image
Figure - 78.13


project-image
Figure - 78.14

Then, I sliced 3D models (STL files) in Ultimaker Cura.

project-image
Figure - 78.15


project-image
Figure - 78.16

Since I wanted to create a solid structure for the smartwatch case with the sliding top cover and complement the Ben 10 theme with a unique Ultimatrix iteration, I utilized this PLA filament:

Finally, I printed all parts (models) with my Creality CR-6 SE 3D Printer. Although I am a novice in 3D printing, and it is my first FDM 3D printer, I got incredible results effortlessly with the CR-6 SE :)

project-image
Figure - 78.17

Step 1.1: Assembling the smartwatch and making connections & adjustments


// Connections
// XIAO BLE :  
//                                Grove - UV Sensor
// A0  --------------------------- SIG
//                                BMP180 Barometric Pressure/Temperature/Altitude Sensor
// A4  --------------------------- SDA
// A5  --------------------------- SCL
//                                SSD1306 OLED Display (128x64)
// A4  --------------------------- SDA
// A5  --------------------------- SCL
//                                MicroSD Card Module (Built-in on the XIAO Expansion board)
// D10 --------------------------- MOSI
// D9  --------------------------- MISO
// D8  --------------------------- CLK (SCK)
// D2  --------------------------- CS  
//                                Button (Built-in on the XIAO Expansion board)
// D1  --------------------------- +
//                                Keyes 10mm RGB LED Module (140C05)
// D7  --------------------------- R
// D3  --------------------------- G
// D6  --------------------------- B  

First of all, I soldered female pin headers to XIAO BLE in order to connect it to the XIAO expansion board. Also, I changed my 3.7V LiPo battery's pre-attached connector with a JST 2.0 standard connector so as to power the expansion board.

project-image
Figure - 78.18


project-image
Figure - 78.19

To collect the UV radiation and weather data, I connected the UV sensor (Grove) and the BMP180 precision sensor to XIAO BLE via the expansion board. Since the expansion board has a Grove port supporting analog sensors, I connected the UV sensor via a Grove cable.

To display and log the collected data, I utilized the built-in SSD1306 OLED screen, MicroSD card module, and button on the expansion board. Also, I added a 10mm common anode RGB LED module (Keyes) to indicate the outcomes of operating functions.

project-image
Figure - 78.20

After printing all parts (models), I fastened all components except the expansion board to their corresponding slots on the smartwatch case and the sliding (removable) top cover via a hot glue gun.

To attach the expansion board to the smartwatch case, I utilized 10mm M3 male-female brass hex spacers, M3 screws, and hex nuts.

project-image
Figure - 78.21


project-image
Figure - 78.22


project-image
Figure - 78.23


project-image
Figure - 78.24


project-image
Figure - 78.25


project-image
Figure - 78.26


project-image
Figure - 78.27


project-image
Figure - 78.28

Finally, I affixed yellow rubber hook-and-loop fasteners (15mm wide Velcro) to the holes under the smartwatch case in order to wear the smartwatch effortlessly outdoors.

project-image
Figure - 78.29


project-image
Figure - 78.30


project-image
Figure - 78.31

Step 2: Developing a BLE-enabled Android application w/ the MIT APP Inventor

To be able to obtain the transmitted (advertised) information from the smartwatch over BLE, I decided to develop an Android application from scratch with the MIT APP Inventor.

MIT App Inventor is an intuitive, visual programming environment that allows developers to build fully functional Android applications. Its blocks-based tool (drag-and-drop) facilitates the creation of complex, high-impact apps in significantly less time than the traditional programming environments.

After developing my application, named BLE UV Smartwatch, I published it on Google Play. So, you can install the BLE UV Smartwatch app on any compatible Android device via Google Play.

📲 Install BLE UV Smartwatch on Google Play

Nevertheless, if you want to replicate the BLE UV Smartwatch app on the MIT App Inventor, follow the steps below.

#️⃣ First of all, create an account on the MIT App Inventor.

#️⃣ Download the BLE UV Smartwatch app's project file in the aia format (BLE_UV_Smartwatch.aia) and import the aia file into the MIT App Inventor.

project-image
Figure - 78.32

#️⃣ Since the MIT App Inventor does not support BLE connectivity by default, download the latest version of the BluetoothLE extension and import the BluetoothLE extension into the BLE UV Smartwatch project.

In this tutorial, you can get more information regarding enabling BLE connectivity on the MIT App Inventor.

project-image
Figure - 78.33

#️⃣ Inspect the BLE UV Smartwatch project functions and source code in the Blocks editor.

project-image
Figure - 78.34

#️⃣ After installing the BLE UV Smartwatch app on a compatible Android device, the app starts displaying the transmitted (advertised) information from the smartwatch over BLE immediately to inform the user of potential sun damage risk.

You can get more information regarding the BLE UV Smartwatch app's features in Step 8.

project-image
Figure - 78.35

Step 3: Setting up XIAO BLE on the Arduino IDE

Since the XIAO expansion board supports reading and writing information from/to files on an SD card, I decided to log the collected UV radiation and weather data in a CSV file on the SD card without applying any additional procedures. Also, I employed XIAO BLE to transmit the prediction (detection) result and the recently collected data over BLE after running my neural network model.

However, before proceeding with the following steps, I needed to set up XIAO BLE on the Arduino IDE and install the required libraries for this project.

#️⃣ To add the XIAO BLE board package to the Arduino IDE, navigate to File ➡ Preferences and paste the URL below under Additional Boards Manager URLs.

https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json

project-image
Figure - 78.36

#️⃣ Then, to install the required core, navigate to Tools ➡ Board ➡ Boards Manager and search for Seeed nRF52 Boards.

project-image
Figure - 78.37

#️⃣ After installing the core, navigate to Tools > Board > Seeed nRF Boards and select Seeed XIAO BLE - nRF52840.

project-image
Figure - 78.38

#️⃣ To transmit (advertise) data packets over BLE, download the ArduinoBLE library: Go to Sketch ➡ Include Library ➡ Manage Libraries… and search for ArduinoBLE.

project-image
Figure - 78.39

#️⃣ Finally, download the required libraries for the BMP180 precision sensor and the SSD1306 OLED display:

Adafruit-BMP085-Library | Download

Adafruit_SSD1306 | Download

Adafruit-GFX-Library | Download

Step 3.1: Displaying images on the SSD1306 OLED screen

To display images (black and white) on the SSD1306 OLED screen successfully, I needed to create monochromatic bitmaps from PNG or JPG files and convert those bitmaps to data arrays.

#️⃣ First of all, download the LCD Assistant.

#️⃣ Then, upload a monochromatic bitmap and select Vertical or Horizontal depending on the screen type.

#️⃣ Convert the image (bitmap) and save the output (data array).

#️⃣ Finally, add the data array to the code and print it on the screen.


static const unsigned char PROGMEM sd [] = {
0x0F, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFE, 0x7C, 0xFF, 0x1B, 0x36, 0x6C, 0x9B,
0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93,
0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x1F, 0xFF, 0xFF, 0xFF,
0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xC7, 0xFF, 0xFF, 0xF9, 0x41, 0xFF, 0x1F, 0xF9, 0xDD, 0xFF,
0x1F, 0xFC, 0xDD, 0xFF, 0x1F, 0xFE, 0x5D, 0xFF, 0x1F, 0xF8, 0x43, 0xFF, 0x1F, 0xFD, 0xFF, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE
};

...

display.clearDisplay(); 
display.drawBitmap(48, 0, sd, 32, 44, SSD1306_WHITE);
display.display();  

project-image
Figure - 78.40


project-image
Figure - 78.41


project-image
Figure - 78.42

Step 4: Collecting and storing UV radiation and weather data w/ XIAO BLE

After setting up XIAO BLE and installing the required libraries, I programmed XIAO BLE to collect UV index, temperature, pressure, and altitude measurements in order to save them to the given CSV file on the SD card.

Since I needed to assign sun damage risk levels (classes) empirically as labels for each data record while collecting data outdoors to create a valid data set, I utilized the built-in button on the XIAO expansion board in two different modes (long press and short press) so as to choose among classes and save data records. After selecting a sun damage risk level (class) by short-pressing the button, XIAO BLE appends the selected class and the recently collected data to the given CSV file on the SD card as a new row if the button is long-pressed.

You can download the BLE_smartwatch_data_collect.ino file to try and inspect the code for collecting UV radiation and weather data and for saving information to the given CSV file on the SD card.

⭐ Include the required libraries.


#include <SPI.h>
#include <SD.h>
#include <Adafruit_BMP085.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

⭐ Define the BMP180 precision sensor and the UV sensor's (Grove) voltage signal pin.


Adafruit_BMP085 bmp;

// Define the Grove – UV Sensor pin.
#define UV_pin A0

⭐ Initialize the File class and define the chip select pin for the MicroSD card module on the XIAO expansion board.


File myFile;
const int chip_select = 2;
// Define the CSV file name: 
const char* data_file = "UV_DATA.csv";

⭐ Define the 0.96 SSD1306 OLED display on the XIAO expansion board.


#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET    -1 // Reset pin # (or -1 if sharing Arduino reset pin)

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

⭐ Define monochrome graphics.

⭐ Define the built-in button pin on the expansion board.

⭐ Then, define the button state and the duration variables to utilize the button in two different modes: long press and short press.


#define button 1
// Define the button state and the duration to utilize the integrated button in two different modes: long press and short press.
int button_state = 0;
#define DURATION 2000

⭐ Initialize the SSD1306 OLED screen.


  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.display();
  delay(1000);

⭐ In the err_msg function, display the error message on the SSD1306 OLED screen and turn the RGB LED to red.


void err_msg(){
  // Show the error message on the SSD1306 screen.
  adjustColor(255, 0, 0);
  display.clearDisplay();   
  display.drawBitmap(48, 0, _error, 32, 32, SSD1306_WHITE);
  display.setTextSize(1); 
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,40); 
  display.println("Check the serial monitor to see the error!");
  display.display();  
}

⭐ Check the BMP180 precision sensor connection status.


  while(!bmp.begin()){
    Serial.println("BMP180 Barometric Pressure/Temperature/Altitude Sensor is not found!");
    err_msg();
    delay(1000);
  }
  Serial.println("\nBMP180 Barometric Pressure/Temperature/Altitude Sensor is connected successfully!\n");

⭐ Check the connection status between XIAO BLE and the SD card. If the connection is successful, turn the RGB LED to blue.


  if (!SD.begin(chip_select)){
    Serial.println("SD card initialization failed!\n");
    err_msg();
    while (1);
  }
  Serial.println("SD card is detected successfully!\n");
  adjustColor(0,0,255);
  delay(5000); 

⭐ In the get_UV_radiation function:

⭐ Get the summation of the latest 1024 UV sensor measurements.

⭐ Obtain the average sensor measurement to remove the glitch.

⭐ Estimate the UV index value with the formula below roughly.

Although the UV sensor measurements cannot be converted into the exact EPA standard UV index values, the UV index can be estimated roughly with the given formula.


void get_UV_radiation(){
  int sensorValue;
  long sum = 0;
  // Get the summation of the latest UV sensor measurements.
  for(int i=0;i<1024;i++){
    sensorValue = analogRead(UV_pin);
    sum+=sensorValue;
    delay(2);
  }
  // Obtain the average sensor measurement to remove the glitch.
  long avr_val = sum/1024;
  // Estimate the UV index value with this formula roughly.
  UV_index = (avr_val*1000/4.3-83)/21;
  UV_index = UV_index / 1000;
  Serial.print("Estimated UV index value: "); Serial.println(UV_index); Serial.println();
  delay(20);
}

⭐ In the collect_BMP180_data function:

⭐ Obtain temperature, pressure, altitude, sea level pressure, and real altitude measurements generated by the BMP180 precision sensor.

⭐ Calculate altitude assuming 'standard' barometric pressure of 1013.25 millibars (101325 Pascals).

⭐ If needed, to get a more precise altitude measurement, use the current sea level pressure, which varies with the weather conditions.


void collect_BMP180_data(){
  _temperature = bmp.readTemperature();
  _pressure = bmp.readPressure();
  // Calculate altitude assuming 'standard' barometric pressure of 1013.25 millibars (101325 Pascals).
  _altitude = bmp.readAltitude();
  _sea_level_pressure = bmp.readSealevelPressure();
  // To get a more precise altitude measurement, use the current sea level pressure, which will vary with the weather conditions. 
  _real_altitude = bmp.readAltitude(101500);
  // Print the data generated by the BMP180 Barometric Pressure/Temperature/Altitude Sensor.
  Serial.print("Temperature => "); Serial.print(_temperature); Serial.println(" *C");
  Serial.print("Pressure => "); Serial.print(_pressure); Serial.println(" Pa");
  Serial.print("Altitude => "); Serial.print(_altitude); Serial.println(" meters");
  Serial.print("Pressure at sea level (calculated) => "); Serial.print(_sea_level_pressure); Serial.println(" Pa");
  Serial.print("Real Altitude => "); Serial.print(_real_altitude); Serial.println(" meters\n");
}

⭐ In the home_screen function, display the collected data and the selected class on the SSD1306 OLED screen.


void home_screen(){
  adjustColor(255,0,255);
  display.clearDisplay();   
  display.setTextSize(1); 
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,8);
  display.println("Estimations:");
  display.println("UV Index => " + String(UV_index));
  display.println("Temp. => " + String(_temperature) + " *C");
  display.println("Pressure => " + String(_pressure) + " Pa");
  display.println("Altitude => " + String(_altitude) + " m");
  display.println();
  display.println("Selected Class => " + String(class_number));
  display.display();  
}

⭐ In the save_data_to_SD_Card function:

⭐ Open the given CSV file on the SD card in the WRITE file mode.

⭐ If the given CSV file is opened successfully, create the data record, including the selected sun damage risk level (class), to be inserted as a new row.

⭐ Then, append the recently created data record and close the CSV file.

⭐ After appending the given data record successfully, notify the user by blinking the RGB LED as green and displaying this message on the SSD1306 OLED screen: Data saved to the SD card!

⭐ If XIAO BLE cannot open the given CSV file successfully, show the error message on the SSD1306 OLED screen.


void save_data_to_SD_Card(int risk_level){
  // Open the given CSV file on the SD card in the WRITE file mode.
  // FILE MODES: WRITE, READ
  myFile = SD.open(data_file, FILE_WRITE);
  adjustColor(255,255,0);
  delay(1000);
  // If the given file is opened successfully:
  if(myFile){
    Serial.print("Writing to "); Serial.print(data_file); Serial.println("...");
    // Create the data record to be inserted as a new row: 
    String data_record = String(UV_index) + "," + String(_temperature)  + "," + String(_pressure)  + "," + String(_altitude) + "," + String(risk_level);
    // Append the data record:
    myFile.println(data_record);
    // Close the CSV file:
    myFile.close();
    Serial.println("Data saved successfully!\n");
    // Notify the user after appending the given data record successfully.
    adjustColor(0,255,0);
    display.clearDisplay(); 
    display.drawBitmap(48, 0, sd, 32, 44, SSD1306_WHITE);
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);  
    display.setCursor(0,48); 
    display.println("Data saved to the SD card!");
    display.display();  
  }else{
    // If XIAO BLE cannot open the given CSV file successfully:
    Serial.println("XIAO BLE cannot open the given CSV file successfully!\n");
    err_msg();
  }
  // Exit and clear:
  delay(4000);
}

⭐ Detect whether the built-in button is short-pressed or long-pressed.


  button_state = 0;
  if(!digitalRead(button)){
    adjustColor(255,255,255);
    timer = millis();
    button_state = 1;
    while((millis()-timer) <= DURATION){
      if(digitalRead(button)){
        button_state = 2;
        break;
      }
    }
  }

⭐ If the button is short-pressed, change the class number [0 - 2] to choose among sun damage risk levels (classes).

⭐ If the button is long-pressed, append the recently created data record to the given CSV file on the SD card.


  if(button_state == 1){
    // Save the given data record to the given CSV file on the SD card when long-pressed.
    save_data_to_SD_Card(class_number);
  }else if(button_state == 2){
    // Change the class number when short-pressed.
    class_number++;
    if(class_number > 2) class_number = 0;
    Serial.println("Selected Class: " + String(class_number) + "\n");
  }

project-image
Figure - 78.43


project-image
Figure - 78.44


project-image
Figure - 78.45


project-image
Figure - 78.46

Step 4.1: Logging the collected data in a CSV file on the SD card

After uploading and running the code for collecting UV radiation and weather data and for saving information to the given CSV file on the SD card on XIAO BLE:

☀️⌚ The smartwatch turns the RGB LED to blue if the sensors and the MicroSD card module connections with XIAO BLE are successful.

project-image
Figure - 78.47

☀️⌚ Then, the smartwatch turns the RGB LED to magenta as the default color and displays the collected data and the selected class on the SSD1306 OLED screen:

project-image
Figure - 78.48

☀️⌚ If the button (built-in) is short-pressed, the smartwatch blinks the RGB LED as white and increases the class number in the range of 0-2:

project-image
Figure - 78.49


project-image
Figure - 78.50


project-image
Figure - 78.51

☀️⌚ If the button (built-in) is long-pressed, the smartwatch blinks the RGB LED as yellow and appends the recently created data record to the UV_DATA.CSV file on the SD card, including the selected sun damage risk level (class) under the risk_level data field.

project-image
Figure - 78.52

☀️⌚ If the smartwatch saves the data record successfully to the given CSV file on the SD card, it blinks the RGB LED as green and displays this message on the SSD1306 OLED screen: Data saved to the SD card!

project-image
Figure - 78.53

☀️⌚ If XIAO BLE throws an error while operating, the smartwatch shows the error message on the SSD1306 OLED screen, turns the RGB LED to red, and prints the error details on the serial monitor.

project-image
Figure - 78.54


project-image
Figure - 78.55

☀️⌚ Also, the smartwatch prints notifications and sensor measurements on the serial monitor for debugging.

project-image
Figure - 78.56


project-image
Figure - 78.57

After logging the collected UV radiation and weather data in the given CSV file on the SD card for 20 days outdoors in different parts of the day (early morning, late afternoon, night, etc.), I elicited my data set with eminent validity:

project-image
Figure - 78.58


project-image
Figure - 78.59


project-image
Figure - 78.60

Since I also needed a testing data set to evaluate the accuracy of my model, I collected additional data to create a modest data set in volume under the test_UV_DATA.CSV file.

project-image
Figure - 78.61

Step 5: Building a neural network model with Edge Impulse

When I completed collating my sun damage risk data set and assigning labels, I had started to work on my artificial neural network model (ANN) to make predictions on sun damage risk levels (classes) based on UV index, temperature, pressure, and altitude measurements.

Since Edge Impulse supports almost every microcontroller and development board due to its model deployment options, I decided to utilize Edge Impulse to build my artificial neural network model. Also, Edge Impulse makes scaling embedded ML applications easier and faster for edge devices such as XIAO BLE.

Even though Edge Impulse supports CSV files to upload samples, the data type should be time series to upload all data records in a single file. Therefore, I needed to follow the steps below to format my data set so as to train my model accurately:

As explained in the previous steps, I assigned sun damage risk classes for each data record empirically while logging data outdoors with the smartwatch. Since UV radiation can impinge on our health detrimentally, it was crucial to assign insightful classes to forecast sun damage risk levels precisely with the limited data volume. Therefore, I derived my classes in reference to the UV Index Scale by EPA, conforming with international guidelines for UVI reporting established by the WHO[2].

project-image
Figure - 78.62

Since the assigned classes are stored under the risk_level data field in the UV_DATA.CSV file, I preprocessed my data set effortlessly to obtain labels for each data record (sample):

Plausibly, Edge Impulse allows building predictive models optimized in size and accuracy automatically and deploying the trained model as an Arduino library. Therefore, after scaling (normalizing) and preprocessing my data set to create samples, I was able to build an accurate neural network model to forecast sun damage risk levels and run it on XIAO BLE effortlessly.

Since I published my Edge Impulse project, you can inspect my neural network model on Edge Impulse.

Step 5.1: Preprocessing and scaling (normalizing) the data set to create samples

To scale (normalize) and preprocess my data set so as to create samples, I developed a Python application consisting of one file:

If the data type is not time series, Edge Impulse requires a CSV file with a header indicating data fields per sample to upload data with CSV files. Since Edge Impulse can infer the uploaded sample's label from its file name, the application reads the given data set and generates a CSV file for each data record (sample), named according to the assigned sun damage risk class of the given data record. Also, the application increases the sample number incrementally for each generated sample having the same label:

First of all, I created a class named process_dataset in the process_dataset.py file to execute the following functions precisely.

⭐ Include the required modules.


import numpy as np
import pandas as pd
from csv import writer

⭐ In the __init__ function, read the data set from the given CSV file and define the sun damage risk class names.


    def __init__(self, csv_path):
        # Read the data set from the given CSV file.
        self.df = pd.read_csv(csv_path)
        # Define the class (label) names.
        self.class_names = ["Tolerable", "Risky", "Perilous"]

⭐ In the scale_data_elements function, scale (normalize) data elements to define appropriately formatted data items in the range of 0-1.


    def scale_data_elements(self):
        self.df["scaled_uv_index"] = self.df["uv_index"] / 10
        self.df["scaled_temperature"] = self.df["temperature"] / 100
        self.df["scaled_pressure"] = self.df["pressure"] / 100000
        self.df["scaled_altitude"] = self.df["altitude"] / 100
        print("Data Elements Scaled Successfully!")

⭐ In the split_dataset_by_labels function:

⭐ Split the data set according to the given sun damage risk level (class).

⭐ Define the header indicating data elements.

⭐ Create a data record (sample) with the scaled data elements and increase the sample number for each sample.

⭐ Then, create a CSV file named with the assigned sun damage risk class (label), identified with the sample number.

⭐ Each sample includes four data items [shape=(4,)]:

[1.2, 0.31379999999999997, 0.9698, 0.9237000000000001]


    def split_dataset_by_labels(self, class_number):
        l = len(self.df)
        sample_number = 0
        # Split the data set according to sun damage risk levels (classes):
        for i in range(l):
            # Add the header as the first row:
            processed_data = [["uv_index","temperature","pressure","altitude"]]
            if (self.df["risk_level"][i] == class_number):
                row = [self.df["scaled_uv_index"][i], self.df["scaled_temperature"][i], self.df["scaled_pressure"][i], self.df["scaled_altitude"][i]]
                processed_data.append(row)
                # Increase the sample number for each sample:   
                sample_number+=1   
                # Create a CSV file for each sample identified with the sample number.
                filename = "{}.sample_{}.csv".format(self.class_names[class_number], sample_number)
                with open(filename, "a", newline="") as f:
                    for r in range(len(processed_data)):
                        writer(f).writerow(processed_data[r])
                    f.close()
                print("CSV File Successfully Created: " + filename)

⭐ Finally, run the split_dataset_by_labels function for each sun damage risk level (class) to create samples in the CSV format.


dataset.scale_data_elements()
for c in range(len(dataset.class_names)):
    dataset.split_dataset_by_labels(c)

project-image
Figure - 78.63


project-image
Figure - 78.64

💻 After executing the application, it generates a CSV file for each data record (sample) in the given data set (training or testing) and prints the file names on the shell for debugging.

📌 Training samples:

project-image
Figure - 78.65


project-image
Figure - 78.66


project-image
Figure - 78.67

📌 Testing samples:

project-image
Figure - 78.68


project-image
Figure - 78.69

project-image
Figure - 78.70

Step 5.2: Uploading samples to Edge Impulse

After generating training and testing samples successfully, I uploaded them to my project on Edge Impulse.

#️⃣ First of all, sign up for Edge Impulse and create a new project.

project-image
Figure - 78.71

#️⃣ Navigate to the Data acquisition page and click the Upload existing data button.

project-image
Figure - 78.72


project-image
Figure - 78.73

#️⃣ Then, choose the data category (training or testing) and select Infer from filename under Label to deduce labels from file names automatically.

#️⃣ Finally, select files and click the Begin upload button.

project-image
Figure - 78.74


project-image
Figure - 78.75


project-image
Figure - 78.76


project-image
Figure - 78.77


project-image
Figure - 78.78


project-image
Figure - 78.79

Step 5.3: Training the model on sun damage risk levels

After uploading my training and testing samples successfully, I designed an impulse and trained it on sun damage risk levels (classes).

An impulse is a custom neural network model in Edge Impulse. I created my impulse by employing the Raw Data block and the Classification learning block.

The Raw Data block generates windows from data samples without any specific signal processing.

The Classification learning block represents a Keras neural network model. Also, it lets the user change the model settings, architecture, and layers.

#️⃣ Go to the Create impulse page. Then, select the Raw Data block and the Classification learning block. Finally, click Save Impulse.

project-image
Figure - 78.80

#️⃣ Before generating features for the model, go to the Raw data page and click Save parameters.

project-image
Figure - 78.81

#️⃣ After saving parameters, click Generate features to apply the Raw Data block to training samples.

project-image
Figure - 78.82


project-image
Figure - 78.83

#️⃣ Finally, navigate to the NN Classifier page and click Start training.

project-image
Figure - 78.84


project-image
Figure - 78.85

I utilized the default classification model settings, architecture, and layers to build my neural network model.

After generating features and training my model with training samples, Edge Impulse evaluated the precision score (accuracy) as 100%.

The precision score is approximately 100% due to the volume and variety of training samples. In technical terms, the model overfits the training data set. Therefore, I am still collecting data to improve my training data set.

project-image
Figure - 78.86


project-image
Figure - 78.87

Step 5.4: Evaluating the model accuracy and deploying the model

After building and training my neural network model, I tested its accuracy and validity by utilizing testing samples.

The evaluated accuracy of the model is 90.91%.

#️⃣ To validate the trained model, go to the Model testing page and click Classify all.

project-image
Figure - 78.88


project-image
Figure - 78.89


project-image
Figure - 78.90

After validating my neural network model, I deployed it as a fully optimized and customizable Arduino library.

#️⃣ To deploy the validated model as an Arduino library, navigate to the Deployment page and select Arduino library.

#️⃣ Then, choose the Unoptimized (float32) option to deploy the model without downsizing the accuracy.

#️⃣ Finally, click Build to download the model as an Arduino library.

project-image
Figure - 78.91


project-image
Figure - 78.92


project-image
Figure - 78.93

Step 6: Setting up the Edge Impulse model on XIAO BLE

After building, training, and deploying my model as an Arduino library on Edge Impulse, I needed to upload and run the Arduino library on XIAO BLE directly so as to create an easy-to-use and capable smartwatch operating outdoors with minimal latency and power consumption.

Since Edge Impulse optimizes and formats signal processing, configuration, and learning blocks into a single package while deploying models as Arduino libraries, I was able to import my model effortlessly to run inferences.

#️⃣ After downloading the model as an Arduino library in the ZIP file format, go to Sketch > Include Library > Add .ZIP Library...

#️⃣ Then, include the BLE_Smartwatch_Detecting_Potential_Sun_Damage_inferencing.h file to import the Edge Impulse neural network model.


#include <BLE_Smartwatch_Detecting_Potential_Sun_Damage_inferencing.h>

After importing my model successfully on the Arduino IDE, I utilized XIAO BLE to run inferences at regular intervals so as to forecast sun damage risk levels.

Also, after running inferences successfully, I employed XIAO BLE to transmit (advertise) the prediction (detection) result and the recently collected data over BLE as a peripheral device.

In any Bluetooth® Low Energy (also referred to as Bluetooth® LE or BLE) connection, devices can have one of these two roles: the central and the peripheral. A peripheral device (also called a client) advertises or broadcasts information about itself to devices in its range, while a central device (also called a server) performs scans to listen for devices broadcasting information. You can get more information regarding BLE connections and procedures, such as services and characteristics, from here.

You can download the BLE_smartwatch_run_model.ino file to try and inspect the code for running Edge Impulse neural network models and transmitting (advertising) information over BLE with XIAO BLE.

You can inspect the corresponding functions and settings in Step 4.

⭐ Include the required libraries.


#include <ArduinoBLE.h>
#include <Adafruit_BMP085.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Include the Edge Impulse model converted to an Arduino library:
#include <BLE_Smartwatch_Detecting_Potential_Sun_Damage_inferencing.h>

⭐ Define the required parameters to run an inference with the Edge Impulse model.

⭐ Define the features array (buffer) to classify one frame of data.


#define FREQUENCY_HZ        EI_CLASSIFIER_FREQUENCY
#define INTERVAL_MS         (1000 / (FREQUENCY_HZ + 1))

// Define the features array to classify one frame of data.
float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
size_t feature_ix = 0;

⭐ Define the threshold value (0.60) for the model outputs (predictions).

⭐ Define the sun damage risk level (class) names and color codes:


float threshold = 0.60;

// Define the sun damage risk level (class) names and color codes:
String classes[] = {"Perilous", "Risky", "Tolerable"};
int color_codes[3][3] = {{255,0,0}, {255,255,0}, {0,255,0}};

⭐ Create the BLE service and data characteristics. Then, allow the remote device (central) to read and notify.


BLEService BLE_smartwatch("19B10000-E8F2-537E-4F6C-D104768A1214");

// Create data characteristics and allow the remote device (central) to read and notify:
BLEFloatCharacteristic temperatureCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic altitudeCharacteristic("19B10002-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic UVCharacteristic("19B10003-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic pressureCharacteristic("19B10004-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic classCharacteristic("19B10005-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);

⭐ Define monochrome graphics.

⭐ Create an array including icons for each sun damage risk level (class).


static const unsigned char PROGMEM *class_icons[] = {tolerable, risky, perilous};

⭐ Check the BLE initialization status and print the XIAO BLE address information on the serial monitor.


  while(!BLE.begin()){
    Serial.println("BLE initialization is failed!");
    err_msg();
  }
  Serial.println("\nBLE initialization is successful!\n");
  // Print this peripheral device's address information:
  Serial.print("MAC Address: "); Serial.println(BLE.address());
  Serial.print("Service UUID Address: "); Serial.println(BLE_smartwatch.uuid()); Serial.println();

⭐ Set the local name (BLE UV Smartwatch) for XIAO BLE and the UUID for the advertised (transmitted) service.

⭐ Add the given data characteristics to the service. Then, add the service to the device.

⭐ Assign event handlers for connected and disconnected devices to/from XIAO BLE.

⭐ Finally, start advertising (broadcasting) information.


  BLE.setLocalName("BLE UV Smartwatch");
  // Set the UUID for the service this peripheral advertises:
  BLE.setAdvertisedService(BLE_smartwatch);

  // Add the given data characteristics to the service:
  BLE_smartwatch.addCharacteristic(temperatureCharacteristic);
  BLE_smartwatch.addCharacteristic(altitudeCharacteristic);
  BLE_smartwatch.addCharacteristic(UVCharacteristic);
  BLE_smartwatch.addCharacteristic(pressureCharacteristic);
  BLE_smartwatch.addCharacteristic(classCharacteristic);

  // Add the service to the device:
  BLE.addService(BLE_smartwatch);

  // Assign event handlers for connected, disconnected devices to this peripheral:
  BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
  BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);

  // Start advertising:
  BLE.advertise();
  Serial.println(("Bluetooth device active, waiting for connections..."));

⭐ In the run_inference_to_make_predictions function:

⭐ Scale (normalize) the collected data depending on the given model and copy the scaled data items to the features array (buffer).

⭐ If required, multiply the scaled data items while copying them to the features array (buffer).

⭐ Display the progress of copying data to the features buffer on the serial monitor.

⭐ If the features buffer is full, create a signal object from the features buffer (frame).

⭐ Then, run the classifier.

⭐ Print the inference timings on the serial monitor.

⭐ Read the prediction (detection) result for each sun damage risk class (label).

⭐ Obtain the detection result greater than the given threshold (0.60). It represents the most accurate label (sun damage risk class) predicted by the model.

⭐ Print the detected anomalies on the serial monitor, if any.

⭐ Finally, clear the features buffer (frame).


void run_inference_to_make_predictions(int multiply){
  // Scale (normalize) data items depending on the given model:
  float scaled_UV_index = UV_index / 10;
  float scaled_temperature = _temperature / 100;
  float scaled_pressure = _pressure / 100000;
  float scaled_altitude = _altitude / 100;

  // Copy the scaled data items to the features buffer.
  // If required, multiply the scaled data items while copying them to the features buffer.
  for(int i=0; i<multiply; i++){  
    features[feature_ix++] = scaled_UV_index;
    features[feature_ix++] = scaled_temperature;
    features[feature_ix++] = scaled_pressure;
    features[feature_ix++] = scaled_altitude;
  }

  // Display the progress of copying data to the features buffer.
  Serial.print("Features Buffer Progress: "); Serial.print(feature_ix); Serial.print(" / "); Serial.println(EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
  
  // Run inference:
  if(feature_ix == EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE){    
    ei_impulse_result_t result;
    // Create a signal object from the features buffer (frame).
    signal_t signal;
    numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
    // Run the classifier:
    EI_IMPULSE_ERROR res = run_classifier(&signal, &result, false);
    ei_printf("\nrun_classifier returned: %d\n", res);
    if(res != 0) return;

    // Print the inference timings on the serial monitor.
    ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n", 
        result.timing.dsp, result.timing.classification, result.timing.anomaly);

    // Obtain the prediction results for each label (class).
    for(size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++){
      // Print the prediction results on the serial monitor.
      ei_printf("%s:\t%.5f\n", result.classification[ix].label, result.classification[ix].value);
      // Get the predicted label (class).
      if(result.classification[ix].value >= threshold) predicted_class = ix;
    }
    Serial.print("\nPredicted Class: "); Serial.println(predicted_class);

    // Detect anomalies, if any:
    #if EI_CLASSIFIER_HAS_ANOMALY == 1
      ei_printf("Anomaly : \t%.3f\n", result.anomaly);
    #endif

    // Clear the features buffer (frame):
    feature_ix = 0;
  }
}

⭐ In the update_characteristics function, update all float data characteristics to transmit (advertise) the given information over BLE.


void update_characteristics(){
  // Update all data characteristics (floats):
  temperatureCharacteristic.writeValue(_temperature);
  altitudeCharacteristic.writeValue(_altitude);
  UVCharacteristic.writeValue(float(UV_index));
  pressureCharacteristic.writeValue(float(_pressure));
  classCharacteristic.writeValue(float(predicted_class));
  Serial.println("\n\nBLE: Data Characteristics Updated Successfully!\n");
}

⭐ Every 30 seconds, attempt to transmit (advertise) the recently collected data and the prediction (detection) result over BLE if the Edge Impulse model forecasts a sun damage risk level (label) successfully.

⭐ After updating data characteristics, display the prediction (detection) result (class) on the SSD1306 OLED screen with its assigned monochrome icon and turn the RGB LED to its given color code.

⭐ Finally, clear the predicted label (class) and update the timer.


  if(millis() - timer >= 30*1000){
    // If the Edge Impulse model predicted a label (class) successfully:
    if(predicted_class != -1){
      update_characteristics();
      // After updating characteristics, notify the user and print the predicted label (class) on the screen.
      display.clearDisplay();
      display.drawBitmap(48, 0, class_icons[predicted_class], 32, 32, SSD1306_WHITE);
      display.setTextSize(1); 
      display.setTextColor(SSD1306_WHITE);
      // Print:
      display.setCursor(0,40);
      display.println("BLE: Data Transmitted");
      String c = "Class: " + classes[predicted_class];
      int str_x = c.length() * 6;
      display.setCursor((SCREEN_WIDTH - str_x) / 2, 56);
      display.println(c);
      display.display();
      adjustColor(color_codes[predicted_class][0], color_codes[predicted_class][1], color_codes[predicted_class][2]);
      delay(5000);
      // Clear the predicted label (class).
      predicted_class = -1;
    }
    // Update the timer:
    timer = millis();
  }

project-image
Figure - 78.94


project-image
Figure - 78.95


project-image
Figure - 78.96


project-image
Figure - 78.97


project-image
Figure - 78.98


project-image
Figure - 78.99

Step 7: Running the model on XIAO BLE to make predictions on sun damage risk levels

When the features array (buffer) is full with data items, my Edge Impulse neural network model predicts possibilities of labels (sun damage risk classes) for the given features buffer as an array of 3 numbers. They represent the model's "confidence" that the given features array (buffer) corresponds to each of the three different sun damage risk levels (classes) based on UV index, temperature, pressure, and altitude measurements [0 - 2], as shown in Step 5:

As discussed in the previous steps, I also employed XIAO BLE to transmit (advertise) the prediction (detection) result and the recently collected data over BLE.

After executing the BLE_smartwatch_run_model.ino file on XIAO BLE:

☀️⌚ The smartwatch turns the RGB LED to blue if the BLE initialization status is successful.

project-image
Figure - 78.100


project-image
Figure - 78.101

☀️⌚ Then, the smartwatch turns the RGB LED to magenta as the default color and displays the collected data on the SSD1306 OLED screen:

project-image
Figure - 78.102


project-image
Figure - 78.103

☀️⌚ The smartwatch runs inferences with the Edge Impulse model at regular intervals by filling the features buffer with the recently collected UV index, temperature, pressure, and altitude measurements.

☀️⌚ Then, the smartwatch displays the detection result, which represents the most accurate label (sun damage risk class) predicted by the model.

☀️⌚ Each sun damage risk level (class) has a unique monochrome icon to be shown on the SSD1306 OLED screen and a color code for adjusting the RGB LED when being predicted (detected) by the model:

☀️⌚ After running inference successfully, the smartwatch also transmits (advertises) the prediction result (class) and the recently collected data over BLE.

☀️⌚ If XIAO BLE transmits the given information over BLE successfully, the smartwatch prints this message on the screen:

BLE: Data Transmitted

project-image
Figure - 78.104


project-image
Figure - 78.105


project-image
Figure - 78.106


project-image
Figure - 78.107


project-image
Figure - 78.108


project-image
Figure - 78.109

☀️⌚ Also, the smartwatch prints notifications and sensor measurements on the serial monitor for debugging.

project-image
Figure - 78.110


project-image
Figure - 78.111


project-image
Figure - 78.112

As far as my experiments go, the smartwatch operates impeccably while forecasting sun damage risk levels (classes) and transmitting (advertising) the prediction result over BLE outdoors :)

project-image
Figure - 78.113

Step 8: Monitoring the model prediction result over BLE via the BLE UV Smartwatch app

As discussed in Step 2, I developed an Android application (BLE UV Smartwatch) from scratch with the MIT APP Inventor so as to obtain and display the transmitted (advertised) information from the smartwatch over BLE.

☀️⌚ If the Scan button is pressed, the application scans for compatible BLE devices and shows them as a list.

project-image
Figure - 78.114

☀️⌚ If the Stop button is pressed, the application desists from scanning devices.

project-image
Figure - 78.115

☀️⌚ If the Connect button is pressed, the application attempts to connect to the selected BLE device (smartwatch) over BLE.

project-image
Figure - 78.116

☀️⌚ If the application connects to the smartwatch (peripheral device) as the central device successfully, the application waits for the advertised (transmitted) data packets from the smartwatch.

project-image
Figure - 78.117

☀️⌚ When the application receives the advertised data packets, it shows the recently collected data by the smartwatch and the sun damage risk level (class) predicted by the Edge Impulse model.

☀️⌚ Each sun damage risk level (class) has a unique fusion state image and color code to be displayed on the screen when being received:

project-image
Figure - 78.118


project-image
Figure - 78.119


project-image
Figure - 78.120

☀️⌚ If the Disconnect button is pressed, the application disconnects from the smartwatch and clears all received information.

project-image
Figure - 78.121

Videos and Conclusion




After completing all steps above and experimenting, I have employed the smartwatch to forecast and detect sun damage risk levels while hiking or wandering outdoors so as to get prescient warnings regarding possible sunburn and health risks to mitigate the brunt of inadvertent excess sun exposure.

project-image
Figure - 78.122

Further Discussions

By applying neural network models trained on UV index, temperature, pressure, and altitude measurements in detecting sun damage risk levels outdoors, we can achieve to:

☀️⌚ avert UV-related skin disorders,

☀️⌚ prevent severe health problems such as photoaging, DNA damage, and immunosuppression,

☀️⌚ reduce the chances of getting UV-related eye damage, such as cataracts.

project-image
Figure - 78.123

References

[1] UV Radiation, United States Environmental Protection Agency, EPA 430-F-10-025, June 2010, https://www.epa.gov/sites/default/files/documents/uvradiation.pdf.

[2] UV Index Scale, United States Environmental Protection Agency, EPA, https://www.epa.gov/sunsafety/uv-index-scale-0.

Code

BLE_smartwatch_data_collect.ino

Download



         /////////////////////////////////////////////  
        //   BLE AI-driven Smartwatch Detecting    //
       //   Potential Sun Damage w/ Edge Impulse  //
      //             ---------------             //
     //               (XIAO BLE)                //           
    //             by Kutluhan Aktar           // 
   //                                         //
  /////////////////////////////////////////////

//
// Log UV & weather data on an SD card to train an Edge Impulse model. Then, run it to get informed of sun damage over BLE via an Android app.
//
// For more information:
// https://www.theamplituhedron.com/projects/BLE_AI_driven_Smartwatch_Detecting_Potential_Sun_Damage_w_Edge_Impulse
//
//
// Connections
// XIAO BLE :  
//                                Grove - UV Sensor
// A0  --------------------------- SIG
//                                BMP180 Barometric Pressure/Temperature/Altitude Sensor
// A4  --------------------------- SDA
// A5  --------------------------- SCL
//                                SSD1306 OLED Display (128x64)
// A4  --------------------------- SDA
// A5  --------------------------- SCL
//                                MicroSD Card Module (Built-in on the XIAO Expansion board)
// D10 --------------------------- MOSI
// D9  --------------------------- MISO
// D8  --------------------------- CLK (SCK)
// D2  --------------------------- CS  
//                                Button (Built-in on the XIAO Expansion board)
// D1  --------------------------- +
//                                Keyes 10mm RGB LED Module (140C05)
// D7  --------------------------- R
// D3  --------------------------- G
// D6  --------------------------- B  


// Include the required libraries:
#include <SPI.h>
#include <SD.h>
#include <Adafruit_BMP085.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Define the BMP180 Barometric Pressure/Temperature/Altitude Sensor.
Adafruit_BMP085 bmp;

// Define the Grove – UV Sensor pin.
#define UV_pin A0

// Initialize the File class and define the chip select pin:
File myFile;
const int chip_select = 2;
// Define the CSV file name: 
const char* data_file = "UV_DATA.csv";

// Define the 0.96 OLED display (SSD1306) on the XIAO Expansion board. 
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET    -1 // Reset pin # (or -1 if sharing Arduino reset pin)

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Define monochrome graphics:
static const unsigned char PROGMEM _error [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x01, 0x80, 0x01, 0x80,
0x06, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x30, 0x08, 0x01, 0x80, 0x10, 0x10, 0x03, 0xC0, 0x08,
0x30, 0x02, 0x40, 0x0C, 0x20, 0x02, 0x40, 0x04, 0x60, 0x02, 0x40, 0x06, 0x40, 0x02, 0x40, 0x02,
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02,
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x03, 0xC0, 0x02, 0x40, 0x01, 0x80, 0x02,
0x40, 0x00, 0x00, 0x02, 0x60, 0x00, 0x00, 0x06, 0x20, 0x01, 0x80, 0x04, 0x30, 0x03, 0xC0, 0x0C,
0x10, 0x03, 0xC0, 0x08, 0x08, 0x01, 0x80, 0x10, 0x0C, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x60,
0x01, 0x80, 0x01, 0x80, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const unsigned char PROGMEM sd [] = {
0x0F, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFE, 0x7C, 0xFF, 0x1B, 0x36, 0x6C, 0x9B,
0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93,
0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x19, 0x26, 0x4C, 0x93, 0x1F, 0xFF, 0xFF, 0xFF,
0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0xFF, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xC7, 0xFF, 0xFF, 0xF9, 0x41, 0xFF, 0x1F, 0xF9, 0xDD, 0xFF,
0x1F, 0xFC, 0xDD, 0xFF, 0x1F, 0xFE, 0x5D, 0xFF, 0x1F, 0xF8, 0x43, 0xFF, 0x1F, 0xFD, 0xFF, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE
};

// Define the integrated button pin on the XIAO Expansion board.
#define button 1
// Define the button state and the duration to utilize the integrated button in two different modes: long press and short press.
int button_state = 0;
#define DURATION 2000

// Define the RGB LED pins:
#define redPin     7
#define greenPin   3
#define bluePin    6

// Define the data holders:
int class_number = 0;
float _temperature, _altitude, _real_altitude;
int UV_index, _pressure, _sea_level_pressure;
long timer;
 
void setup(){
  Serial.begin(9600);
  // while(!Serial); // Uncomment for debugging.

  pinMode(button, INPUT_PULLUP);
  
  // Initialize the SSD1306 screen:
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.display();
  delay(1000);

  display.clearDisplay();   
  display.setTextSize(2); 
  display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
  display.setCursor(0,0);
  display.println("BLE");
  display.println("Smartwatch");
  display.setTextSize(1);
  display.println("\n\nw/ Android");
  display.println("& Edge Impulse");
  display.display();

  // Check the BMP180 Barometric Pressure/Temperature/Altitude Sensor connection status: 
  while(!bmp.begin()){
    Serial.println("BMP180 Barometric Pressure/Temperature/Altitude Sensor is not found!");
    err_msg();
    delay(1000);
  }
  Serial.println("\nBMP180 Barometric Pressure/Temperature/Altitude Sensor is connected successfully!\n");

  // Check the connection status between XIAO BLE and the SD card.
  if (!SD.begin(chip_select)){
    Serial.println("SD card initialization failed!\n");
    err_msg();
    while (1);
  }
  Serial.println("SD card is detected successfully!\n");
  adjustColor(0,0,255);
  delay(5000);  
}
 
void loop(){
  get_UV_radiation();
  delay(100);
  collect_BMP180_data();
  delay(500);

  // Show the collected data on the screen.
  home_screen();

  // Detect the long press and short press button modes:
  button_state = 0;
  if(!digitalRead(button)){
    adjustColor(255,255,255);
    timer = millis();
    button_state = 1;
    while((millis()-timer) <= DURATION){
      if(digitalRead(button)){
        button_state = 2;
        break;
      }
    }
  }
  
  if(button_state == 1){
    // Save the given data record to the given CSV file on the SD card when long-pressed.
    save_data_to_SD_Card(class_number);
  }else if(button_state == 2){
    // Change the class number when short-pressed.
    class_number++;
    if(class_number > 2) class_number = 0;
    Serial.println("Selected Class: " + String(class_number) + "\n");
  }

}
void save_data_to_SD_Card(int risk_level){
  // Open the given CSV file on the SD card in the WRITE file mode.
  // FILE MODES: WRITE, READ
  myFile = SD.open(data_file, FILE_WRITE);
  adjustColor(255,255,0);
  delay(1000);
  // If the given file is opened successfully:
  if(myFile){
    Serial.print("Writing to "); Serial.print(data_file); Serial.println("...");
    // Create the data record to be inserted as a new row: 
    String data_record = String(UV_index) + "," + String(_temperature)  + "," + String(_pressure)  + "," + String(_altitude) + "," + String(risk_level);
    // Append the data record:
    myFile.println(data_record);
    // Close the CSV file:
    myFile.close();
    Serial.println("Data saved successfully!\n");
    // Notify the user after appending the given data record successfully.
    adjustColor(0,255,0);
    display.clearDisplay(); 
    display.drawBitmap(48, 0, sd, 32, 44, SSD1306_WHITE);
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);  
    display.setCursor(0,48); 
    display.println("Data saved to the SD card!");
    display.display();  
  }else{
    // If XIAO BLE cannot open the given CSV file successfully:
    Serial.println("XIAO BLE cannot open the given CSV file successfully!\n");
    err_msg();
  }
  // Exit and clear:
  delay(4000);
}

void home_screen(){
  adjustColor(255,0,255);
  display.clearDisplay();   
  display.setTextSize(1); 
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,8);
  display.println("Estimations:");
  display.println("UV Index => " + String(UV_index));
  display.println("Temp. => " + String(_temperature) + " *C");
  display.println("Pressure => " + String(_pressure) + " Pa");
  display.println("Altitude => " + String(_altitude) + " m");
  display.println();
  display.println("Selected Class => " + String(class_number));
  display.display();  
}

void get_UV_radiation(){
  int sensorValue;
  long sum = 0;
  // Get the summation of the latest UV sensor measurements.
  for(int i=0;i<1024;i++){
    sensorValue = analogRead(UV_pin);
    sum+=sensorValue;
    delay(2);
  }
  // Obtain the average sensor measurement to remove the glitch.
  long avr_val = sum/1024;
  // Estimate the UV index value with this formula roughly.
  UV_index = (avr_val*1000/4.3-83)/21;
  UV_index = UV_index / 1000;
  Serial.print("Estimated UV index value: "); Serial.println(UV_index); Serial.println();
  delay(20);
}

void collect_BMP180_data(){
  _temperature = bmp.readTemperature();
  _pressure = bmp.readPressure();
  // Calculate altitude assuming 'standard' barometric pressure of 1013.25 millibars (101325 Pascals).
  _altitude = bmp.readAltitude();
  _sea_level_pressure = bmp.readSealevelPressure();
  // To get a more precise altitude measurement, use the current sea level pressure, which will vary with the weather conditions. 
  _real_altitude = bmp.readAltitude(101500);
  // Print the data generated by the BMP180 Barometric Pressure/Temperature/Altitude Sensor.
  Serial.print("Temperature => "); Serial.print(_temperature); Serial.println(" *C");
  Serial.print("Pressure => "); Serial.print(_pressure); Serial.println(" Pa");
  Serial.print("Altitude => "); Serial.print(_altitude); Serial.println(" meters");
  Serial.print("Pressure at sea level (calculated) => "); Serial.print(_sea_level_pressure); Serial.println(" Pa");
  Serial.print("Real Altitude => "); Serial.print(_real_altitude); Serial.println(" meters\n");
}

void err_msg(){
  // Show the error message on the SSD1306 screen.
  adjustColor(255, 0, 0);
  display.clearDisplay();   
  display.drawBitmap(48, 0, _error, 32, 32, SSD1306_WHITE);
  display.setTextSize(1); 
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,40); 
  display.println("Check the serial monitor to see the error!");
  display.display();  
}

void adjustColor(int r, int g, int b){
  analogWrite(redPin, (255-r));
  analogWrite(greenPin, (255-g));
  analogWrite(bluePin, (255-b));
}


BLE_smartwatch_run_model.ino

Download



         /////////////////////////////////////////////  
        //   BLE AI-driven Smartwatch Detecting    //
       //   Potential Sun Damage w/ Edge Impulse  //
      //             ---------------             //
     //               (XIAO BLE)                //           
    //             by Kutluhan Aktar           // 
   //                                         //
  /////////////////////////////////////////////

//
// Log UV & weather data on an SD card to train an Edge Impulse model. Then, run it to get informed of sun damage over BLE via an Android app.
//
// For more information:
// https://www.theamplituhedron.com/projects/BLE_AI_driven_Smartwatch_Detecting_Potential_Sun_Damage_w_Edge_Impulse
//
//
// Connections
// XIAO BLE :  
//                                Grove - UV Sensor
// A0  --------------------------- SIG
//                                BMP180 Barometric Pressure/Temperature/Altitude Sensor
// A4  --------------------------- SDA
// A5  --------------------------- SCL
//                                SSD1306 OLED Display (128x64)
// A4  --------------------------- SDA
// A5  --------------------------- SCL
//                                MicroSD Card Module (Built-in on the XIAO Expansion board)
// D10 --------------------------- MOSI
// D9  --------------------------- MISO
// D8  --------------------------- CLK (SCK)
// D2  --------------------------- CS  
//                                Button (Built-in on the XIAO Expansion board)
// D1  --------------------------- +
//                                Keyes 10mm RGB LED Module (140C05)
// D7  --------------------------- R
// D3  --------------------------- G
// D6  --------------------------- B  


// Include the required libraries:
#include <ArduinoBLE.h>
#include <Adafruit_BMP085.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// Include the Edge Impulse model converted to an Arduino library:
#include <BLE_Smartwatch_Detecting_Potential_Sun_Damage_inferencing.h>

// Define the required parameters to run an inference with the Edge Impulse model.
#define FREQUENCY_HZ        EI_CLASSIFIER_FREQUENCY
#define INTERVAL_MS         (1000 / (FREQUENCY_HZ + 1))

// Define the features array to classify one frame of data.
float features[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE];
size_t feature_ix = 0;

// Define the threshold value for the model outputs (predictions).
float threshold = 0.60;

// Define the sun damage risk level (class) names and color codes:
String classes[] = {"Perilous", "Risky", "Tolerable"};
int color_codes[3][3] = {{255,0,0}, {255,255,0}, {0,255,0}};

// Create the BLE service:
BLEService BLE_smartwatch("19B10000-E8F2-537E-4F6C-D104768A1214");

// Create data characteristics and allow the remote device (central) to read and notify:
BLEFloatCharacteristic temperatureCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic altitudeCharacteristic("19B10002-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic UVCharacteristic("19B10003-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic pressureCharacteristic("19B10004-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);
BLEFloatCharacteristic classCharacteristic("19B10005-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify);

// Define the BMP180 Barometric Pressure/Temperature/Altitude Sensor.
Adafruit_BMP085 bmp;

// Define the Grove – UV Sensor pin.
#define UV_pin A0

// Define the 0.96 OLED display (SSD1306) on the XIAO Expansion board. 
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET    -1 // Reset pin # (or -1 if sharing Arduino reset pin)

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Define monochrome graphics:
static const unsigned char PROGMEM _error [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x01, 0x80, 0x01, 0x80,
0x06, 0x00, 0x00, 0x60, 0x0C, 0x00, 0x00, 0x30, 0x08, 0x01, 0x80, 0x10, 0x10, 0x03, 0xC0, 0x08,
0x30, 0x02, 0x40, 0x0C, 0x20, 0x02, 0x40, 0x04, 0x60, 0x02, 0x40, 0x06, 0x40, 0x02, 0x40, 0x02,
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02,
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x03, 0xC0, 0x02, 0x40, 0x01, 0x80, 0x02,
0x40, 0x00, 0x00, 0x02, 0x60, 0x00, 0x00, 0x06, 0x20, 0x01, 0x80, 0x04, 0x30, 0x03, 0xC0, 0x0C,
0x10, 0x03, 0xC0, 0x08, 0x08, 0x01, 0x80, 0x10, 0x0C, 0x00, 0x00, 0x30, 0x06, 0x00, 0x00, 0x60,
0x01, 0x80, 0x01, 0x80, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const unsigned char PROGMEM tolerable [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00,
0x00, 0x01, 0x80, 0x00, 0x04, 0x03, 0xC0, 0x20, 0x03, 0x00, 0x00, 0xC0, 0x03, 0xC7, 0xE3, 0xC0,
0x01, 0x9F, 0xF9, 0x80, 0x01, 0x3F, 0xFC, 0x80, 0x00, 0x7F, 0xFE, 0x00, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0xFF, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x05, 0xFF, 0xFF, 0xA0, 0x3D, 0xFF, 0xFF, 0xBC,
0x7D, 0xFF, 0xFF, 0xBE, 0x0D, 0xFF, 0xFF, 0xB0, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFE, 0x00, 0x01, 0x3F, 0xFC, 0x80, 0x01, 0x9F, 0xF9, 0x80,
0x03, 0xC7, 0xE3, 0xC0, 0x03, 0x80, 0x01, 0xC0, 0x06, 0x03, 0xC0, 0x60, 0x00, 0x03, 0xC0, 0x00,
0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const unsigned char PROGMEM risky [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0x80, 0x00, 0x02, 0x03, 0xC0, 0x00,
0x03, 0xC7, 0xE3, 0x80, 0x01, 0xE0, 0x07, 0x80, 0x01, 0xCF, 0xF3, 0x80, 0x01, 0x3F, 0xFC, 0x80,
0x00, 0x7F, 0xFE, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x1D, 0xFF, 0xFF, 0xB8, 0x7B, 0xFF, 0xFF, 0xDE,
0x3B, 0xFF, 0xFF, 0xDC, 0x1B, 0xFF, 0xFF, 0xD8, 0x0F, 0xFF, 0xFF, 0xF0, 0x07, 0xFF, 0xFF, 0xE0,
0x07, 0xFF, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xF0, 0x1B, 0xFF, 0xFF, 0xD8, 0x3B, 0xFF, 0xFF, 0xDC,
0x7B, 0xFF, 0xFF, 0xDE, 0x0D, 0xFF, 0xFF, 0xB0, 0x00, 0xFF, 0xFF, 0x00, 0x01, 0x7F, 0xFE, 0x80,
0x01, 0x3F, 0xFD, 0x80, 0x01, 0xCF, 0xF3, 0x80, 0x01, 0xE0, 0x07, 0x80, 0x03, 0x87, 0xE1, 0x80,
0x00, 0x03, 0xC0, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const unsigned char PROGMEM perilous [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x11, 0x88, 0x00, 0x00, 0x19, 0x98, 0x00,
0x01, 0x19, 0x98, 0x80, 0x00, 0x9D, 0xBB, 0x00, 0x00, 0xE3, 0x87, 0x00, 0x08, 0xDF, 0xF2, 0x30,
0x07, 0x3F, 0xFC, 0xE0, 0x03, 0x7F, 0xFE, 0xC0, 0x02, 0xFF, 0xFF, 0x00, 0x7D, 0xFF, 0xFF, 0x3C,
0x1D, 0xFF, 0xFF, 0xB8, 0x05, 0xFF, 0xFF, 0xA0, 0x03, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xFF, 0xBE,
0x1F, 0xFF, 0xFF, 0xB8, 0x03, 0xFF, 0xFF, 0x80, 0x0D, 0xFF, 0xFF, 0xB0, 0x1D, 0xFF, 0xFF, 0xB8,
0x60, 0xFF, 0xFF, 0x04, 0x02, 0xFF, 0xFE, 0x40, 0x07, 0x7F, 0xFE, 0xE0, 0x0E, 0x3F, 0xF8, 0x70,
0x18, 0xEF, 0xE7, 0x10, 0x00, 0xD8, 0x13, 0x00, 0x01, 0x9D, 0xB9, 0x80, 0x01, 0x19, 0x98, 0x80,
0x00, 0x11, 0x98, 0x00, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// Create an array including icons for labels (classes).
static const unsigned char PROGMEM *class_icons[] = {tolerable, risky, perilous};

// Define the RGB LED pins:
#define redPin     7
#define greenPin   3
#define bluePin    6

// Define the data holders:
float _temperature, _altitude, _real_altitude;
int UV_index, _pressure, _sea_level_pressure;
long timer;
int predicted_class = -1;
 
void setup(){
  Serial.begin(9600);
  // while(!Serial); // Uncomment for debugging.

  // Initialize the SSD1306 screen:
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.display();
  delay(1000);

  display.clearDisplay();   
  display.setTextSize(2); 
  display.setTextColor(SSD1306_BLACK, SSD1306_WHITE);
  display.setCursor(0,0);
  display.println("BLE");
  display.println("Smartwatch");
  display.setTextSize(1);
  display.println("\n\nw/ Android");
  display.println("& Edge Impulse");
  display.display();

  // Check the BLE initialization status:
  while(!BLE.begin()){
    Serial.println("BLE initialization is failed!");
    err_msg();
  }
  Serial.println("\nBLE initialization is successful!\n");
  // Print this peripheral device's address information:
  Serial.print("MAC Address: "); Serial.println(BLE.address());
  Serial.print("Service UUID Address: "); Serial.println(BLE_smartwatch.uuid()); Serial.println();

  // Set the local name this peripheral advertises: 
  BLE.setLocalName("BLE UV Smartwatch");
  // Set the UUID for the service this peripheral advertises:
  BLE.setAdvertisedService(BLE_smartwatch);

  // Add the given data characteristics to the service:
  BLE_smartwatch.addCharacteristic(temperatureCharacteristic);
  BLE_smartwatch.addCharacteristic(altitudeCharacteristic);
  BLE_smartwatch.addCharacteristic(UVCharacteristic);
  BLE_smartwatch.addCharacteristic(pressureCharacteristic);
  BLE_smartwatch.addCharacteristic(classCharacteristic);

  // Add the service to the device:
  BLE.addService(BLE_smartwatch);

  // Assign event handlers for connected, disconnected devices to this peripheral:
  BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
  BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);

  // Start advertising:
  BLE.advertise();
  Serial.println(("Bluetooth device active, waiting for connections..."));

  // Check the BMP180 Barometric Pressure/Temperature/Altitude Sensor connection status: 
  while(!bmp.begin()){
    Serial.println("BMP180 Barometric Pressure/Temperature/Altitude Sensor is not found!");
    err_msg();
    delay(1000);
  }
  Serial.println("\nBMP180 Barometric Pressure/Temperature/Altitude Sensor is connected successfully!\n");

  adjustColor(0,0,255);
  delay(5000);  
}
 
void loop(){
  get_UV_radiation();
  delay(100);
  collect_BMP180_data();
  delay(500);

  // Run inference:
  run_inference_to_make_predictions(10);

  // Every 30 seconds, advertise (transmit) the collected data and the predicted label (class) to the Android application over BLE.
  if(millis() - timer >= 30*1000){
    // If the Edge Impulse model predicted a label (class) successfully:
    if(predicted_class != -1){
      update_characteristics();
      // After updating characteristics, notify the user and print the predicted label (class) on the screen.
      display.clearDisplay();
      display.drawBitmap(48, 0, class_icons[predicted_class], 32, 32, SSD1306_WHITE);
      display.setTextSize(1); 
      display.setTextColor(SSD1306_WHITE);
      // Print:
      display.setCursor(0,40);
      display.println("BLE: Data Transmitted");
      String c = "Class: " + classes[predicted_class];
      int str_x = c.length() * 6;
      display.setCursor((SCREEN_WIDTH - str_x) / 2, 56);
      display.println(c);
      display.display();
      adjustColor(color_codes[predicted_class][0], color_codes[predicted_class][1], color_codes[predicted_class][2]);
      delay(5000);
      // Clear the predicted label (class).
      predicted_class = -1;
    }
    // Update the timer:
    timer = millis();
  }
  
  // Show the collected data on the screen.
  home_screen();

  // Poll for BLE events:
  BLE.poll();
}

void run_inference_to_make_predictions(int multiply){
  // Scale (normalize) data items depending on the given model:
  float scaled_UV_index = UV_index / 10;
  float scaled_temperature = _temperature / 100;
  float scaled_pressure = _pressure / 100000;
  float scaled_altitude = _altitude / 100;

  // Copy the scaled data items to the features buffer.
  // If required, multiply the scaled data items while copying them to the features buffer.
  for(int i=0; i<multiply; i++){  
    features[feature_ix++] = scaled_UV_index;
    features[feature_ix++] = scaled_temperature;
    features[feature_ix++] = scaled_pressure;
    features[feature_ix++] = scaled_altitude;
  }

  // Display the progress of copying data to the features buffer.
  Serial.print("Features Buffer Progress: "); Serial.print(feature_ix); Serial.print(" / "); Serial.println(EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
  
  // Run inference:
  if(feature_ix == EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE){    
    ei_impulse_result_t result;
    // Create a signal object from the features buffer (frame).
    signal_t signal;
    numpy::signal_from_buffer(features, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
    // Run the classifier:
    EI_IMPULSE_ERROR res = run_classifier(&signal, &result, false);
    ei_printf("\nrun_classifier returned: %d\n", res);
    if(res != 0) return;

    // Print the inference timings on the serial monitor.
    ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n", 
        result.timing.dsp, result.timing.classification, result.timing.anomaly);

    // Obtain the prediction results for each label (class).
    for(size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++){
      // Print the prediction results on the serial monitor.
      ei_printf("%s:\t%.5f\n", result.classification[ix].label, result.classification[ix].value);
      // Get the predicted label (class).
      if(result.classification[ix].value >= threshold) predicted_class = ix;
    }
    Serial.print("\nPredicted Class: "); Serial.println(predicted_class);

    // Detect anomalies, if any:
    #if EI_CLASSIFIER_HAS_ANOMALY == 1
      ei_printf("Anomaly : \t%.3f\n", result.anomaly);
    #endif

    // Clear the features buffer (frame):
    feature_ix = 0;
  }
}

void update_characteristics(){
  // Update all data characteristics (floats):
  temperatureCharacteristic.writeValue(_temperature);
  altitudeCharacteristic.writeValue(_altitude);
  UVCharacteristic.writeValue(float(UV_index));
  pressureCharacteristic.writeValue(float(_pressure));
  classCharacteristic.writeValue(float(predicted_class));
  Serial.println("\n\nBLE: Data Characteristics Updated Successfully!\n");
}

void home_screen(){
  adjustColor(255,0,255);
  display.clearDisplay();   
  display.setTextSize(1); 
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,8);
  display.println("Estimations:");
  display.println("UV Index => " + String(UV_index));
  display.println("Temp. => " + String(_temperature) + " *C");
  display.println("Pressure => " + String(_pressure) + " Pa");
  display.println("Altitude => " + String(_altitude) + " m");
  display.display();  
}

void get_UV_radiation(){
  int sensorValue;
  long sum = 0;
  // Get the summation of the latest UV sensor measurements.
  for(int i=0;i<1024;i++){
    sensorValue = analogRead(UV_pin);
    sum+=sensorValue;
    delay(2);
  }
  // Obtain the average sensor measurement to remove the glitch.
  long avr_val = sum/1024;
  // Estimate the UV index value with this formula roughly.
  UV_index = (avr_val*1000/4.3-83)/21;
  UV_index = UV_index / 1000;
  Serial.print("\nEstimated UV index value: "); Serial.println(UV_index);
  delay(20);
}

void collect_BMP180_data(){
  _temperature = bmp.readTemperature();
  _pressure = bmp.readPressure();
  // Calculate altitude assuming 'standard' barometric pressure of 1013.25 millibars (101325 Pascals).
  _altitude = bmp.readAltitude();
  _sea_level_pressure = bmp.readSealevelPressure();
  // To get a more precise altitude measurement, use the current sea level pressure, which will vary with the weather conditions. 
  _real_altitude = bmp.readAltitude(101500);
  // Print the data generated by the BMP180 Barometric Pressure/Temperature/Altitude Sensor.
  Serial.print("Temperature => "); Serial.print(_temperature); Serial.println(" *C");
  Serial.print("Pressure => "); Serial.print(_pressure); Serial.println(" Pa");
  Serial.print("Altitude => "); Serial.print(_altitude); Serial.println(" meters");
  Serial.print("Pressure at sea level (calculated) => "); Serial.print(_sea_level_pressure); Serial.println(" Pa");
  Serial.print("Real Altitude => "); Serial.print(_real_altitude); Serial.println(" meters\n");
}

void err_msg(){
  // Show the error message on the SSD1306 screen.
  adjustColor(255, 0, 0);
  display.clearDisplay();   
  display.drawBitmap(48, 0, _error, 32, 32, SSD1306_WHITE);
  display.setTextSize(1); 
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0,40); 
  display.println("Check the serial monitor to see the error!");
  display.display();  
}

void blePeripheralConnectHandler(BLEDevice central) {
  // Central connected event handler:
  Serial.print("Connected event, central: ");
  Serial.println(central.address());
}

void blePeripheralDisconnectHandler(BLEDevice central) {
  // Central disconnected event handler:
  Serial.print("Disconnected event, central: ");
  Serial.println(central.address());
}

void adjustColor(int r, int g, int b){
  analogWrite(redPin, (255-r));
  analogWrite(greenPin, (255-g));
  analogWrite(bluePin, (255-b));
}

void ei_printf(const char *format, ...){
  static char print_buf[1024] = { 0 };
  va_list args;
  va_start(args, format);
  int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
  va_end(args);
  if(r > 0){ Serial.write(print_buf); }
}


process_dataset.py

Download



# BLE AI-driven Smartwatch Detecting Potential Sun Damage w/ Edge Impulse
#
# Windows, Linux, or Ubuntu
#
# By Kutluhan Aktar
#
# Log UV & weather data on an SD card to train an Edge Impulse model. Then, run it to get informed of sun damage over BLE via an Android app.
# 
#
# For more information:
# https://www.theamplituhedron.com/projects/BLE_AI_driven_Smartwatch_Detecting_Potential_Sun_Damage_w_Edge_Impulse

import numpy as np
import pandas as pd
from csv import writer

# Create a class to modify the given data set so as to upload properly formatted samples to Edge Impulse.
class process_dataset:
    def __init__(self, csv_path):
        # Read the data set from the given CSV file.
        self.df = pd.read_csv(csv_path)
        # Define the class (label) names.
        self.class_names = ["Tolerable", "Risky", "Perilous"]
    # Scale (normalize) data to define appropriately formatted inputs.
    def scale_data_elements(self):
        self.df["scaled_uv_index"] = self.df["uv_index"] / 10
        self.df["scaled_temperature"] = self.df["temperature"] / 100
        self.df["scaled_pressure"] = self.df["pressure"] / 100000
        self.df["scaled_altitude"] = self.df["altitude"] / 100
        print("Data Elements Scaled Successfully!")
    # Split the data set to generate a separate CSV file for each sample.     
    def split_dataset_by_labels(self, class_number):
        l = len(self.df)
        sample_number = 0
        # Split the data set according to sun damage risk levels (classes):
        for i in range(l):
            # Add the header as the first row:
            processed_data = [["uv_index","temperature","pressure","altitude"]]
            if (self.df["risk_level"][i] == class_number):
                row = [self.df["scaled_uv_index"][i], self.df["scaled_temperature"][i], self.df["scaled_pressure"][i], self.df["scaled_altitude"][i]]
                processed_data.append(row)
                # Increase the sample number for each sample:   
                sample_number+=1   
                # Create a CSV file for each sample identified with the sample number.
                filename = "{}.sample_{}.csv".format(self.class_names[class_number], sample_number)
                with open(filename, "a", newline="") as f:
                    for r in range(len(processed_data)):
                        writer(f).writerow(processed_data[r])
                    f.close()
                print("CSV File Successfully Created: " + filename)
        
# Define a new class object named 'dataset':
dataset = process_dataset("UV_DATA.CSV")

# Scale data and generate a separate CSV file for each sample:
dataset.scale_data_elements()
for c in range(len(dataset.class_names)):
    dataset.split_dataset_by_labels(c)
            

Schematics

project-image
Schematic - 78.1


project-image
Schematic - 78.2

Downloads

BLE_Smartwatch_Case_v1.stl

Download


BLE_Smartwatch_Case_Sliding_Cover_v1.stl

Download


Edge Impulse Model (Arduino Library)

Download


BLE_UV_Smartwatch.aia

Download