433 lines
12 KiB
JavaScript
433 lines
12 KiB
JavaScript
// 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
|