My projects consists of two bracelets that can be worn everyday, called the WonderCuffs. Inspired by Wonder Woman's bracelets, they embark some concealed tech to help their wearer manage and mitigate some symptoms of menopause :
- Thanks to a NFC chip, approaching your phone to the left bracelet launches a private VR website that will offer you some guided breathing exercises following an anxiety reducing pattern in case of a panic attack.These exercises include visuals synced to your own breathing thanks to a capacitive fabric sensor, so that you can follow the prompts more easily.
- The right cuff is meant to help directly with one the most common and well-known symptom of menopause : hot flashes. Thanks to a peltier module, the cuff generates some cold that you can apply on your wrists or the inside of your elbow, a known hack to feel cooler rapidly and efficiently.
I hope that the combined effects of these three possibles actions (with the breathing exercises proposed in a short-term and a long-term format) will help relieve some of the constant discomfort and stigma that seems to be associated with menopause.
The main component I used for the left cuff is the Espruino puck.js : this device can be programmed using Javascript using a web IDE, and embarks a Bluetooth Low Energy (BLE) module, two LEDs, a push button, a programmable NFC chip and some other sensors I will not be using today (magnometer and ambiant light sensor) along with some GPIO pins, one of which has capacitive touch capabilities (we'll see in a moment why this is helpful).
When experiencing a panic attack (panic disorders being one of the many symptoms associated with menopause), the user can click the button on the cuff : this changes the NFC broadcast URL to one that will lead them to a virtual reality web application that will take them through a guided breathing exercise.
I chose to use VR for its immersive abilities : studies have shown that the sense of presence offered by this media surpasses that of other formats (videos, recordings, etc.) and make it an excellent choice for healthcare.
As congruent visual, auditory and tactile cues help enhance the illusion of presence offered by virtual reality, I've done my best to add such elements in the application : the user is immersed in a bright-colored world, and a yellow levitating sphere indicates them when to breathe in or out, following classic patterns found in guided respiration exercises.
In order to make the web app even more immersive, I've added a conductive fabric to the lining of the cuff, hooked to the capacitive sense input of the board : when the VR app is connected to the cuff (via web bluetooth), the user can press on this part of the cuff when breathing in, and release the pressure when breathing out. A second, orange colored sphere will then follow the rythm of their breathing, allowing them to match it to the yellow one, further enhancing the experience.
My main concern being the ease of use of the product, rather than having the user create an account, their settings in the app are associated with the unique ID of the NCF chip : this allows me to associate each setting file to a specific user without having to add an authentification layer for them to go through. Moreover, relying on the ID of the chip allows the data to remain completely anonymous as there is no way to identify the user.
My main objective was to make the cuff as easy as possible to use, given the state of distress one can be in when experiencing a panic attack.
As such, the web app can be used with or without using the touch sensor (in which case the user just needs to follow the cues of the yellow sphere), but it also works with or without VR goggles : when launched, the web app displays a regular (non stereolithic) view, allowing the user to switch to VR only if they want to, by pressing the VR button on the bottom right of the scene.
Step 1 : programming the cuff
The first step in making the left cuff was to program the puck.js to make it conform to what I've described earlier. This is done using the IDE offered here https://www.espruino.com/ide/ which allows you to enter some Javascript code that you can then send to the Puck via web bluetooth.
The code I'm using is available on the project repo at the bottom : it starts by setting the regular URL broadcast by the NFC chip, appending the last digits of the NFC address as a parameter so that the web app can take that into account to identify the user.
// setting the default URL that leads to the symptom tracking app with the unique ID of the user
var adress = "https://tracking-menopause.glitch.me?ID=" + NRF.getAddress().substr(-5);
NRF.nfcURL(adress);
It then sets a listener on the button that changes the URL to that of the VR app using the same principe to identify the user.
// Whenever the button on the left cuff is pressed, the URL changes so that it leads the user to the controlled breathing exercise
setWatch(function() {
adress = "https://hacking-menopause.glitch.me?ID=" + NRF.getAddress().substr(-5);
NRF.nfcURL(adress);
}, BTN, { repeat:true, edge:"rising", debounce: 50 });
We then set up the BLE service that will be advertised, using custom UUIDs since this is not a standard BLE service
// Setting up the BLE Service and characteristic that will later be used for the breathing exercise
NRF.setServices({
0xBCDE : {
0xABCD : {
value : "Hello",
maxLen : 20,
notify: true,
readable : true,
}
}
});
Finally, the two callbacks at the bottom of the code set up and clear an interval timer that will make the Puck publish the value of the capacitive sensor using a Bluetooth LE service characteristic every 500 ms whenever the device is connected via Bluetooth. This allows me to preserve the battery life of the device and updating the value only when the cuff is being used in the VR app.
// This function reads the value of the capacitive touch sensor and update the BLE characteristic accordingly
function sendReading() {
LED1.write(toggle);
toggle = !toggle ;
let data = Puck.light();
string = Math.floor(data*100) + "%";
console.log(string);
NRF.updateServices({
0xBCDE : {
0xABCD : {
value : string,
notify: true
}
}
});
}
// Whenever connected to the user's mobile phone via Bluetooth, we set an interval timer so that the sendReading function is executed regularly
NRF.on('connect', function() {myInterval = setInterval(sendReading, 500);});
// In order to save battery life, we clear the interval timer when the device has disconnected
NRF.on('disconnect', function() { clearInterval(myInterval);
LED1.write(false);
});
Now that the hardware part is taken care of, let's switch to the VR content !
Step 2 : Building the VR app
In order to build the VR web application, I used a framework call A-Frame which allows you to build rich VR content using mostly HTML : https://aframe.io/
Using A-Frame means that the content can be made compatible with a variety of smartphone-based VR platforms, some of which might include dedicated controlers (like the Google Daydream) while some will have to rely on gaze-based interactions (like most Google cardboard variants). It also means that the app should be compatible with more advanced VR devices (such as the HTC Vive line or the Oculus line) and controlers, although these can not launch the app from the NFC chip as far as I know.
As using the web bluetooth API of the browsers is only allowed for websites served via HTTPS, I used the Glitch platform which allows you to build an app online with instant deployment and a customizable URL to host both my apps.
A-Frame follows an entity-component-system principle, so I used the primitive components as well as a few custom ones to add the behaviour I wanted to my scene. Again, the code is available on the repo :
The header is where we include the A-Frame library and the components we're using, plus the file we're going to create for communicate with the Puck.js (bluetooth-comm.js)
<html>
<head>
<script src="https://aframe.io/releases/1.0.0/aframe.min.js"></script>
<script src="https://unpkg.com/aframe-animation-timeline-component@2.0.0/dist/aframe-animation-timeline-component.min.js"></script>
<script src="https://unpkg.com/aframe-environment-component@1.1.0/dist/aframe-environment-component.min.js"></script>
<script src="https://unpkg.com/aframe-event-set-component@5.0.0/dist/aframe-event-set-component.min.js"></script>
<link rel="stylesheet" href="/style.css"></link>
<script src="bluetooth-comm.js"></script>
</head>
The body starts with a button that we'll use to connect to the Puck. After that, we set up the A-Frame content : we declare one A-Scene, and a number of assets (audio, images) that will be used in our scene
<body>
<div id="overlay">
<button id="button" class="connect">CONNECT</button>
</div>
<a-scene
animation-timeline__1="timeline:#boatTimeline; loop:true"
loading-screen="dotsColor: red; backgroundColor: black"
>
<a-assets>
<audio
id="click-sound"
crossorigin="anonymous"
src="https://cdn.aframe.io/360-image-gallery-boilerplate/audio/click.ogg"
></audio>
<a-timeline id="boatTimeline">
<a-timeline-animation
select="#sphere"
name="goup"
></a-timeline-animation>
<a-timeline-animation
select="#sphere"
name="godown"
></a-timeline-animation>
</a-timeline>
</a-assets>
We have to setup a camera and make it act as a cursor for our user
<a-entity camera look-controls position="0 1.6 0">
<a-entity
cursor="fuse: true; fuseTimeout: 1500"
raycaster="objects: .clickable"
position="0 0 -1"
geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03"
material="color: black; shader: flat"
animation__click="property: scale; from: 0.1 0.1 0.1; to: 1 1 1; easing: easeInCubic; dur: 150; startEvents: click"
animation__clickreset="property: scale; to: 0.1 0.1 0.1; dur: 1; startEvents: animationcomplete__click"
animation__fusing="property: scale; from: 1 1 1; to: 0.1 0.1 0.1; easing: easeInCubic; dur: 150; startEvents: fusing"
>
</a-entity>
</a-entity>
<a-entity
class="link clickable"
geometry="primitive: plane; height: 1; width: 1"
material="shader: flat; color: #ffffff"
position="-3.5 5 -5"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #settings; _delay: 300; visible: true"
></a-entity>
Onto the scene itself : it starts with an environment so that the content is not in the middle of a 3D void as can happen sometimes in VR.
<a-entity
environment="preset:default ;
skyType: gradient;
skyColor: #ff40ff;
horizonColor: #ff4000;
seed: 17;
lighting: distant;
fog: 0.32;
ground: noise;
groundYScale: 0.81;
groundTexture: walkernoise;
groundColor: #ae3241;
groundColor2: #db4453;
dressing: cubes;
dressingAmount: 200;
dressingColor: #a9313d;
dressingScale: 7;
dressingUniformScale: true;
dressingVariance: '7 7 7';
gridColor: #239893"
></a-entity>
Last but not least, we position two spheres that will be used for the breathing exercise
<a-entity
id="sphere"
geometry="primitive: sphere; radius: 1"
material="color: yellow;"
position="-1 0.3 -5"
animation__goup="property: position;
from: -1 1.3 -5;
to: -1 5 -5;
dur: 2000;
easing: linear;
loop: false"
animation__godown="property: position;
from: -1 5 -5;
to: -1 1.3 -5;
dur: 3000;
easing: linear;
loop: false"
>
</a-entity>
<a-entity
id="mySphere"
geometry="primitive: sphere; radius: 1"
material="color: red;"
position="1 1.3 -5"
animation="property: position;
from: 1 1.3 -5;
to: 1 1.4 -5;
dur: 2000;
easing: linear;
loop: false"
>
</a-entity>
</a-scene>
</body>
</html>
In this configuration, the app displays :
- a yellow levitating sphere that the user can follow during the guided breathing exercise, breathing in when the sphere goes up and breathing out when it goes down. (id = sphere)
- a red levitating sphere with a default animation that we will then change depending on the data that we receive from the cuff.
Step 3 : Connecting the two together
Let's now have a look at bluetooth-comm.js , the file in charge of communication between the microcontroler and the web app
The first step is to declare a class that will take care of the BLE communications : connecting to the Puck (filtering by name), selecting the right service and characteristic according to the values we chose when programming the Puck, and subscribing to notification whenever that characteristic changes
class PuckConnector {
constructor() {
this.device = null;
this.onDisconnected = this.onDisconnected.bind(this);
}
request() {
console.log("request");
let options = {
filters: [
{
name: "Puck.js 064f"
}
],
optionalServices: ["0000bcde-0000-1000-8000-00805f9b34fb"]
};
return navigator.bluetooth.requestDevice(options).then(device => {
this.device = device;
this.device.addEventListener(
"gattserverdisconnected",
this.onDisconnected
);
});
}
connect() {
console.log("connect");
if (!this.device) {
return Promise.reject("Device is not connected.");
}
console.log("found device");
return this.device.gatt.connect();
}
writeColor(data) {
return this.device.gatt
.getPrimaryService("3e440001-f5bb-357d-719d-179272e4d4d9")
.then(service =>
service.getCharacteristic("0000abcd-0000-1000-8000-00805f9b34fb")
)
.then(characteristic => characteristic.writeValue(data));
}
startTempNotifications(listener) {
button.innerHTML = "DISCONNECT";
button.addEventListener("click", event => {
puckConnector.stopTempNotifications()
.then(_ => puckConnector.disconnect())
});
console.log("subscribing to notifs");
return this.device.gatt
.getPrimaryService("0000bcde-0000-1000-8000-00805f9b34fb")
.then(service =>
service.getCharacteristic("0000abcd-0000-1000-8000-00805f9b34fb")
)
.then(characteristic => characteristic.startNotifications())
.then(characteristic =>
characteristic.addEventListener("characteristicvaluechanged", listener)
);
}
stopTempNotifications(listener) {
return this.device.gatt
.getPrimaryService("0000bcde-0000-1000-8000-00805f9b34fb")
.then(service =>
service.getCharacteristic("0000abcd-0000-1000-8000-00805f9b34fb")
)
.then(characteristic => characteristic.stopNotifications())
.then(characteristic =>
characteristic.removeEventListener(
"characteristicvaluechanged",
listener
)
);
}
disconnect() {
if (!this.device) {
return Promise.reject("Device is not connected.");
}
return this.device.gatt.disconnect();
}
onDisconnected() {
console.log("Device is disconnected.");
}
}
Later in the file, we can then create a PuckConnector object and use the connect button to start the connection process (the BLE connection has to be initiated by the user and cannot be setup automatically)
var puckConnector = new PuckConnector();
document.addEventListener("DOMContentLoaded", function() {
console.log(AFRAME.utils.getUrlParameter('id'));
button = document.getElementById("button");
overlay = document.getElementById("overlay");
if ("bluetooth" in navigator === false) {
overlay.style.display = "none";
} else {
var prevClickState = false;
var angle = 0;
button.addEventListener("click", event => {
puckConnector
.request()
.then(_ => puckConnector.connect())
.then(_ => puckConnector.startTempNotifications(onTempChanged))
.catch(error => {
console.log(error);
});
});
}
});
As you can see we attach the function "onTempChanged" as a callback to use whenever the BLE characteristic changes : this function is in charge of retrieving the data and animating the red sphere accordingly
function onTempChanged(data) {
console.log("temp changed");
let enc = new TextDecoder("utf-8");
let receivedValue = enc.decode(data.currentTarget.value);
console.log(receivedValue);
//let color ='color:'+'#'+(0x1000000+(Math.random())*0xffffff).toString(16).substr(1,6)+''
let height = 1.3+((5 - 1.3) * parseInt(receivedValue)) / 100;
let position = "1 " + height + " -5";
let sphere = document.getElementById("mySphere");
let animation =
"property: position; to: 0" + position + " 0; loop: false; dur: 1000";
console.log("target " + animation);
sphere.setAttribute("animation", animation);
}
Step 4 : the cuff itself
Given our current quarantined state and the limited time, I opted for re-using an existing bracelet for this project. The problem that I encountered was that the only bracelet that was large enough to conceal the circuit was made of metal, which prevented the readings from the Puck.js from being accurate. I'm hoping to update this project as soon as possible with a custom made bracelet that will fix this.
I used some conductive thread to connect a piece of conductive fabric to the capacitive sense pin of the Puck.js : the fabric is folded in half so that pressing it while breathing in will cause the readings to go up, which will make the red sphere levitate higher.
Comments