Skip to content

Instantly share code, notes, and snippets.

@hebinzin
Last active April 18, 2023 13:34
Show Gist options
  • Save hebinzin/f1b2a7cdf4339a0ab698d5d5142f35eb to your computer and use it in GitHub Desktop.
Save hebinzin/f1b2a7cdf4339a0ab698d5d5142f35eb to your computer and use it in GitHub Desktop.
Nestor (bangle.js app) files for manual upload. Check the `README.md` file for instructions.

Nestor

Description:

Nestor is an alcohol consumption tracker and blood alcohol content estimator for the bangle.js 2 smartwatch.

Disclaimer:

The purpose of this application is not to support any kind of alcohol intake, or to provide qualified advice on this subject, but to be helpful to people who would like to get rough information from their own consumption.

While its calculation methods may be inspired by science, they might result in rather superficial data, providing only overall estimates so users might reflect on their alcohol intake and hopefully improve their habits to healthier ones.

It should never replace proper therapy.

Any health issues related to alcoholic habits must be treated by specialized professionals.

How to install:

While Bangle.js applications may commonly be installed through the Bangle.js App Loader, Nestor is not yet available there. It needs to be manually installed at the Espruino Web IDE, by following these steps:

  1. Follow this tutorial section to configure Bluetooth on your device.
  2. Follow this part to connect the device to the IDE through Bluetooth LE.
  3. At the vertical bar between the dark and light areas of the IDE, click the "Access files in device's storage" icon. A "device storage" dialog will open with a list of all the device's files and an "Upload files" area on top.
  4. Upload nestor.app.js, nestor.settings.js and nestor.img.
  5. At the dark area on the left of the IDE, run the following code:
require("Storage").write("nestor.info",{"id":"nestor","name":"Nestor","type":"app","src":"nestor.app.js","icon":"nestor.img","version":"0.0.1","tags":"tool,clock","files":"nestor.info,nestor.app.js,nestor.settings.js","data":"nestor.json",});
  1. Press and hold the bangle.js 2 button for a few seconds to reset the device.

Now the application might be accessed from the launcher.

How to use:

1. Setting up:

The users are supposed to use Nestor while in an alcoholic beverage drinking "session" (e.g., a party or meeting) so they can keep track of how many glasses they drank and what is (approximately) the alcohol level in their blood. The application should first be configured with some of the users' attributes, so it can make evaluations as accurate as possible. Let's do it, then:

  1. While at the clock screen, press the device button to unlock the device, and press it again to go to the launcher
  2. Go to the Settings app, and then to the Apps submenu.
  3. Select Nestor to configure Nestor's settings. You may set:
  • Biological sex as either Male or Female;
  • Height (metric) as your height in meters;
  • Weight (kg) as your weight in kilograms;
  • The Reset counter option will be ignored for now.
  1. Press the device's button three times to return to the clock screen.

The application is now configured and can therefore be run by following these steps:

  1. At the clock, unlock the screen and go to the launcher.
  2. Launch Nestor by selecting it from the list (it may vary according to the launcher used).

2. Interacting with the main screen:

At Nestor's main screen, you will see the widgets on top and a few other numerals on the main portion of the screen:

  • A clock on the upper half of the screen, showing the current time.
  • A number on the bottom left half of the screen, accounting for the number of drinks registered by the user.
  • A rational number on the bottom right half of the screen, with a percent sign, depicting the user's Blood Alcohol Content.

With the device unlocked, the user can slide the finger from the bottom to the top of the screen to launch a prompt, where he/she will have three options:

  1. Yes to add a "glass" to the drink counter.
  2. No to go back to the previous menu.
  3. .. to go to a "beverage menu", where the user can configure the beverage attributes.

3. Use case:

Let's say, for instance, that a user is drinking wine at dinner. It may have 120 ml served each time, with 12.5% of alcohol volume. Knowing that, he can then enter the beverage menu (by selecting the .. option at the prompt) and change Volume (ml) and Alcohol (%) to represent these values. The user can then add a drink to the counter, by going to the prompt again and choosing Yes. It's advised to do this just after a glass is finished.

After that, a smaller clock will appear at the center of the screen, presenting an estimate of the time by which the alcohol will be fully metabolized by the user's organism. If another drink is added before that, the time will increment. When the time is finally reached, the counter will reset to zero.

4. BAC information:

An important part of the application is the number that's displayed at the bottom right half of the screen, which represents the mass of alcohol per 100 mL of blood (the Blood Alcohol Content) of the user. It uses both Widmark and Nadler's formulae, thus accounting for the user's approximate Total Blood Volume, and the mass of alcohol the person drank.

As the user adds more glasses to the counter or drinks stronger (i.e., more alcoholic) beverages, this number grows. It will change its colors, giving a visual clue to the user's intoxication level (as referenced in this article). E.g., when it's displayed in red, it's recommended that the user stops drinking ASAP, as he/she's getting dangerously inebriated.

How this application works:

To provide its functionality, Nestor needs five files:

  1. nestor.info, which essentially has cues so that the device can "understand" how to display the application and which files it accesses.
  2. nestor.img, the icon that represents Nestor in the launcher.
  3. nestor.json, which stores all persistent attributes (variables) that provide the application functionality.
  4. nestor.settings.js, which adds a global settings screen and enables the user to change its attributes.
  5. nestor.app.js, which reads all the attributes, performs calculations from them, and displays the interface with those calculations' results.

To better understand how Nestor works, we'll take a look at the last three files:

nestor.json

A JavaScript Object Notation file, storing the following variables:

  • cooldown, for the time by when the user's blood alcohol content will be metabolized.
  • lastDrink, for the time when the user last registered a glass.
  • bio, for the user's biological sex, which modifies some formulae constants (changeable through the global app settings).
  • height, for the user's height in meters (changeable through the global app settings).
  • weight, for the user's weight in kilograms (changeable through the global app settings).
  • counter, for the drink glass counter (changeable via prompt and through the settings screen).
  • volume, for the volume (in mL) of each glass drank (changeable through the prompt's beverage menu).
  • ratio, for the beverage's alcohol percentage (changeable through the prompt's beverage menu).

nestor.settings.js

A JavaScript file, created (with the help of this guide) to allow users to change bio, height and weight values. It also grants the user the possibility to reset the drink counter if needed. It was created mostly to facilitate development testing.

nestor.app.js

It has JavaScript code that presents users with an interface that allows them to count and keep track of the number of glasses they drank in a "session", show their estimated BAC, and when - approximately - it will be metabolized, aside from the current time and their's widgets of choice.

To see more about this file, its variables and functions, check for the video demonstration further below.

Motivation:

Despite the many known harms that alcohol abuse can cause to people's health and society overall, its consumption is an meaningful aspect in many cultures. In lots of countries, it is common to gather couples, friends, and families while drinking alcoholic beverages, sometimes even as the meeting's main purpose. As consequentially there are many, many people bound to unhealthy habits related to alcohol intake, it's important to bring out information and tools with the potential to help people rethink those habits and pursue a more balanced lifestyle. In that context, Nestor is a minimal and simple effort to provide a tool to help alcohol consumers be more conscious about how much they are drinking and what might be consequences.

Design choices:

Being open source, useful, and cool as they are, bangle.js devices are, after all, microcontrollers. They have little storage and very limited RAM. That means they must be rather efficient to run without issues and allow users to harness the power of computing on their wrists, and so must be their applications' source code.

While I initially developed the application following coding style conventions that value more space, readability, and larger indentation, most were not designed for JavaScript, but for the C programming language. I adopted them because I was used to that kind of "look", mostly because of the CS50's Introduction to Computer Science course, from which this application emerged as a final project.

This style, however, does not pair well with the bangle.js, as it can lead to significant issues regarding the device's performance. Instead, there are specific guidelines for its applications' source code that should be followed for more efficient usage of resources.

Beyond the strict instructions regarding code, I opted to display a simplistic user interface, using only the device's built-in fonts and a few visual cues to aid the user to identify what's been displayed (i.e., a "glass" for the counter, a percent sign for the BAC, and basic colors to reinforce intoxication alerts). Apart from the launcher icon, everything is drawn by the application itself.

Improvements:

As of now, Nestor is already capable of doing what it claims, but of course, it can still be improved in many of ways. Apart from bug corrections, these are some considered changes that may enhance its functionality and/or performance:

  • Let users choose between unit systems (i.e., metric or imperial), so that they can set measures in units other than meters, liters, or grams.
  • Improve the UI readability, by making better use of space.
  • Add a visual clue as to how to access the prompt from the main screen.
  • Provide a way to access the application settings menu from the beverage menu.
  • Improve the way alcohol metabolism estimates are done (for instance, it could be achieved by registering BAC outside the source code, i.e., at nestor.json).
  • Decrease BAC as time goes on (for now, it stays the same until the counter is reset to zero).

The video linked above is a complement to this README.md file, in which I show some additional information, like:

  • A succinct introduction to myself;
  • A brief presentation of the bangle.js 2 smartwatch;
  • A basic demonstration of the Espruino Web IDE;
  • Instructions on how to install and use the Nestor app;

Acknowledgements:

As recommended by Espruino, this application is MIT-licensed. It doesn't include any kind of copyrighted, proprietary code, and it was designed from the ground up, obviously with the help from the glorious bangle.js 2 software reference and the internet. I'm also thankful for the Mozilla's JavaScript reference, which helped me a lot to get more familiar with the language (to the point that I might now even start to enjoy it, ha!).

While I was drafting Nestor, I stumbled upon several articles on the subject of consumption, metabolism, and alcohol related habits, and also a lot of stuff about bangle.js software throughout the Espruino documentation. Most of these resources - or at least those that were fundamentally relevant - are linked around this file. I also came across other applications that provided similar functionality to Nestor - one even for the bangle.js also! Yet, I strived to create the most original rendition of the idea that I could with my current skills. These are some of the applications I've found:

It's been very enjoyable to develop Nestor. I'll keep improving it and will surely create and contribute more with bangle.js stuff from now on. Espruino is a wonderful platform for technology, one of the very few initiatives to support a variety of programmable, open hardware. Bangle.js may not be polished as other vendors' proprietary smartwatches, but it shines in giving users both the openness, trust, and variety of software that only an open-source platform could give. And a community that keeps making it grow. Many thanks for that!

Last but not least, I have no words to thank CS50x, the course that brought me from zero programming knowledge to this. My deep gratitude to the people who makes it possible to exist.

const S = require("Storage");
const X = g.getWidth();
const Y = g.getHeight();
function save(object, key, value, file)
// Save an object's value to a file
{
object[key] = value;
S.writeJSON(file, object);
}
function drawUI()
// Display the user interface, accounting for variables
{
g.clear(reset);
// Display clock first, then set it's refresh rate
drawClock();
let clockRefresh = setInterval(drawClock, 60000);
// Read data from json file
let data = Object.assign({
bio: 1,
height: 1.68,
weight: 68,
counter: 0,
volume: 150,
ratio: 4.5,
}, S.readJSON('nestor.json', true) || {});
// Display counter
g.setFontAlign(0, 0).setFont("6x8", 3);
g.drawString(data.counter, X * 0.28, Y * 0.72, true);
// Set a regular check for the counter timeout
let counterRefresh = setInterval(clearCounter, 60000);
let bac = calcBAC(
calcABV(data.volume, data.ratio),
data.counter,
calcTBV(data.bio, data.height, data.weight)
);
drawEnd(inferEnd(bac, data.bio));
waitPrompt(eval(bac), clockRefresh, counterRefresh);
g.setFontAlign(0, 0).setFont("6x8", 3);
g.drawString(bac.toFixed(2).substring(1), X * 0.72, Y * 0.72, true);
g.drawString(' %', X * 0.72, Y * 0.86, true);
let glass = [
X * 0.09, Y * 0.59,
X * 0.16, Y * 0.93,
X * 0.36, Y * 0.93,
X * 0.43, Y * 0.59
];
g.drawPoly(glass);
Bangle.loadWidgets();
Bangle.drawWidgets();
}
function drawClock()
// Draw current time
{
g.reset();
let time = require('locale').time(new Date(), 1);
g.setFontAlign(0, 0).setFont("6x8", 4);
g.drawString(time, X * 0.5, Y * 0.3, true);
}
function clearCounter()
// Clears the counter at the time specified in scope
{
let scope = Object.assign({
counter: 0,
cooldown: Date.now()
}, S.readJSON('nestor.json', true) || {});
let localCounter = scope.counter;
if (Date.now() > scope.cooldown) {
localCounter = 0;
save(scope, 'counter', localCounter, 'nestor.json');
drawUI();
}
}
function calcABV(volume, ratio)
// Estimates the alcohol mass by volume in a beverage
{
return ((volume * ratio) / 100) * 0.79;
}
function calcTBV(isMale, h, w)
// Estimates the user's Total Blood Volume (based on Nadler's Formula)
{
if (isMale)
return 0.37 * (h * h * h) + 0.032 * w + 0.6;
else
return 0.36 * (h * h * h) + 0.033 * w + 0.18;
}
function calcBAC(abv, drinks, tbv)
// Returns the user "Blood Alcohol Content" (based on Widmark Formula)
{
return (abv * drinks) / (tbv * 100);
}
function inferEnd(bac, isMale)
// Anticipates the alcohol metabolization time
{
let scope = Object.assign({
cooldown: Date.now(),
lastDrink: Date.now()
}, S.readJSON('nestor.json', true) || {});
let rate = isMale ? 0.015 : 0.017;
let endTime = (3600000 * (bac / rate)) + scope.lastDrink;
save(scope, 'cooldown', endTime, 'nestor.json');
return endTime;
}
function drawEnd(timestamp)
// Display the approximate time by when the user's BAC will be processed
{
g.reset();
if (timestamp > Date.now()) {
let ClearOutTime = require('locale').time(new Date(timestamp), 1);
g.setFontAlign(0, 0).setFont("6x8", 2);
g.drawString(ClearOutTime, X * 0.5, Y * 0.48, true);
}
}
function eval(bac)
// Evaluates the situation to adjust the HUD accordingly
{
let conclusion;
if (bac > 0.159) {
g.setColor(1, 0, 0);
conclusion = 'You shouldn\'t go on. Count another?';
} else if (bac > 0.079) {
g.setColor(1, 1, 0);
conclusion = 'Be careful! Count another glass?';
} else if (bac > 0.039) {
g.setColor(0, 1, 0);
conclusion = 'Count one more drink?';
} else {
conclusion = 'Count up a drink?';
g.reset();
}
return conclusion;
}
function waitPrompt(text, id1, id2)
// Prompt to add a drink to the counter
{
let prompt = false;
Bangle.on('swipe', (directionLR, directionUD) => {
if (
directionUD === -1 &&
!Bangle.isLocked() &&
!prompt
) {
clearInterval(id1);
clearInterval(id2);
prompt = true;
let scope = Object.assign({
counter: 0,
lastDrink: Date.now()
}, S.readJSON('nestor.json', true) || {});
localCounter = scope.counter;
E.showPrompt(text, {
title: 'Nestor',
buttons: {
'Yes': 1,
'No': 0,
'..': -1
},
remove: () => {
drawUI();
}
}).then((v) => {
if (v > 0) {
localCounter++;
save(scope, 'counter', localCounter, 'nestor.json');
save(scope, 'lastDrink', Date.now(), 'nestor.json');
drawUI();
} else if (v < 0) {
bevMenu();
} else {
drawUI();
}
});
}
});
}
function bevMenu()
// Set and display menu to configure beverage attributes
{
let beverage = Object.assign({
volume: 150,
ratio: 4.5
}, S.readJSON('nestor.json', true) || {});
let bevAttributes = {
'': {
'title': 'Beverage'
},
'< Back': () => {
drawUI();
},
'Volume (ml)': {
value: beverage.volume,
min: 10,
max: 1000,
setp: 1,
onchange: v => save(beverage, 'volume', v, 'nestor.json')
},
'Alcohol (%)': {
value: beverage.ratio,
min: 0.5,
max: 70.0,
step: 0.5,
onchange: r => save(beverage, 'ratio', r, 'nestor.json')
},
};
E.showMenu(bevAttributes);
}
drawUI();
/*
* Run this code at the espruino IDE
* to generate `nestor.info` file in the
* device storage:
*/
require("Storage").write("nestor.info",{
"id":"nestor",
"name":"Nestor",
"type":"app",
"src":"nestor.app.js",
"icon":"nestor.img",
"version":"0.0.1",
"tags":"tool,clock",
"files":"nestor.info,nestor.app.js,nestor.settings.js",
"data":"nestor.json",
});
(function(back) {
const S = require('Storage');
let settings = Object.assign({
bio: 1,
height: 1.68,
weight: 68,
counter: 0
}, S.readJSON('nestor.json', true) || {});
function save(key, value) {
settings[key] = value;
S.writeJSON('nestor.json', settings);
}
const BIO = ['Female', 'Male'];
let confMenu = {
'': {
'title': 'Nestor'
},
'< Back': back,
'Biological sex': {
value: settings.bio,
min: 0,
max: 1,
format: b => BIO[b],
onchange: b => save('bio', b)
},
'Height (metric)': {
value: settings.height,
min: 0.55, // Chandra Bahadur Dangi
max: 2.72, // Robert Wadlow
step: 0.01,
onchange: h => save('height', h)
},
'Weight (kg)': {
value: settings.weight,
min: 2, // Lucía Zárate
max: 635, // Jon Brower Minnoch
step: 1,
onchange: w => save('weight', w)
},
'Reset Counter': () => {
//save('counter', 0);
E.showPrompt('Confirm erase counter?', {
title: 'Nestor',
buttons: {
'Yes': true,
'No': false
},
remove: () => {
load(__FILE__);
}
}).then((v) => {
if (v) save('counter', 0);
load(__FILE__);
});
}
};
E.showMenu(confMenu);
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment