Connecting to PC, programming of an app
This week's worked focused on developing an app in PC and connecting it to our devices. My plan was to develop an app for my iPhone, that would be able to control my bracelet device. That means I have to learn Swift and how to publish an app for my Apple device. Spoiler alert, as an app development was something completely new to me, with addition to never work with swift language or BLE (Bluetooth Low energy) communication and time pressure regarding my Bachelor thesis, I wasn't able to get the app work in time.
To have at least some outcome of this week, I have decided to look on the internet and try to find different method to build an app for Apple device. I have stumbled uppon this video that uses App development platform called Capacitor and developed my app using this tutorial. It is kind of cheating, because the platform uses web interface to design the GUI and JavaScript for the app logic, which is something we have done previous week.
The cpp code can be seen below
#include "I2Cdev.h"
#include "MPU6050.h"
#include
#include
#include
#include
#include
#include
// Display
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C // I2C address for SSD1306
// Frequency detection
#define SAMPLE_RATE 100
#define MAX_THRESHOLD 20384
#define MIN_THRESHOLD 12384
#define MA_WINDOW_SIZE 10
#define PEAK_WINDOW_SIZE 10
#define CLEARANCE_DELAY 2000
// BLE
BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
BLECharacteristic* pCharacteristic_2 = NULL;
BLEDescriptor *pDescr;
BLE2902 *pBLE2902;
bool deviceConnected = false;
bool oldDeviceConnected = false;
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "9d7a19b5-33f4-4283-8036-f0af024c0dd6"
#define CHAR1_UUID "2288b759-4967-48dc-abcc-91e194acf8f6"
#define CHAR2_UUID "2698f81e-a4e7-4707-98ff-64d62fc8cb07"
// Variable to store the HTTP request
String header;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
MPU6050 mpu; // MPU6050 address is 0x68
hw_timer_t *timer = NULL;
volatile bool flag = false;
int MOTOR_PIN = D0; // PWM pin for motor control
int16_t ax, ay, az;
bool blinkState;
// MPU6050 offsets
int16_t ax_offset = -3107;
int16_t ay_offset = 1885;
int16_t az_offset = 837;
uint16_t currAccel;
uint16_t maBuffer[MA_WINDOW_SIZE];
unsigned int maIndex = 0;
uint32_t maSum = 0;
unsigned long peakBuffer[PEAK_WINDOW_SIZE] = {0};
unsigned int peakIndex = 0;
unsigned long periodSum = 0;
unsigned long currTimeDiff = 0;
unsigned long lastTime = 0;
uint16_t prevAvg = 0;
uint16_t currAvg = 0;
volatile bool highPeakFlag = false;
volatile bool computeAvg = false;
float currentFreq = 0.0;
float goalFreq = 0.0;
void IRAM_ATTR onTimer() {
flag = true;
}
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
class CharacteristicCallBack: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pChar) override {
std::string value = pChar->getValue();
if (value.length() == 4) { // float32 is exactly 4 bytes
memcpy(&goalFreq, value.data(), sizeof(float)); // copy bytes into float
Serial.println(goalFreq, 4); // show 4 decimals
}
}
};
void setup() {
/*--Start I2C interface--*/
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
Wire.begin();
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
Fastwire::setup(400, true);
#endif
Serial.begin(115200);
delay(5000);
/*Initialize device and check connection*/
Serial.println("Initializing MPU...");
mpu.initialize();
Serial.println("Testing MPU6050 connection...");
if(mpu.testConnection() == false){
Serial.println("MPU6050 connection failed");
while(true);
}
else{
Serial.println("MPU6050 connection successful");
}
mpu.setDLPFMode(5);
Wire.begin(6, 7); // For Seeed XIAO ESP32
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
while (true);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Display Ready!");
display.display();
timer = timerBegin(0, 80, true);
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 10000, true); // 100 Hz sampling
timerAlarmEnable(timer);
/* Setting accelerometer offsets */
Serial.println("Updating internal sensor offsets...\n");
mpu.setXAccelOffset(ax_offset);
mpu.setYAccelOffset(ay_offset);
mpu.setZAccelOffset(az_offset);
/*Configure board LED pin for output*/
pinMode(LED_BUILTIN, OUTPUT);
pinMode(MOTOR_PIN, OUTPUT);
// Fill sample buffer
for (int i = 0; i < MA_WINDOW_SIZE; i++) {
mpu.getAcceleration(&ax, &ay, &az);
currAccel = sqrt(ax * ax + ay * ay + az * az);
maBuffer[i] = currAccel;
maSum += currAccel;
delay(10);
}
prevAvg = maSum / MA_WINDOW_SIZE;
// Create the BLE Device
BLEDevice::init("ESP32");
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// Create the BLE Service
BLEService *pService = pServer->createService(SERVICE_UUID);
// Create a BLE Characteristic
pCharacteristic = pService->createCharacteristic(
CHAR1_UUID,
BLECharacteristic::PROPERTY_NOTIFY
);
pCharacteristic_2 = pService->createCharacteristic(
CHAR2_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
// Create a BLE Descriptor
pDescr = new BLEDescriptor((uint16_t)0x2901);
pDescr->setValue("A very interesting variable");
pCharacteristic->addDescriptor(pDescr);
pBLE2902 = new BLE2902();
pBLE2902->setNotifications(true);
// Add all Descriptors here
pCharacteristic->addDescriptor(pBLE2902);
pCharacteristic_2->addDescriptor(new BLE2902());
// After defining the desriptors, set the callback functions
pCharacteristic_2->setCallbacks(new CharacteristicCallBack());
// Start the service
pService->start();
// Start advertising
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(false);
pAdvertising->setMinPreferred(0x0); // set value to 0x00 to not advertise this parameter
BLEDevice::startAdvertising();
Serial.println("Waiting a client connection to notify...");
}
void loop() {
if (flag) {
flag = false;
unsigned long currentMillis = millis();
mpu.getAcceleration(&ax, &ay, &az);
currAccel = sqrt(ax * ax + ay * ay + az * az);
maSum = maSum - maBuffer[maIndex] + currAccel;
maBuffer[maIndex] = currAccel;
maIndex = (maIndex + 1) % MA_WINDOW_SIZE;
currAvg = maSum / MA_WINDOW_SIZE;
static int lastSlope = 0;
int slope = currAvg - prevAvg;
/* Detect peaks */
if (lastSlope > 0 && slope < 0 && prevAvg > MAX_THRESHOLD && !highPeakFlag) {
highPeakFlag = true;
currTimeDiff = currentMillis - lastTime;
periodSum = periodSum - peakBuffer[peakIndex] + currTimeDiff;
peakBuffer[peakIndex] = currTimeDiff;
if ((peakIndex + 1) % PEAK_WINDOW_SIZE == 0) {
computeAvg = true;
}
peakIndex = (peakIndex + 1) % PEAK_WINDOW_SIZE;
lastTime = currentMillis;
} else if (lastSlope < 0 && slope > 0 && prevAvg < MIN_THRESHOLD && highPeakFlag) {
highPeakFlag = false;
currTimeDiff = currentMillis - lastTime;
periodSum = periodSum - peakBuffer[peakIndex] + currTimeDiff;
peakBuffer[peakIndex] = currTimeDiff;
if ((peakIndex + 1) % PEAK_WINDOW_SIZE == 0) {
computeAvg = true;
}
peakIndex = (peakIndex + 1) % PEAK_WINDOW_SIZE;
lastTime = currentMillis;
}
/* Clear buffer when no peaks detected for CLEARANCE_DELAY ms */
if (currentMillis - lastTime > CLEARANCE_DELAY) {
computeAvg = false;
periodSum = 0;
memset(peakBuffer, 0, sizeof(peakBuffer));
peakIndex = 0;
}
/* Compute frequency */
if (computeAvg) {
uint32_t periodAvg = (2 * periodSum) / PEAK_WINDOW_SIZE;
currentFreq = 1000.0 / periodAvg;
Serial.println("Period sum: " + String(periodSum) + "\t" + "Period avg: " + String(periodAvg) + "\t" + "Frequency: " + String(currentFreq));
} else {
currentFreq = 0.0;
}
lastSlope = slope;
prevAvg = currAvg;
float freqDiff = abs(currentFreq - goalFreq);
int pwmVal;
if (currentFreq == 0.0) {
pwmVal = 0;
} else {
if (freqDiff > 0.05) {
pwmVal = (int)((freqDiff - 0.05) * (255.0-140.0) / (2.0 - 0.05) + 140.0);
//pwmVal = (int)((currentFreq - 1.2) * (255.0-140.0) / (3.0 - 1.2) + 140.0);
}
else {
pwmVal = 0;
}
}
if (pwmVal > 255) {
pwmVal = 255;
} else if (pwmVal < 140 && pwmVal > 0) {
pwmVal = 140;
}else if (pwmVal < 0) {
pwmVal = 0;
}
/* Write on display */
display.clearDisplay();
display.setCursor(0, 0);
display.printf("Running frequency:\n");
display.printf("%.2f steps per second\n", currentFreq);
//display.printf("PWM %d\n", pwmVal);
display.printf("Goal frequency:\n");
display.printf("%.2f steps per second\n", goalFreq);
display.display();
analogWrite(MOTOR_PIN, pwmVal);
if (deviceConnected) {
pCharacteristic->setValue((uint8_t*)¤tFreq, sizeof(currentFreq));
pCharacteristic->notify();
}
}
}
The communication on the end of an app is done through JavaScript
import { BleClient } from '@capacitor-community/bluetooth-le'
import { SplashScreen } from '@capacitor/splash-screen';
SplashScreen.hide();
export async function scan() {
try {
await BleClient.initialize();
await BleClient.requestLEScan({
}, (result) =>{
console.log('Scan result:', result);
});
setTimeout(async() => {
await BleClient.stopLEScan();
console.log('Scan stopped');
}
, 5000); // Stop scanning after 5 seconds
} catch (error) {
console.error('Error during scan:', error);
}
}
let deviceObject;
export async function connect() {
try {
await BleClient.initialize();
const device = await BleClient.requestDevice({
services: ["9d7a19b5-33f4-4283-8036-f0af024c0dd6"],
})
await BleClient.connect(device.deviceId, (deviceId) => onDisconnect(deviceId));
console.log('Connected to device:', device);
deviceObject = device;
} catch (error) {
console.error('Error during scan:', error);
}
}
const char1Value = document.getElementById('char1');
async function startListen() {
await BleClient.startNotifications(
deviceObject.deviceId,
"9d7a19b5-33f4-4283-8036-f0af024c0dd6",
"2288b759-4967-48dc-abcc-91e194acf8f6",
(value) => {
const freq = value.getFloat32(0, true); // <- read as Float32, little-endian
console.log('Notification received:', freq);
char1Value.innerHTML = freq.toFixed(2); // show two decimal places
}
);
}
let sliderValue;
const slider = document.getElementById('slider1');
const slider1Reading = document.getElementById('slider1-reading');
slider1Reading.innerHTML = slider.value;
async function writeData(value) {
const bufferSize = 4; // 4 bytes for Float32
const buffer = new ArrayBuffer(bufferSize);
const dataView = new DataView(buffer);
dataView.setFloat32(0, value, true); // Write float32 at offset 0, Little Endian
await BleClient.write(
deviceObject.deviceId,
"9d7a19b5-33f4-4283-8036-f0af024c0dd6",
"2698f81e-a4e7-4707-98ff-64d62fc8cb07",
dataView
);
}
slider1.addEventListener('input', function() {
slider1Reading.innerHTML = this.value;
sliderValue = parseFloat(this.value);
});
function onDisconnect(deviceId) {
console.log('Disconnected from device:', deviceId);
}
const button1 = document.getElementById('button1');
button1.addEventListener('click', () => {
connect();
});
const button2 = document.getElementById('button2');
button2.addEventListener('click', () => {
startListen();
});
const button3 = document.getElementById('button3');
button3.addEventListener('click', () => {
writeData(sliderValue)
});
The GUI itself isn't anything special as it works based on html and css.
To prevent writing this week off as a failure, I have learning a simple new way to develop an app for both Apple and Android device. I learned more about Bluetooth communication protocol, implemented the two way communication between my ESP32 and iPhone, making it easy, fast and errorless. I have learned how to upload an app on my iPhone, making it easier for future work. The app interface can be seen here
Plans for the future
I have not thrown away an idea of making the app using the swift language. I want the bracelet to record data of the run (time and running frequency) on the SD card and be able to see the data as graphs in my app. Wait for the follow up.