Merge remote-tracking branch 'upstream/master'

master
nravanelli 2025-05-15 11:25:39 +08:00
commit e828476851
25 changed files with 926 additions and 306 deletions

View File

@ -34,3 +34,4 @@ clkinfo.addInteractive that would cause ReferenceError.
0.32: Make the border of the clock_info box extend all the way to the right of the screen. 0.32: Make the border of the clock_info box extend all the way to the right of the screen.
0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749) 0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749)
0.34: Support 12-hour time format 0.34: Support 12-hour time format
0.35: Adjust clock info positions to better fit long text

View File

@ -141,7 +141,7 @@ let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
var hideClkInfo = info.text == null; var hideClkInfo = info.text == null;
g.reset().setBgColor(g.theme.fg).clearRect(options.x, options.y, options.x+options.w, options.y+options.h); g.reset().setBgColor(g.theme.fg).clearRect(options.x, options.y, options.x+options.w, options.y+options.h);
g.setFontAlign(0,0).setColor(g.theme.bg); g.setFontAlign(-1,-1).setColor(g.theme.bg);
if (options.focus){ if (options.focus){
var y = hideClkInfo ? options.y+20 : options.y+2; var y = hideClkInfo ? options.y+20 : options.y+2;
@ -157,26 +157,36 @@ let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
return; return;
} }
// Set text and font // Set text and font, compute sizes.
var image = info.img; var image = info.img;
var imgWidth = image == null ? 0 : 24;
let imgWidthClear = parseInt(imgWidth*1.3);
var text = String(info.text); var text = String(info.text);
let strWidth;
if(text.split('\n').length > 1){ if(text.split('\n').length > 1){
g.setMiniFont(); g.setMiniFont();
strWidth = g.stringWidth(text);
} else { } else {
g.setSmallFont(); g.setSmallFont();
strWidth = g.stringWidth(text);
if (strWidth+imgWidthClear > options.w) {
g.setMiniFont();
text = g.wrapString(text, options.w-imgWidthClear).join("\n");
strWidth = g.stringWidth(text);
}
} }
// Compute sizes // Compute positions
var strWidth = g.stringWidth(text);
var imgWidth = image == null ? 0 : 24;
var midx = options.x+options.w/2; var midx = options.x+options.w/2;
let imgPosX = Math.max(midx-Math.floor(imgWidthClear/2)-parseInt(strWidth/2), 0);
let strPosX = imgPosX+imgWidthClear;
// Draw // Draw
if (image) { if (image) {
var scale = imgWidth / image.width; var scale = imgWidth / image.width;
g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale}); g.drawImage(image, imgPosX, options.y+6, {scale: scale});
} }
g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20); g.drawString(text, strPosX, options.y+6);
// In case we are in focus and the focus box changes (fullscreen yes/no) // In case we are in focus and the focus box changes (fullscreen yes/no)
// we draw the time again. Otherwise it could happen that a while line is // we draw the time again. Otherwise it could happen that a while line is

View File

@ -1,7 +1,7 @@
{ {
"id": "bwclk", "id": "bwclk",
"name": "BW Clock", "name": "BW Clock",
"version": "0.34", "version": "0.35",
"description": "A very minimalistic clock.", "description": "A very minimalistic clock.",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -13,3 +13,4 @@
0.13: Fixed Battery estimate Default to percentage and improved setting string 0.13: Fixed Battery estimate Default to percentage and improved setting string
0.14: Use `power_usage` module 0.14: Use `power_usage` module
0.15: Ring can now show hours, minute, or seconds hand, day/night left, or battery; Allowed for 12hr time; Ring now goes up in 5% increments; Step goal can be changed; The info that is set on the watchface will retain when leaving the face 0.15: Ring can now show hours, minute, or seconds hand, day/night left, or battery; Allowed for 12hr time; Ring now goes up in 5% increments; Step goal can be changed; The info that is set on the watchface will retain when leaving the face
0.16: Ring is now dynamically-created, rather than displaying pre-rendered rings; Seconds update every second; Ability to see Day ring; Settings options moved around to avoid popping of Steps option disappearing when not used; In Sun setting, ring is fully illuminated between during all of sunrise and sunset.

View File

@ -4,11 +4,10 @@
cyclic information line that includes, day, date, steps, battery, cyclic information line that includes, day, date, steps, battery,
sunrise and sunset times* sunrise and sunset times*
Written by: [Hugh Barney](https://github.com/hughbarney) For support Written by: [Hugh Barney](https://github.com/hughbarney) and [David Volovskiy](https://github.com/voloved)
and discussion please post in the [Bangle JS For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)
Forum](http://forum.espruino.com/microcosms/1424/)
* Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel) * Derived from [The Ring](https://banglejs.com/apps/?id=thering) proof of concept and the [Pastel clock](https://banglejs.com/apps/?q=pastel), though all rings are dynamically created.
* Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer * Includes the [Lazybones](https://banglejs.com/apps/?q=lazybones) Idle warning timer
* Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate, Battery Estimate) * Touch the top right/top left to cycle through the info display (Day, Date, Steps, Sunrise, Sunset, Heart Rate, Battery Estimate)
* The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle * The heart rate monitor is turned on only when Heart rate is selected and will take a few seconds to settle
@ -24,7 +23,8 @@ See [#1248](https://github.com/espruino/BangleApps/issues/1248)
* In the settings, the ring can be set to: * In the settings, the ring can be set to:
* Hours - Displays the ring as though it's the hour hand on an analog clock. * Hours - Displays the ring as though it's the hour hand on an analog clock.
* Minutes - Displays the ring as though it's the minute hand on an analog clock. * Minutes - Displays the ring as though it's the minute hand on an analog clock.
* Seconds - Displays the ring as though it's the seconds hand on an analog clock. * Seconds - Displays the ring as though it's the seconds hand on an analog clock. This option uses far more battery than any other option as it updates the screen 60 times more often.
* Day - Displays the ring as how much of the day is left. Functionally, it fills the ring half as quickly as the Hours option.
* Steps - Displays the ring as the amount of steps taken that day out of Step Target setting. * Steps - Displays the ring as the amount of steps taken that day out of Step Target setting.
* Battery - Displays the ring as the amount of battery percentage left. * Battery - Displays the ring as the amount of battery percentage left.
* Sun - Displays the ring as the amount of time that has passed from sunrise to sunset in the day and the amount of time between sunset and sunrise at night. * Sun - Displays the ring as the amount of time that has passed from sunrise to sunset in the day and the amount of time between sunset and sunrise at night.

View File

@ -1,12 +1,15 @@
var SunCalc = require("suncalc"); // from modules folder var SunCalc = require("suncalc"); // from modules folder
const storage = require('Storage'); const storage = require('Storage');
const widget_utils = require('widget_utils'); const widget_utils = require('widget_utils');
let settings = undefined;
let location = undefined;
const SETTINGS_FILE = "daisy.json"; const SETTINGS_FILE = "daisy.json";
const global_settings = storage.readJSON("setting.json", true) || {};
const LOCATION_FILE = "mylocation.json"; const LOCATION_FILE = "mylocation.json";
const h = g.getHeight(); const h = g.getHeight();
const w = g.getWidth(); const w = g.getWidth();
let settings; const rad = h/2;
let location; const hyp = Math.sqrt(Math.pow(rad, 2) + Math.pow(rad, 2));
// variable for controlling idle alert // variable for controlling idle alert
let lastStep = getTime(); let lastStep = getTime();
@ -19,8 +22,11 @@ let pal2; // palette for 50-100%
const infoLine = (3*h/4) - 6; const infoLine = (3*h/4) - 6;
const infoWidth = 56; const infoWidth = 56;
const infoHeight = 11; const infoHeight = 11;
const sec_update = 3000; // This ms between updates when the ring is in Seconds mode const ringEdge = 4;
const ringThick = 6;
let nextUpdateMs;
var drawingSteps = false; var drawingSteps = false;
var prevRing = {start: null, end: null, max: null};
function log_debug(o) { function log_debug(o) {
//print(o); //print(o);
@ -86,7 +92,7 @@ function loadSettings() {
settings.fg = settings.fg||'#0f0'; settings.fg = settings.fg||'#0f0';
settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check); settings.idle_check = (settings.idle_check === undefined ? true : settings.idle_check);
settings.batt_hours = (settings.batt_hours === undefined ? false : settings.batt_hours); settings.batt_hours = (settings.batt_hours === undefined ? false : settings.batt_hours);
settings.hr_12 = (settings.hr_12 === undefined ? false : settings.hr_12); settings.hr_12 = (global_settings["12hour"] === undefined ? false : global_settings["12hour"]);
settings.ring = settings.ring||'Steps'; settings.ring = settings.ring||'Steps';
settings.idxInfo = settings.idxInfo||0; settings.idxInfo = settings.idxInfo||0;
settings.step_target = settings.step_target||10000; settings.step_target = settings.step_target||10000;
@ -113,6 +119,7 @@ function extractTime(d){
var sunRise = "00:00"; var sunRise = "00:00";
var sunSet = "00:00"; var sunSet = "00:00";
var drawCount = 0; var drawCount = 0;
var night; // In terms of minutes
var sunStart; // In terms of ms var sunStart; // In terms of ms
var sunEnd; // In terms of minutes var sunEnd; // In terms of minutes
var sunFull; // In terms of ms var sunFull; // In terms of ms
@ -140,6 +147,7 @@ function updateSunRiseSunSet(now, lat, lon, sunLeftCalcs){
sunStart = times.sunset; sunStart = times.sunset;
sunFull = timesTmrw.sunrise - sunStart; sunFull = timesTmrw.sunrise - sunStart;
sunEnd = getMinutesFromDate(timesTmrw.sunrise); sunEnd = getMinutesFromDate(timesTmrw.sunrise);
night = getMinutesFromDate(timesTmrw.sunriseEnd);
} }
else { else {
sunLeft = dateCopy - times.sunrise; sunLeft = dateCopy - times.sunrise;
@ -150,12 +158,14 @@ function updateSunRiseSunSet(now, lat, lon, sunLeftCalcs){
sunStart = timesYest.sunset; sunStart = timesYest.sunset;
sunFull = times.sunrise - sunStart; sunFull = times.sunrise - sunStart;
sunEnd = getMinutesFromDate(times.sunrise); sunEnd = getMinutesFromDate(times.sunrise);
night = getMinutesFromDate(times.sunriseEnd);
} }
else { // We're in the middle of the day else { // We're in the middle of the day
isDaytime = true; isDaytime = true;
sunStart = times.sunrise; sunStart = times.sunriseEnd;
sunFull = times.sunset - sunStart; sunFull = times.sunsetStart - sunStart;
sunEnd = getMinutesFromDate(times.sunset); sunEnd = getMinutesFromDate(times.sunsetStart);
night = getMinutesFromDate(times.sunset);
} }
} }
} }
@ -252,48 +262,83 @@ function drawHrm() {
g.drawString(hrmCurrent, (w/2) + 10, infoLine); g.drawString(hrmCurrent, (w/2) + 10, infoLine);
} }
function draw() { function draw(drawRingOnly) {
if (!idle) if (!idle) {
if (drawRingOnly) {
drawGaugeImage(new Date());
}
else {
drawClock(); drawClock();
}
}
else else
drawIdle(); drawIdle();
queueDraw(); queueDraw();
} }
function drawGaugeImage(date) {
var hh = date.getHours();
var mm = date.getMinutes();
var ring_fill;
var invertRing = false;
var ring_max = 100;
switch (settings.ring) {
case 'Hours':
ring_fill = ((hh % 12) * 60) + mm;
ring_max = 720;
break;
case 'Minutes':
ring_fill = mm;
ring_max = 60;
break;
case 'Seconds':
ring_fill = date.getSeconds();
ring_max = 60;
break;
case 'Day':
ring_fill = (hh * 60) + mm;
ring_max = 1440;
break;
case 'Steps':
ring_fill = getSteps();
ring_max = settings.step_target;
break;
case 'Battery':
ring_fill = E.getBattery();
break;
case 'Sun':
var dayMin = getMinutesFromDate(date);
if (dayMin >= sunEnd && dayMin <= night) ring_fill = ring_max;
else {
ring_fill = ring_max * (date - sunStart) / sunFull;
if (ring_fill > ring_max) { // If we're now past a sunrise of sunset
updateSunRiseSunSet(date, location.lat, location.lon, true);
ring_fill = ring_max * (date - sunStart) / sunFull;
}
}
invertRing = !isDaytime;
break;
}
var start = 0;
var end = Math.round(ring_fill);
if (invertRing) {
start = ring_max - end;
end = ring_max;
}
if (end !== prevRing.end || start !== prevRing.start || ring_max !== prevRing.max) {
drawRing(start, end, ring_max);
prevRing.start = start;
prevRing.end = end;
prevRing.max = ring_max;
log_debug("Redrew ring at " + hh + ":" + mm);
}
log_debug("Start: "+ start + " end: " +end);
}
function drawClock() { function drawClock() {
var date = new Date(); var date = new Date();
var hh = date.getHours(); var hh = date.getHours();
var mm = date.getMinutes(); var mm = date.getMinutes();
var ring_percent;
var invertRing = false;
switch (settings.ring) {
case 'Hours':
ring_percent = Math.round((10*(((hh % 12) * 60) + mm))/72);
break;
case 'Minutes':
ring_percent = Math.round((10*mm)/6);
break;
case 'Seconds':
ring_percent = Math.round((10*date.getSeconds())/6);
break;
case 'Steps':
ring_percent = Math.round(100*(getSteps()/settings.step_target));
break;
case 'Battery':
ring_percent = E.getBattery();
break;
case 'Sun':
ring_percent = 100 * (date - sunStart) / sunFull;
if (ring_percent > 100) { // If we're now past a sunrise of sunset
updateSunRiseSunSet(date, location.lat, location.lon, true);
ring_percent = 100 * (date - sunStart) / sunFull;
}
// If we're exactly on the minute that the sun is setting/rising
if (getMinutesFromDate(date) == sunEnd) ring_percent = 100;
invertRing = !isDaytime;
break;
}
if (settings.hr_12) { if (settings.hr_12) {
hh = hh % 12; hh = hh % 12;
if (hh == 0) hh = 12; if (hh == 0) hh = 12;
@ -303,8 +348,8 @@ function drawClock() {
g.reset(); g.reset();
g.setColor(g.theme.bg); g.setColor(g.theme.bg);
g.fillRect(0, 0, w, h); g.fillEllipse(ringEdge+ringThick,ringEdge+ringThick,w-ringEdge-ringThick,h-ringEdge-ringThick); // Clears the text within the circle
g.drawImage(getGaugeImage(ring_percent, settings.ring, invertRing), 0, 0); drawGaugeImage(date);
setLargeFont(); setLargeFont();
g.setColor(settings.fg); g.setColor(settings.fg);
@ -318,8 +363,10 @@ function drawClock() {
drawInfo(); drawInfo();
// recalc sunrise / sunset every hour // recalc sunrise / sunset every hour
if (drawCount % 60 == 0) if (drawCount % 60 == 0) {
updateSunRiseSunSet(date, location.lat, location.lon, settings.ring == 'Sun'); let recalcSunLeft = (settings.ring == 'Sun');
updateSunRiseSunSet(date, location.lat, location.lon, recalcSunLeft);
}
drawCount++; drawCount++;
} }
@ -359,198 +406,63 @@ Bangle.on('HRM', function(hrm) {
///////////////// GAUGE images ///////////////////////////////////// ///////////////// GAUGE images /////////////////////////////////////
function addPoint(loc, max) {
// putting into 1 function like this, rather than individual variables var angle = ((2*Math.PI)/max) * loc;
// reduces ram usage from 70%-13% var x = hyp * Math.sin(angle);
function getGaugeImage(p, type, reverse) { var y = hyp * Math.cos(angle + Math.PI);
const endsDontShowList = ['Minutes', 'Seconds']; // Don't show non-5% increments with these ring types x += rad;
if (reverse) p = 100 - p; y += rad;
var endsDontShow = endsDontShowList.includes(type); return [Math.round(x),Math.round(y)];
// p0
if (p < 2 || (p < 5 && endsDontShow)) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLLLzWEABLgbVgohEGopYaiofDBihWVHJpYYDgYPbKx1ACJhYZIwT4OcAZWYHyRYUIgQXQH4RqOThCXUYRpCHNyQVVQQTwVQiSZWIQSEQNgSYSIYiEQQSyEUCQLDSOAyCnQiSCYQiSCYQiSCZDaDARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A"))
};
// p2
if (p < 5) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/ADNUFE8FqtVq2q1AqkFIIrDAAOAFMEBFQYrE1WgKsYrGLL4qFFY2pqDWeFZdUVkAhCAQMKFYdVLDUVFQYMHlWq0oMJKyoOJlQrCLDBWDB5clB5xWOoARMCARYWKwT4OgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI"))
};
// p5
if (p < 10) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZeUVkFUBQcKFYdVqArZioqDBg8qFYQMIKyoOJlWpBoJYYKwYPLlIPOKx1ACJgQCLCxWCawgAJgpYXKwY+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA="))
};
// p10
if (p < 15) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAOkQSdUFacK1WloCCSCaAAEFYKaQQSyEC0pvQirZTbomlIh6CYZAZFOQTBxDQhyCYOQhoPQS4bQHaBzaVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI"))
};
// p15
if (p < 20) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWAOpbRSucWAWVO5DaeZ5jaeJhgrBbTqkLbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A=="))
};
// p20
if (p < 25) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4AWgNVoAEGAERSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A="))
};
// p25
if (p < 30) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4AKgNVoAr/Ff4r/Ff4r/Ff4rNqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA="))
};
// p30
if (p < 35) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccFawkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI"))
};
// p35
if (p < 40) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIiorgjWqBI8FqtAFb1W1ArJbjz9BFZAKBbjxMBsALIFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5"))
};
// p40
if (p < 45) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjUBQR6EaiqCPQjVVQSATCqtUFSZvB1WACiSEUY4KCQQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A"))
};
// p45
if (p < 50) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal2 : pal1),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVqtW1WoFUgpBFYYABwApggIqDFYmq0BVjFYxZfFQorGLLrWCFZbgbVgtUBQcKLD8VFQYMHlQsDKzoOJFgZYYKwYPLFgZWaoARMLDJWCawgAJcAZWYCZ6FCLCkFFQNQCZ8CFYOoFaZWSLAmAQShWQLAiESQQRtTLAKESFQNUFacKQiSCCoArTgCESQSyEUirZTboyCnQiSCYQiSCYQiSCZQgeAVxwqYQgSwMVwNUFbMKWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbhdVFcTcHbT7cDFY0BbT7cD0ArxgtVoArfgGq1ArHFUDcBFY0VFceqFY1UFcMKFY1VFcmAFYtQFcMCFYsBFcugFYtAFcMAFYsFFcuoFYoqigEqFeEVFcuqFYlUFccKFYlVFc2AFYdQFccCFf4rbgNVoArjgGq0Ar/FbMFFc+oFYYqkgEqFf4r/FY0VqgrlhWqFf4r/Ff4rdqorowArBqArlgQr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlhQrCioroAYIr/Ff4r/FbcFqorllWoFf4r/FY9AFcmqFYUBFc+gFf4rZgFVqAqjgWqwAr/FbdUFccKFYkVFcwFDitVFccqFYkFFcuoFeNAFcWqFYkBFcugFYtQFUMCFYsAFcuAFYtUFcMKFY0VFcgHFitVFcMqFY0FFceoFY9AFcGqFY0BqtQFT8C1WgFeMAqtUFb8K1WAFY7cglQrIioriBI8FqtAFb2q1ArJbjzaBFZEBbj7aB0ALIFcLaHbkLaJFYbcd1QrKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrBqgqThQrBwAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMVFYNUCJsKFQOqFShYEoARMrRWXLAiFMiorCFSxYEFhQ6BFYJWXLAosIBgVWKzBYGcAsFBIdWKzIhGABI1EADArNoArcFhgqeWQwAEqAqeLJRVfcBLWdAH4A5A="))
};
// p50
if (p < 55) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAiFMQIRWWLAosKFQZWXLAosIFQZWYLAzgFawZWbAAMKFgmq1IoEAANUFTQABFZtAFbgsFFYwqeWQorFVjZZJFYhVfcAwrCazoA/AHI"))
};
// p55
if (p < 60) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAiEXKwKuNQjSCQQjSCQQjSCRAAIrB1AqTgorBoAUQQiyCSQgjdSbISCRQgZYSKwKCSQghYQKwSCSQghYQKwSCTAAMqFYOoCJsFFQNVFShYEwARMFQRWVLAmVQJxWWLAgcLFQZWXLAWpJJQqDKzBYC0ofDqjWHKzYhHABA1EADArNoArcFhgqegEBFRKsbLJxVfcBLWdAH4A5A=="))
};
// p60
if (p < 65) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WoFb8FqtAFY7cgiorIlQriBI8K1WAFb1VqgrJbjzaBFZECbj7aBqALIFcLaHbkLaJFYbcdqorKbjzaKbkDaLbgSwcVwLaJWD6uLFYawaVwIrMbgKwaVwLaKbgawaVwLaLbgawZQQLaLWDiuOWAaEYQQKuMWAelNBqCLVxqEC0oRPQS6EC0oSQQSyECFYKEVQSIABFYI/QAAcFFYJDRCgSCmYYjdSCqqYCLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A"))
};
// p65
if (p < 70) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZgGq0AqjgNVoAr/FbeoFccFFYkqFcwFDlWqFccVFYkKFctUFeOAFcVVFYkCFctQFYugFUMBFYsAFctAFYuoFcMFFY0qFcgHFlWqFcMVFY0KFcdUFY+AFcFVFY0C1WgFT8BqtQFeMA1WkFb8FqtAFY+VbUArIlVVFcIJHhI1IAC9VqiNJXI7aYFZAKKbS5MJFcKkJXRLafBYbcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A="))
};
// p70
if (p < 75) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rb1ArlgorClQroAYIr/Ff4r/FbcK1QrlitUFf4r/FY+AFclVFYUCFc9QFf4rZAgoAggNVoAr/FbdUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA="))
};
// p75
if (p < 80) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AFYegFccBFf4rbgWqwArjgFVqAr/FbMKFc9UFYYqkgEVFf4r/FY0q1ArlgtVFf4r/Ff4rd1QrooArB0ArlgIr/Ff4r/Ff4r/Ff4rOqtQFf4r/Ff4r/Ff4r/FZVUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5"))
};
// p80
if (p < 85) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AcIdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA="))
};
// p85
if (p < 90) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhEBtQrgbhEFrTacbhkFqzadbgQrIXRbcfqoribg5hJbjIrGXILlIbjIiGFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5"))
};
// p90
if (p < 95) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESquq1ArTgqESNgOqwArTIYKERH4KCUQigSBbKTdGCKKCVQiTCCFSyERCALBQQjAPBoArXDZ7ARObKuBSZwcaVzR0QFYKuZWAYNZWCJJKMoKuaWAahKBhiwTJRSudURorBFTgfMVzqjDO5DaeZ5jaeJhhiKbi4rIbT4hLqoriPI7afUpS5BbTwiKFdZgIADSmHFYIqgbgIrGcgIriEYwzHADZ7HRY4rdaYrjHADcBFYoGBFcgkEGQwAeFYqKHFbzUEcQ4AdiorwiorlEogxFAD59FWoorhoArDqArjgIr/FbYwFAEJSDFf4rXgornqgrDFUkAior/Ff4rGAYYAjKYYr/Ff4r/FbdVFdFAFYNQFcsBFf4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/FbdUFcsFFYUVFdADBFf4r/Ff4rbAYYAjKYYr/Ff4rFoArkqorCgIrnqAr/FbIEFAEBSFFf4rYqgrjgorEiormAocVAogAfEooxFFcB9EFdq1DAD9VFYkBFctQFYoGEADokHFcp8FRQoAdag7iFFb4HFioHGADYjHGY4rcPYyLHADbTHcYNQFT4iIFdZgIADKmJqrcgiorIBIIrhMKIAXUpIrBbjzaBFZAKKbS5MJFcKkJbj4fLBYLcdqorKbjzPMbjxKNMhauTURawdJJorBWDShBFZiRBWDQcOHRyuPOhorBWDIbPWDRzQSYKEYIwLLOHgSEXDIJyPQjD2SQjCCQQjSCRCYY/QN4xDRQiyCSQgjdSCqqECLCRWBYyiECISBWCYqgXCLCBWCQSYYEIhxqCeChYFThoQCKypYEIxgPPLB4cKFQZWXDoosIBhhYWcArWDKzYhHABA1EADArNoArcFhgqeWQysgLJxVfcBLWdAH4A5A"))
};
// p95
if (p < 98 || (p < 100 && endsDontShow)) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLsFFZrgbgNVFAeoGohYfiorDBhIACKzVVtQqIFgpYYDgVqB5xYXKwVVoARMLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=="))
};
// p98
if (p < 100) return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtV1WpqtUFUlVAAIrCAANAFMEBEoQrFqtQKsQrHLL4jEFY5ZdawIrMcDasEEIo1FLDUVD4YMUKyo5NLDAcDB7ZWOoARMLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHI"))
};
// p100
return {
width : 176, height : 176, bpp : 2,
transparent : -1,
palette : (reverse ? pal1 : pal2),
buffer : require("heatshrink").decompress(atob("AH4A/AH4ACgtVAAVUFUgpDAAdAFMEBFQ4ABqBVnLMQqLFjzWEABLgbVgohEGoqyaiofDBihWVHJpYYDgYPbKxz5NLDJGCfBzgDKzA+SLChECC6A/CNRycIS6jCNIQ5uSCqqCCeCqESTKxCCQiBsCTCRDEQiCCWQigSBYaRwGQU6ESQTCESQTCESQTIbQYCJzZVwKTODjSuaOiArBVzKwDBrKwRJJRlBVzSwDUJQMMWCZKKVzqiNFYIqcD5iudUYZ3IbTzPMbTxMMMRTcXFZDafEJdVFcR5HbT6lKXILaeERQrrMBAAaUw4rBFUDcBFYzkBFcQjGGY4AbPY6LHFbrTFcY4AbgIrFAwIrkEggyGADwrFRQ4reagjiHADsVFeEVFcolEGIoAfPoq1FFcNAFYdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA="))
};
} }
function polyArray(start, end, max) {
const eighth = max / 8;
if (start == end) return []; // No array to draw if the points are the same.
let startOrigin = start;
let endOrigin = end;
start %= max;
end %= max;
if(start == 0 && startOrigin != 0) start = max;
if(end == 0 && endOrigin != 0) end = max;
if (start > end) end += max;
var array = [g.getHeight()/2, g.getHeight()/2];
var pt = addPoint(start, max);
array.push(pt[0], pt[1]);
for (let i = start + eighth; i < end; i += eighth) {
pt = addPoint(i, max);
array.push(pt[0], pt[1]);
}
pt = addPoint(end, max);
array.push(pt[0], pt[1]);
log_debug("Poly Arr: " + array);
return array;
}
function drawRing(start, end, max) {
// Create persistent `buf` inside the function scope
if (!drawRing._buf) {
drawRing._buf = Graphics.createArrayBuffer(w, h, 2, { msb: true });
}
const buf = drawRing._buf;
let img = { width: w, height: h, transparent: 0,
bpp: 2, palette: pal1, buffer: buf.buffer };
buf.clear();
buf.setColor(1).fillEllipse(ringEdge,ringEdge,w-ringEdge,h-ringEdge);
buf.setColor(0).fillEllipse(ringEdge+ringThick,ringEdge+ringThick,w-ringEdge-ringThick,h-ringEdge-ringThick);
img.palette = pal2;
g.drawImage(img, 0, 0); // Draws a filled-in circle
if((end - start) >= max) return; // No need to add the unfilled circle
buf.clear();
buf.setColor(1).fillEllipse(ringEdge,ringEdge,w-ringEdge,h-ringEdge);
buf.setColor(0).fillEllipse(ringEdge+ringThick,ringEdge+ringThick,w-ringEdge-ringThick,h-ringEdge-ringThick);
buf.setColor(0).fillPoly(polyArray(start, end, max)); // Masks the filled-in part of the segment over the unfilled part
img.palette = pal1;
g.drawImage(img, 0, 0); // Draws the unfilled-in segment
return;
}
///////////////// IDLE TIMER ///////////////////////////////////// ///////////////// IDLE TIMER /////////////////////////////////////
function drawIdle() { function drawIdle() {
@ -607,7 +519,7 @@ function dismissPrompt() {
warned = false; warned = false;
lastStep = getTime(); lastStep = getTime();
Bangle.buzz(100); Bangle.buzz(100);
draw(); draw(false);
} }
var dismissBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", dismissPrompt, "Dismiss"); var dismissBtn = new BUTTON("big",0, 3*h/4 ,w, h/4, "#0ff", dismissPrompt, "Dismiss");
@ -682,6 +594,35 @@ function buzzer(n) {
}, 500); }, 500);
} }
function getDelayMs(prevDelayMs, ring_setting, now) {
// Much of the logic here is for slowing or speeding the delay on the seconds setting.
// returns [ms before next update, if only the ring should be updated]
const sec_batt = [20, 50];
const sec_delay = [10000, 2000, 1000];
const deadband = 5;
if (ring_setting == 'Seconds') {
const nearNextMinute = (now % 60000) >= (60000 - prevDelayMs);
if (nearNextMinute) {
let batt = E.getBattery();
for (let i = 0; i < sec_batt.length; i++) {
if (batt <= sec_batt[i])
return [sec_delay[i], false];
}
// Check for coming out of the above states w/ deadband
for (let i = 0; i < sec_batt.length; i++) {
if (prevDelayMs == sec_delay[i] && batt >= (sec_batt[i] + deadband))
return [sec_delay[i + 1], false];
}
return [sec_delay[sec_delay.length - 1], false];
}
else {
return [prevDelayMs, true];
}
}
else
return [60000, false];
}
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// timeout used to update every minute // timeout used to update every minute
@ -689,19 +630,22 @@ var drawTimeout;
// schedule a draw for the next minute or every sec_update ms // schedule a draw for the next minute or every sec_update ms
function queueDraw() { function queueDraw() {
let delay = settings.ring == 'Seconds' ? sec_update - (Date.now() % sec_update) : 60000 - (Date.now() % 60000); let now = Date.now();
var nextUpdateRet = getDelayMs(nextUpdateMs, settings.ring, now);
nextUpdateMs = nextUpdateRet[0];
let delay = nextUpdateMs - (now % nextUpdateMs);
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() { drawTimeout = setTimeout(function() {
drawTimeout = undefined; drawTimeout = undefined;
checkIdle(); checkIdle();
draw(); draw(nextUpdateRet[1]);
}, delay); }, delay);
} }
// Stop updates when LCD is off, restart when on // Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{ Bangle.on('lcdPower',on=>{
if (on) { if (on) {
draw(); // draw immediately, queue redraw draw(false); // draw immediately, queue redraw
} else { // stop draw timer } else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; drawTimeout = undefined;
@ -716,7 +660,7 @@ Bangle.setUI("clockupdown", btn=> {
Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0); Bangle.setHRMPower(infoMode == "ID_HRM" ? 1 : 0);
resetHrm(); resetHrm();
log_debug("idxInfo=" + settings.idxInfo); log_debug("idxInfo=" + settings.idxInfo);
draw(); draw(false);
storage.write(SETTINGS_FILE, settings); // Retains idxInfo when leaving the face storage.write(SETTINGS_FILE, settings); // Retains idxInfo when leaving the face
}); });
@ -724,6 +668,7 @@ loadSettings();
loadLocation(); loadLocation();
var infoMode = infoList[settings.idxInfo]; var infoMode = infoList[settings.idxInfo];
updateSunRiseSunSet(new Date(), location.lat, location.lon, true); updateSunRiseSunSet(new Date(), location.lat, location.lon, true);
nextUpdateMs = getDelayMs(1000, settings.ring, Date.now())[0];
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
@ -731,4 +676,4 @@ Bangle.loadWidgets();
* we are not drawing the widgets as we are taking over the whole screen * we are not drawing the widgets as we are taking over the whole screen
*/ */
widget_utils.hide(); widget_utils.hide();
draw(); draw(false);

View File

@ -1,8 +1,8 @@
{ "id": "daisy", { "id": "daisy",
"name": "Daisy", "name": "Daisy",
"version": "0.15", "version": "0.16",
"dependencies": {"mylocation":"app"}, "dependencies": {"mylocation":"app"},
"description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "description": "A beautiful digital clock with large ring gauge, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times",
"icon": "app.png", "icon": "app.png",
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",

View File

@ -7,36 +7,35 @@
'color': 'Green', 'color': 'Green',
'check_idle' : true, 'check_idle' : true,
'batt_hours' : false, 'batt_hours' : false,
'hr_12' : false,
'ring' : 'Steps', 'ring' : 'Steps',
'idxInfo' : 0, 'idxInfo' : 0,
'step_target' : 10000}; 'step_target' : 10000};
// ...and overwrite them with any saved values // ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings // This way saved values are preserved if a new version adds more settings
const storage = require('Storage'); const storage = require('Storage');
let settings = storage.readJSON(SETTINGS_FILE, 1) || s; let settings = storage.readJSON(SETTINGS_FILE, 1) || s;
const saved = settings || {}; const saved = settings || {};
for (const key in saved) { for (const key in saved) {
s[key] = saved[key]; s[key] = saved[key];
} }
function save() { function save() {
settings = s; settings = s;
storage.write(SETTINGS_FILE, settings); storage.write(SETTINGS_FILE, settings);
} }
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue']; var color_options = ['Green','Orange','Cyan','Purple','Red','Blue'];
var fg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f']; var fg_code = ['#0f0','#ff0','#0ff','#f0f','#f00','#00f'];
var gy_code = ['#020','#220','#022','#202','#200','#002']; var gy_code = ['#020','#220','#022','#202','#200','#002'];
var ring_options = ['Hours', 'Minutes', 'Seconds', 'Steps', 'Battery', 'Sun']; var ring_options = ['Hours', 'Minutes', 'Seconds', 'Day', 'Sun', 'Steps', 'Battery'];
var step_options = [100, 1000, 5000, 10000, 15000, 20000]; var step_options = [100, 1000, 5000, 10000, 15000, 20000];
function showMainMenu() { function showMainMenu() {
let appMenu = { let appMenu = {
'': { 'title': 'Daisy Clock' }, '': { 'title': 'Daisy Clock' },
'< Back': back, '< Back': back,
'Colour': { 'Color': {
value: 0 | color_options.indexOf(s.color), value: 0 | color_options.indexOf(s.color),
min: 0, max: color_options.length - 1, min: 0, max: color_options.length - 1,
format: v => color_options[v], format: v => color_options[v],
@ -45,28 +44,7 @@
s.fg = fg_code[v]; s.fg = fg_code[v];
s.gy = gy_code[v]; s.gy = gy_code[v];
save(); save();
}, }
},
'Idle Warning': {
value: !!s.idle_check,
onchange: v => {
s.idle_check = v;
save();
},
},
'Expected Battery Life In Days Not Percentage': {
value: !!s.batt_hours,
onchange: v => {
s.batt_hours = v;
save();
},
},
'12 Hr Time': {
value: !!s.hr_12,
onchange: v => {
s.hr_12 = v;
save();
},
}, },
'Ring Display': { 'Ring Display': {
value: 0 | ring_options.indexOf(s.ring), value: 0 | ring_options.indexOf(s.ring),
@ -95,6 +73,21 @@
}, },
}; };
} }
appMenu['Idle Warning'] = {
value: !!s.idle_check,
onchange: v => {
s.idle_check = v;
save();
},
};
appMenu['Battery Life Format'] = {
value: !!s.batt_hours,
format: value => value?"Days":"%",
onchange: v => {
s.batt_hours = v;
save();
},
};
E.showMenu(appMenu); E.showMenu(appMenu);
} }

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t3uJeVuMQCqcBLisAi4XLxAABFxAXKgc4DBAuBRhQXEDAq7MmYXEwBHEXZYXFGAOqAAKDMmczC4mIC62CC50PC4JIBkQABiIvRmURAAUSjQXSFwMoxGKC6CRFwUSVYgXLPIgXXwMYegoXLJAYXCGBnzGA0hPQIwMgYwGC6gwCC4ZIMC4gYBC604C4ZISmcRVgapQAAMhC6GIJIwXCMBcIxGDDBAuLC4IwGAARGMAAQWGmAXPJQoWMC4pwCCpoXJAB4XXAH4A/ABQA=")) require("heatshrink").decompress(atob("mEw4UA///31lAQM0hRL/ACED4ALJh4LKn4jK/iMLBRMMCxQuFgNUFxMFqtALoXwBYklqoYCNI0pqtVGpALCoE/Og2pBYNU/4uFgGqBYNVBZV/BZG//4ABBY+f6oYBHY2q0okBvpTG1RICvp6CAAcqHgQLJyoLBqALHGAN9VYgABhQwCuqSGBYIwBBY8CBYOVBYI8FBYWl+oLGMAIABBYJIGBYQjBBY1aEgQLHqwYCBY0BqtqBYOpHYoLBDAYiFgqaBpWq0ALILgwLZAH4AFA"))

View File

@ -3,12 +3,13 @@
"name": "Meridian Clock", "name": "Meridian Clock",
"shortName": "Meridian", "shortName": "Meridian",
"version": "0.01", "version": "0.01",
"description": "An elegant clock", "description": "An elegant clock with two ClockInfos",
"screenshots": [{ "url": "screenshot.png" }], "screenshots": [{ "url": "screenshot.png" }],
"icon": "icon.png", "icon": "icon.png",
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock,clkinfo",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"dependencies" : { "clock_info":"module" },
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{ "name": "meridian.app.js", "url": "app.js" }, { "name": "meridian.app.js", "url": "app.js" },

View File

@ -6,7 +6,7 @@
"icon": "icon.png", "icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
"type": "clock", "type": "clock",
"tags": "clock,clkinfo,clockbg", "tags": "clock,clockbg",
"supports" : ["BANGLEJS2"], "supports" : ["BANGLEJS2"],
"dependencies" : { "clockbg":"module" }, "dependencies" : { "clockbg":"module" },
"storage": [ "storage": [

View File

@ -1,2 +1,3 @@
0.01: New Clock! 0.01: New Clock!
0.02: Clockinfos now save under correct name, and wrap correctly to >1 line 0.02: Clockinfos now save under correct name, and wrap correctly to >1 line
0.03: Fix time's border when drawing over a solid color background

View File

@ -73,10 +73,9 @@ for (var i=0;i<10;i++)
var mn = d.getMinutes().toString().padStart(2,0); var mn = d.getMinutes().toString().padStart(2,0);
var date = require("locale").date(new Date()).split(" ").slice(0,2).join(" ").toUpperCase(); var date = require("locale").date(new Date()).split(" ").slice(0,2).join(" ").toUpperCase();
var x = 6, y = 16, w = 55, h = 67, datesz = 20, s=5; var x = 6, y = 16, w = 55, h = 67, datesz = 20, s=5;
g.reset();
background.fillRect(x, y, x + w*2, y + h*2 + datesz); background.fillRect(x, y, x + w*2, y + h*2 + datesz);
var dx = x+w, dy = y+h+datesz-10; var dx = x+w, dy = y+h+datesz-10;
g.setFont("LECO1976Regular").setFontAlign(0,0); g.reset().setFont("LECO1976Regular").setFontAlign(0,0);
g.setColor(g.theme.bg).drawString(date, dx+3,dy-3).drawString(date, dx+3,dy+3); g.setColor(g.theme.bg).drawString(date, dx+3,dy-3).drawString(date, dx+3,dy+3);
g.drawString(date, dx-3,dy-3).drawString(date, dx-3,dy+3); g.drawString(date, dx-3,dy-3).drawString(date, dx-3,dy+3);
g.drawString(date, dx,dy-3).drawString(date, dx,dy+3); g.drawString(date, dx,dy-3).drawString(date, dx,dy+3);

View File

@ -1,7 +1,7 @@
{ "id": "twotwoclock", { "id": "twotwoclock",
"name": "TwoTwo Clock", "name": "TwoTwo Clock",
"shortName":"22 Clock", "shortName":"22 Clock",
"version":"0.02", "version":"0.03",
"description": "A clock with the time split over two lines, with custom backgrounds and two ClockInfos", "description": "A clock with the time split over two lines, with custom backgrounds and two ClockInfos",
"icon": "icon.png", "icon": "icon.png",
"type": "clock", "type": "clock",

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

1
apps/umpire/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: First release with heart rate monitoring

94
apps/umpire/README.md Normal file
View File

@ -0,0 +1,94 @@
# Umpire Ball Counter
*Cricket umpire ball counter and match event logger for Bangle.js 2 hackable smart watch*
![Bangle.js2 smart watch in a collage of screenshots](20250511_210249-COLLAGE.jpg)
## Background
There are a few ways to keep track of balls when umpiring a cricket match. These vary from the rudimentary - having some stones in your pockets and transferring them from one side to the other - to the "modern" umpire's clicker. The latter allows you to track fairly delivered balls, overs completed and wickets fallen.
In addition, the umpire needs to know the time and to record events like when "Play" was called or when a fielder left the field. These events are written down on paper.
The umpire also needs to assess whether either team is timewasting and whether a new batter has made it out to the crease on time, as required by the Laws.
This app is designed to replace the umpire clicker, and keep track of events for analysis and reporting after each match.
## Objectives ##
The app has three objectives:
1. Count how many balls and wickets have been logged, show the current over number and the time.
2. Log these and other events with a timestamp and a duration since the last event.
3. Allow the data to be viewed on the watch and on a BLE-paired mobile device as structured data.
## Modules ##
The app is split into three screens shown to the umpire:
1. Main Menu
2. In-play Screen
3. Log viewer
There is an additional module to allow settings to be changed via the watch Settings menu; the number of balls in the over and the number of overs in the innings are configurable, so that 15 eight-ball overs, T20, 40-50 overs and The Hundred can all be supported.
A further module of the app is the web page used to view the log file in the app loader. The file can be deleted or a copy downloaded.
## Interaction ##
Bangle.js 2 app interactions are governed by the Bangle and Espruino (E) libraries. This app uses the following patterns:
### Swipe and Button ###
The in-play screen detects swipe and button events using Bangle.setUI.
When in-play, use of the button increments the (fairly delivered) ball count. The app does not suppress the screen lock so by default it requires the umpire to press once to unlock the watch and a second press to log the ball. Bangle.buzz is used to give positive feedback when the app takes action so that the screen does not need to be looked at.
After logging the third-to-last ball in the over, the app buzzes twice. This is typically when the umpire needs to check the balls remaining with the other umpire, in case they have not logged every fair delivery.
After the second-to-last ball the app gives one long buzz to remind the umpire that the next ball will close the over (if fairly delivered).
Logging the final ball triggers the change of over.
When the last over of the innings is complete the screen shows "END" but allows play to continue to be logged until the new innings is triggered.
Whilst the in-play screen is displayed swiping will cause the following actions to occur:
- **Swipe Up** performs the same action as the Button press, incrementing the ball count.
- **Swipe Down** decrements the ball count and logs the action as a "Correction".
- **Swipe Right** shows the log viewer.
- **Swipe Left** shows the main menu.
### Confirmation Prompts ###
When choosing "Wicket" or "Recall" from the main menu, E.showPrompt is used to ask for positive confirmation before incrementing or decrementing the wickets, respectively. Those functions also increment or decrement the ball count.
### Scrollers ###
The main menu and log viewer both use E.showScroller to display a scrollable list of tappable items. This is in preference to E.showMenu which has very small touch targets.
Whenever a scroller is shown, the in-play swipe interactions are switched off.
Tapping on the log viewer returns the umpire to the in-play screen.
### Twist to refresh ###
The app uses Bangle.twist to detect the umpire turning their wrist to view the screen and, when the in-play screen is active, will refresh the current time and elapsed time since the last ball.
## Timing ##
The app calculates three durations:
1. **Ball to ball** - When logging a ball the elapsed time since the last logged ball.
2. **Overs** - When logging the last ball of the over, the elapsed time since the last ball of the previous over was logged (or when "Play" was initially called).
3. **Lost Time** - When logging the next ball after a call of "Time" the elapsed time since the call of "Time".
## Other Metadata Logging ##
The app now includes logging of other sensor data from the watch. the following items have been added to the log function:
1. **Steps** - When the app is launched it notes the current step count and then logs the number of steps the umpire has taken on every ball.
2. **Battery Level** - In order to assess whether the heart rate monitor is running the battery down, the battery percentage is logged on every ball.
3. **Heart Rate Monitoring** - The main menu includes an option to "Start HRM" and then the heart rate is logged on every ball.
N.B. The HRM takes about 5 seconds to start measuring the umpire's heart rate.
Once switched on, the HRM has two additional features:
1. **Battery Saving** - After the Call of Time the HRM is powered down until (Play is called and) the next ball is logged.
2. **Auto-logging** - The app Settings menu has an additional item which is a heartbeat rate over which the app should automatically log an event. Once the umpire's heart rate goes over the limit for 10 consecutive seconds an event will be logged and the in-play screen will show the heart rate on every refresh. Until the heart rate falls below the threshold no further heart events will be logged. A further event will not be triggered until the limit is exceeded again for 20 consecutive seconds. The HRM can be stopped from the main menu during the match.

1
apps/umpire/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///yn+hW803RBwUP/gUDlWAAoc//4FD1WgAoYiB4AEBgWq1QLD/1vBYUK0uqEgf9r4LClWVtQkDC4mqqtWBYYvBFwWpqtV1BHGFwILBHgcP+AuDBYNqPwwuBBZAuDEYgACFwdWHYaMDFwVaQgguGTggu/F34uxewguFEYguGHYYuHgEDKYIuHgEP9WVFw4LC0ouHgE//2qFw8A///1QuHgf//kqFxH/4EKFxH/BI4uC/gKIFwPwBZAuCBZAuWBYIuJEYIuJHYIKJgA"))

432
apps/umpire/app.js Normal file
View File

@ -0,0 +1,432 @@
// settings and environment
var settings = Object.assign({
// default values
ballsPerOver: 6,
oversPerInnings: 40,
heartRateLimit: 130
}, require('Storage').readJSON("umpire.json", true) || {});
const BALLS_PER_OVER = settings.ballsPerOver;
const OVERS_PER_INNINGS = settings.oversPerInnings;
const HEART_RATE_LIMIT = settings.heartRateLimit;
delete settings;
const TIMEZONE_OFFSET_HOURS = (new Date()).getTimezoneOffset() / 60;
const STEP_COUNT_OFFSET = Bangle.getStepCount();
const BALL_TO_COME_CHAR = '-';
const BALL_FACED_CHAR = '=';
// globals
var processing = true; //debounce to inhibit twist events
var wickets = 0;
var counter = 0;
var over = 0;
var ballTimes = [];
var overTimes = [];
var timeTimes = [];
var log = [];
var timeCalled = false;
var batteryPercents = [];
var battery = 0;
var heartRate = '';
var heartRateEventSeconds = 0;
var HRM = false;
function toggleHRM() {
if(HRM) {
Bangle.setHRMPower(0);
HRM = false;
heartRateEventSeconds = 0;
heartRate = '';
} else {
Bangle.setHRMPower(1);
HRM = true;
}
}
function getBattery() {
// calculate last 10 moving average %
batteryPercents.push(E.getBattery());
if(batteryPercents.length > 10) batteryPercents.shift();
return Math.round(batteryPercents.reduce((avg,e,i,arr)=>avg+e/arr.length,0));
}
// process heart rate monitor event
// each second (approx.)
function updateHeartRate(h) {
heartRate = h.bpm || 0;
if(heartRate >= HEART_RATE_LIMIT) {
heartRateEventSeconds++;
if(heartRateEventSeconds==10)
addLog((new Date()), over, counter,
"Heart Rate", ">" + HEART_RATE_LIMIT);
}
if(heartRateEventSeconds > 10
&& heartRate < HEART_RATE_LIMIT)
heartRateEventSeconds = -10;
}
// write events to storage (csv, persistent)
// and memory (can be truncated while running)
function addLog(timeSig, over, ball, matchEvent, metaData) {
var steps = Bangle.getStepCount() - STEP_COUNT_OFFSET;
// write to storage
var csv = [
formatTimeOfDay(timeSig),
over-1, ball,
matchEvent, metaData,
steps, battery, heartRate
];
file.write(csv.join(",")+"\n");
// write to memory
log.unshift({ // in rev. chrono. order
time: formatTimeOfDay(timeSig),
over: over-1,
ball: ball,
matchEvent: matchEvent,
metaData: metaData,
steps: steps,
battery: battery,
heartRate: heartRate
});
}
// display log from memory (not csv)
function showLog() {
processing = true;
Bangle.setUI();
return E.showScroller({
h: 50, c: log.length,
draw: (idx, r) => {
g.setBgColor((idx&1)?"#000":"#112").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
if(log[idx].matchEvent==/*LANG*/"Over Duration"
|| log[idx].matchEvent==/*LANG*/"Innings Duration") {
g.setFont("Vector", 22).drawString(
log[idx].matchEvent,r.x+6,r.y+2);
} else {
g.setFont("Vector", 22).drawString(
log[idx].over + "." +
log[idx].ball + " " +
log[idx].matchEvent,r.x+6,r.y+2);
}
g.setFont("Vector", 18).drawString(
log[idx].time + " " +
log[idx].metaData + " " +
log[idx].heartRate,r.x+6,r.y+27);
},
select: (idx) => {
resumeGame();
}
});
}
// format date (diff) as duration
function formatDuration(timeDate) {
return (timeDate.getHours() + TIMEZONE_OFFSET_HOURS) + ":"
+ timeDate.getMinutes().toString().padStart(2, "0") + ":"
+ timeDate.getSeconds().toString().padStart(2, "0") + "";
}
// format date as clock
function formatTimeOfDay(timeSig) {
return timeSig.getHours() + ":"
+ timeSig.getMinutes().toString().padStart(2, "0");
}
// main ball counter logic
// and in-play screen
function countDown(dir) {
processing = true;
battery = getBattery(); // refresh battery
counter += dir;
// suppress correction on first ball of innings
if(over==1 && counter<0) {
counter=0;
processing = false;
return;
}
// Suppress dir when play after time
if(timeCalled)
counter -= dir;
// Correction to last ball of over
if(counter<0) {
counter = BALLS_PER_OVER -1;
over -= 1;
// use end of over time as last ball time
ballTimes.push(overTimes.pop());
}
// create timestamp for log
var timeSig = new Date();
// calculate elapsed since last ball
var lastBallTime = timeSig.getTime();
if(ballTimes.length>0) {
lastBallTime = ballTimes[ballTimes.length - 1];
} else if(overTimes.length>0) {
lastBallTime = overTimes[overTimes.length - 1];
}
var deadDuration = new Date(
timeSig.getTime() - lastBallTime);
// process new (dead) ball
if(dir!=0) {
// call play after time?
if(timeCalled) {
timeCalled = false;
// resume heart rate monitoring
if(HRM) Bangle.setHRMPower(1);
// calculate time lost and log it
var lastTimeTime = timeTimes[timeTimes.length - 1];
var timeDuration = new Date(
timeSig.getTime() - lastTimeTime);
addLog(timeSig, over, counter,
"Play", /*LANG*/"Lost:" + formatDuration(timeDuration));
} else {
if(counter>0) // reset elapsed time
ballTimes.push(timeSig.getTime());
Bangle.setLCDPower(1); //TODO need any more?
if(dir>0) { // fairly delivered ball
addLog(timeSig, over, counter,
"Ball", formatDuration(deadDuration));
} else { // +1 ball still to come
addLog(timeSig, over, counter,
/*LANG*/"Correction", formatDuration(deadDuration));
}
}
// give haptic feedback
if(counter == BALLS_PER_OVER - 2) {
// buzz twice "2 to come"
Bangle.buzz(400).then(()=>{
return new Promise(
resolve=>setTimeout(resolve,500));
}).then(()=>{
return Bangle.buzz(500);
})
} else if(counter == BALLS_PER_OVER - 1) {
// long buzz "1 to come"
Bangle.buzz(800);
} else {
// otherwise short buzz
Bangle.buzz()
}
// Process end of over
if (counter == BALLS_PER_OVER) {
// calculate match time
var matchDuration = new Date(
timeSig.getTime() - overTimes[0]);
var matchMinutesString = formatDuration(matchDuration);
// calculate over time
var overDuration = new Date(
timeSig.getTime() - overTimes[overTimes.length - 1]);
var overMinutesString = formatDuration(overDuration);
// log end of over
addLog(timeSig, over + 1, 0,
/*LANG*/"Over Duration", overMinutesString);
addLog(timeSig, over + 1, 0,
/*LANG*/"Innings Duration", matchMinutesString);
overTimes.push(timeSig.getTime());
// start new over
over += 1;
counter = 0;
ballTimes = [];
}
}
// refresh in-play screen
g.clear(1);
// draw wickets fallen (top-right)
g.setFontAlign(1,0);
g.setFont("Vector",26).
drawString(wickets, 162, 14, true);
g.setFont("Vector",12).
drawString('\¦\¦\¦', 173, 15, true);
// draw battery and heart rate (top-left)
g.setFontAlign(-1,0);
var heartRateString = 'HR:' + heartRate;
if(heartRateEventSeconds <= 0) heartRateString = '';
g.setFont("Vector",16).
drawString(battery + '% ' + heartRateString, 5, 11, true);
// draw clock (upper-centre)
g.setFontAlign(0,0);
g.setFont("Vector",48).
drawString(formatTimeOfDay(timeSig), 93, 55, true);
// draw over.ball (centre)
var ballString = (over-1) + "." + counter;
if(over > OVERS_PER_INNINGS)
ballString = 'END';
g.setFont("Vector",80).
drawString(ballString, 93, 120, true);
// draw ball graph and elapsed time
var ballGraph =
BALL_FACED_CHAR.repeat(counter)
+ BALL_TO_COME_CHAR.repeat(BALLS_PER_OVER - counter);
if(timeCalled) ballGraph = '-TIME-';
g.setFont("Vector",18).drawString(
ballGraph + ' ' + formatDuration(deadDuration), 93, 166, true);
// return to wait for next input
processing = false;
}
function resumeGame(play) {
processing = true;
Bangle.buzz();
Bangle.setUI({
mode: "custom",
swipe: (directionLR, directionUD)=>{
if (directionLR==-1) {
processing = true;
showMainMenu();
} else if (directionLR==1) {
processing = true;
showLog();
} else if (directionUD==-1) {
processing = true;
countDown(1);
} else {
processing = true;
countDown(-1);
}
},
btn: ()=>{
processing = true;
countDown(1);
}
});
if(over==0) { // at start of innings
over += 1; // N.B. 1-based overs in code
counter = 0; // balls
ballTimes = [];
// set an inital time for comparison
var timeSig = new Date();
overTimes.push(timeSig.getTime());
addLog(timeSig, over, counter,
"Play", "");
}
// load in-play screen
countDown(play? -1: 0);
}
function incrementWickets(inc) {
processing = true;
E.showPrompt(/*LANG*/"Amend wickets by " + inc + "?").
then(function(confirmed) {
if (confirmed) {
Bangle.buzz();
wickets += inc;
var timeSig = new Date();
if(inc>0) {
countDown(1);
addLog(timeSig, over, counter,
"Wicket", "Wickets: " + wickets);
} else {
addLog(timeSig, over, counter,
/*LANG*/"Recall Batter", "Wickets: " + wickets);
}
resumeGame();
} else {
E.showPrompt();
showMainMenu();
}
});
}
function showMainMenu() {
processing = true;
Bangle.setUI();
var scrollMenuItems = [];
// add menu items
if(over>0)
scrollMenuItems.push("« Back");
if(over==0 || timeCalled)
scrollMenuItems.push("Call Play");
if(over>0 && !timeCalled) {
scrollMenuItems.push("Wicket");
if(wickets>0)
scrollMenuItems.push(/*LANG*/"Recall Batter");
scrollMenuItems.push("Call Time");
scrollMenuItems.push("New Innings");
if(!HRM)
scrollMenuItems.push("Start HRM");
}
if(HRM) scrollMenuItems.push("Stop HRM");
// show menu
return E.showScroller({
h: 80, c: scrollMenuItems.length,
draw: (idx, r) => {
g.setBgColor((idx&1)?"#000":"#121").clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1);
g.setFont("Vector", 30).drawString(scrollMenuItems[idx],r.x+10,r.y+28);
},
select: (idx) => {
if(scrollMenuItems[idx]=="Call Time") {
timeCalled = true;
// power down HRM until play
Bangle.setHRMPower(0);
heartRateEventSeconds = 0;
var timeSig = new Date();
timeTimes.push(timeSig.getTime());
addLog(timeSig, over, counter,
"Time", (HRM? "HRM Paused" : ""));
resumeGame();
}
if(scrollMenuItems[idx]=="Call Play")
resumeGame(timeCalled);
if(scrollMenuItems[idx]=="« Back")
resumeGame();
if(scrollMenuItems[idx]=="Wicket")
incrementWickets(1);
if(scrollMenuItems[idx]==/*LANG*/"Recall Batter")
incrementWickets(-1);
if(scrollMenuItems[idx]=="New Innings")
newInnings();
if(scrollMenuItems[idx]=="Start HRM"
|| scrollMenuItems[idx]=="Stop HRM") {
toggleHRM();
resumeGame();
}
}
});
}
function newInnings() {
var timeSig = new Date();
if(over!=0) { // new innings
E.showPrompt(/*LANG*/"Start next innings?").
then(function(confirmed) {
if (confirmed) {
Bangle.buzz();
processing = true; //debounce to inhibit twist events
wickets = 0;
counter = 0;
over = 0;
ballTimes = [];
overTimes = [];
timeTimes = [];
log = [];
timeCalled = false;
addLog(timeSig, OVERS_PER_INNINGS + 1, BALLS_PER_OVER,
"New Innings", require("locale").date(new Date(), 1));
resumeGame();
} else {
E.showPrompt();
showMainMenu();
}
});
} else { // resume innings or start app
addLog(timeSig, OVERS_PER_INNINGS + 1, BALLS_PER_OVER,
"New Innings", require("locale").date(new Date(), 1));
}
}
// initialise file in storage
var file = require("Storage").open("matchlog.csv","a");
// save state on exit TODO WIP
E.on("kill", function() {
console.log("Umpire app closed at " + (over-1) + "." + counter);
});
// set up twist refresh once only
Bangle.on('twist', function() {
if(!processing) {
processing = true; // debounce
countDown(0);
}
});
// set up HRM listener once only
Bangle.on('HRM', function(h) {
updateHeartRate(h)});
newInnings(); // prepare 1st innings
showMainMenu(); // ready to play

BIN
apps/umpire/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,76 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<div id="data"></div>
<button class="btn btn-default" id="btnSave">Download</button>
<button class="btn btn-default" id="btnDelete">Delete</button>
<script src="../../core/lib/interface.js"></script>
<script>
var dataElement = document.getElementById("data");
var csvData = "";
function getData() {
// show loading window
Util.showModal("Loading...");
// get the data
dataElement.innerHTML = "";
Util.readStorageFile(`matchlog.csv`,data=>{
csvData = data.trim();
// remove window
Util.hideModal();
// If no data, report it and exit
if (data.length==0) {
dataElement.innerHTML = "<b>No data found</b>";
return;
}
// Otherwise parse the data and output it as a table
dataElement.innerHTML = `<table style="border: 1px solid #cccccc">
<tr>
<th>Time</th>
<th>Over</th>
<th>Ball</th>
<th>Event</th>
<th>Metadata</th>
<th>Steps</th>
<th>Battery %</th>
<th>Heart Rate</th>
</tr>`+data.trim().split("\n").map(l=>{
l = l.split(",");
return `<tr>
<td>${l[0]}</td>
<td align="center">${l[1]}</td>
<td align="center">${l[2]}</td>
<td>${l[3]}</td>
<td>${l[4]}</td>
<td>${l[5]}</td>
<td>${l[6]}</td>
<td>${l[7]}</td>
</tr>`
}).join("\n")+"</table>";
});
}
// You can call a utility function to save the data
document.getElementById("btnSave").addEventListener("click", function() {
// TODO column headings in output
Util.saveCSV("matchdata", csvData);
});
// Or you can also delete the file
document.getElementById("btnDelete").addEventListener("click", function() {
Util.showModal("Deleting...");
Util.eraseStorageFile("matchlog.csv", function() {
Util.hideModal();
getData();
});
});
// Called when app starts
function onInit() {
getData();
}
</script>
</body>
</html>

18
apps/umpire/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{ "id": "umpire",
"name": "Umpire Ball Counter",
"shortName":"Umpire",
"icon": "app.png",
"version": "0.01",
"description": "Cricket umpire ball counter and match event logger",
"tags": "outdoors,tool,health,sports,referee,judge,assistant",
"readme":"README.md",
"interface": "interface.html",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"umpire.app.js","url":"app.js"},
{"name":"umpire.settings.js","url":"settings.js"},
{"name":"umpire.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"umpire.json"}]
}

43
apps/umpire/settings.js Normal file
View File

@ -0,0 +1,43 @@
(function(back) {
var FILE = "umpire.json";
// Load settings
var settings = Object.assign({
ballsPerOver: 6,
oversPerInnings: 40,
heartRateLimit: 130
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Show the menu
E.showMenu({
"" : { "title" : "Umpire Ball Counter" },
"< Back" : () => back(),
'Balls per over': {
value: settings.ballsPerOver,
min: 4, max: 10,
onchange: v => {
settings.ballsPerOver = v;
writeSettings();
}
},
'Overs per inn.': {
value: settings.oversPerInnings,
min: 12, max: 50,
onchange: v => {
settings.oversPerInnings = v;
writeSettings();
}
},
'Auto-log HRM': {
value: settings.heartRateLimit,
min: 100, max: 200,
onchange: v => {
settings.heartRateLimit = v;
writeSettings();
}
}
});
})

View File

@ -343,6 +343,10 @@ apps.forEach((app,appIdx) => {
if (a>=0 && b>=0 && a<b) if (a>=0 && b>=0 && a<b)
WARN(`Clock ${app.id} file calls loadWidgets before setUI (clock widget/etc won't be aware a clock app is running)`, {file:appDirRelative+file.url, line : fileContents.substr(0,a).split("\n").length}); WARN(`Clock ${app.id} file calls loadWidgets before setUI (clock widget/etc won't be aware a clock app is running)`, {file:appDirRelative+file.url, line : fileContents.substr(0,a).split("\n").length});
} }
if (fileContents.includes("clock_info") && (!app.dependencies || !app.dependencies.clock_info) && !["boot","clock_info"].includes(app.id))
ERROR(`App ${app.id}'s uses clock_info but doesn't have a dependency on it`, {file:appDirRelative+file.url});
if (fileContents.includes("clockbg") && (!app.dependencies || !app.dependencies.clockbg) && !["clockbg"].includes(app.id))
ERROR(`App ${app.id}'s uses clockbg but doesn't have a dependency on it`, {file:appDirRelative+file.url});
// if settings // if settings
if (/\.settings?\.js$/.test(file.name)) { if (/\.settings?\.js$/.test(file.name)) {
// suggest adding to datafiles // suggest adding to datafiles

@ -1 +1 @@
Subproject commit 4dc22df5fd9af2f8fc41a626cc7d8d5456b8e614 Subproject commit fa5b2861b5574faecefb98554202ce34dd8d52d4