Created
February 4, 2022 19:31
-
-
Save zachrattner/dd2e00693cb2dbf8accbe7a7786c68d9 to your computer and use it in GitHub Desktop.
Device Motion Analysis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en-us"> | |
<head> | |
<title>Yembo Sensor Testing</title> | |
<!-- Make page show up ok on mobile devices --> | |
<meta charset="utf-8"/> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> | |
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0" name="viewport"/> | |
<meta name="viewport" content="width=device-width"/> | |
<!-- Bring in Google Font --> | |
<link rel="preconnect" href="https://fonts.googleapis.com"> | |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;600&display=swap" rel="stylesheet"> | |
<style type="text/css"> | |
body { | |
font-family: Montserrat, sans-serif; | |
background: #04264B; | |
} | |
h1, h2, h3, h4, h5, h6 { | |
margin: 0; | |
} | |
button { | |
background: #1771F1; | |
box-shadow: 0px 4px 8px rgba(0, 74, 136, 0.18), 0px 0px 2px rgba(0, 74, 136, 0.12), 0px 0px 1px rgba(0, 0, 0, 0.04); | |
border-radius: 8px; | |
border: none; | |
font-size: 1rem; | |
cursor: pointer; | |
padding: 8px 16px; | |
color: #fff; | |
} | |
button:hover { | |
background: #0C5AC9; | |
box-shadow: 0px 10px 20px rgba(0, 74, 136, 0.18), 0px 2px 6px rgba(0, 74, 136, 0.12), 0px 0px 1px rgba(0, 0, 0, 0.04); | |
} | |
button:active { | |
opacity: 0.9; | |
} | |
div.content-container { | |
background-color: #fff; | |
border-radius: 16px; | |
box-shadow: 0 4px 20px rgb(0 0 0 / 10%); | |
display: flex; | |
flex-direction: column; | |
height: 100%; | |
min-height: 85vh; | |
overflow: hidden; | |
padding: 16px; | |
} | |
div.content-container > div.item-wrapper { | |
max-width: 600px; | |
margin: 8px auto; | |
} | |
pre { | |
width: 100%; | |
min-width: 300px; | |
margin: 8px auto; | |
max-height: 60vh; | |
background-color: #19232E; | |
color: #dce0ef; | |
font-family: monospace; | |
padding: 4px; | |
border-radius: 4px; | |
overflow: scroll; | |
} | |
div#sensor-status { | |
padding: 4px; | |
margin: 16px 0; | |
text-align: center; | |
border-width: 1px; | |
border-style: solid; | |
border-radius: 4px; | |
} | |
div#sensor-status.inactive { | |
color: #69747C; | |
border-color: #69747C; | |
background-color: #DCE0EF; | |
} | |
div#sensor-status.inactive { | |
color: rgba(105, 116, 124, 100%); | |
border-color: rgba(105, 116, 124, 100%); | |
background-color: rgba(105, 116, 124, 20%); | |
} | |
div#sensor-status.success { | |
color: rgba(45, 126, 88, 100%); | |
border-color: rgba(45, 126, 88, 100%); | |
background-color: rgba(45, 126, 88, 20%); | |
} | |
div#sensor-status.warning { | |
color: rgba(241, 180, 4, 100%); | |
border-color: rgba(241, 180, 4, 100%); | |
background-color: rgba(241, 180, 4, 20%); | |
} | |
div#sensor-status.error { | |
color: rgba(210, 47, 47, 100%); | |
border-color: rgba(210, 47, 47, 100%); | |
background-color: rgba(210, 47, 47, 20%); | |
} | |
div#log-wrapper { | |
display: none; | |
} | |
div#title-wrapper { | |
text-align: center; | |
} | |
button#sensor-control { | |
margin: 16px 0; | |
font-family: Montserrat, sans-serif; | |
cursor: pointer; | |
} | |
</style> | |
<script> | |
const sensor = { | |
dom: {}, | |
status: 'unknown', | |
statusText: null, | |
// Log buffer for displaying most recent entries in the UI | |
log: [], | |
MAX_LOG_LENGTH: 10, | |
// Limits on vector sum of motion events | |
ACCELERATION_THRESHOLD: 8, | |
ANGLE_CHANGE_THRESHOLD: 200, | |
/* Consider the reading to be "bad" if this many consecutive events are bad. | |
* This smooths out spurious/noisy samples */ | |
BAD_MOTION_CONSECUTIVE_THRESHOLD: 15, | |
attachEventHandlers: () => { | |
sensor.dom.$sensorControl = document.getElementById("sensor-control"); | |
sensor.dom.$sensorStatus = document.getElementById("sensor-status"); | |
sensor.dom.$logWrapper = document.getElementById("log-wrapper"); | |
sensor.dom.$log = document.getElementById("log"); | |
// Button either starts or stops analysis, depending on state | |
sensor.dom.$sensorControl.onclick = (e) => { | |
switch (sensor.status) { | |
case 'analyzing': | |
sensor.stopAnalysis(); | |
break; | |
case 'unknown': | |
default: | |
sensor.handlePermissions(); | |
break; | |
} | |
}; | |
}, | |
// Update UI and start analyzing sensor data | |
onPermissionGranted: () => { | |
sensor.status = 'analyzing'; | |
sensor.dom.$sensorStatus.className = 'success'; | |
sensor.dom.$sensorStatus.innerText = '📊 Analyzing data...'; | |
sensor.dom.$sensorControl.innerText = 'Stop Analyzing'; | |
sensor.requestMotionData(); | |
}, | |
// Show permission error | |
onPermissionDenied: () => { | |
sensor.status = 'failed'; | |
sensor.dom.$sensorStatus.className = 'error'; | |
sensor.dom.$sensorStatus.innerText = '🚨 Permission denied'; | |
}, | |
// Show unsupported browser error | |
onUnsupportedBrowserDetected: () => { | |
sensor.status = 'failed'; | |
sensor.dom.$sensorStatus.className = 'error'; | |
sensor.dom.$sensorStatus.innerText = '🚨 Sorry, your browser doesn\'t support monitoring accelerometer data.'; | |
}, | |
// Request motion data from the browser | |
requestMotionData: () => { | |
sensor.dom.$logWrapper.style.display = 'block'; | |
sensor.dom.$log.innerText = 'Waiting for motion data...'; | |
window.addEventListener('devicemotion', sensor.handleMotion); | |
}, | |
// Calculate magnitude of a vector | |
calcVectorMagnitude: (x, y, z) => { | |
return Math.sqrt((x ?? 0) ** 2 + (y ?? 0) ** 2 + (z ?? 0) ** 2); | |
}, | |
isMotionOk: (event) => { | |
let isAccelerationOk = true; | |
let isRotationOk = true; | |
// Rotation around each axis cannot be too fast | |
const { alpha, beta, gamma } = event.rotationRate ?? {}; | |
const gyroscopeSum = sensor.calcVectorMagnitude(alpha, beta, gamma); | |
if (gyroscopeSum > sensor.ANGLE_CHANGE_THRESHOLD) { | |
isRotationOk = false; | |
} | |
// Linear acceleration cannot be too fast | |
const { x, y, z } = event.acceleration ?? {}; | |
const accelerometerSum = sensor.calcVectorMagnitude(x, y, z); | |
if (accelerometerSum > sensor.ACCELERATION_THRESHOLD) { | |
isAccelerationOk = false; | |
} | |
// Motion is ok if both quantities are within allowable limits | |
const isInstantaneousMotionOk = isAccelerationOk && isRotationOk; | |
if (isInstantaneousMotionOk) { | |
sensor.numConsecutiveBadMotionCounts = 0; | |
} | |
else { | |
sensor.numConsecutiveBadMotionCounts++; | |
} | |
/* Consider the overall motion to be in a bad state if too many consecutive bad readings | |
* have been detected */ | |
return (sensor.numConsecutiveBadMotionCounts < sensor.BAD_MOTION_CONSECUTIVE_THRESHOLD); | |
}, | |
handleMotion: (event) => { | |
const isMotionOk = sensor.isMotionOk(event); | |
const now = new Date().toISOString(); | |
const x = event.acceleration.x; | |
const y = event.acceleration.y; | |
const z = event.acceleration.z; | |
const alpha = event.rotationRate.alpha; | |
const beta = event.rotationRate.beta; | |
const gamma = event.rotationRate.gamma; | |
const result = isMotionOk ? 'ok' : 'too fast'; | |
const logPacket = `timestamp: ${now}\n\tx: ${x}\n\ty: ${y}\n\tz: ${z}\n\tα: ${alpha}\n\tβ: ${beta}\n\tγ: ${gamma}\n\tresult: ${result}\n`; | |
// Treat log as a circular buffer, otherwise text will get way too long and page will hang | |
sensor.log.unshift(logPacket); | |
if (sensor.log.length > sensor.MAX_LOG_LENGTH) { | |
sensor.log.pop(); | |
} | |
// Update UI to reflect motion analysis status | |
sensor.dom.$log.innerText = sensor.log.join("\n"); | |
if (isMotionOk) { | |
sensor.dom.$sensorStatus.className = 'success'; | |
sensor.dom.$sensorStatus.innerText = '✅ Speed ok'; | |
} | |
else { | |
sensor.dom.$sensorStatus.className = 'warning'; | |
sensor.dom.$sensorStatus.innerText = '🐌 Slow down, partner'; | |
} | |
}, | |
handlePermissions: () => { | |
/* Not all browsers require permission to access motion events. Check if the function to request | |
* permission exists, and call it if so. */ | |
const isPermissionCheckNeeded = typeof DeviceMotionEvent.requestPermission === 'function'; | |
if (isPermissionCheckNeeded) { | |
DeviceMotionEvent.requestPermission().then(permissionState => { | |
if (permissionState === 'granted') { | |
sensor.onPermissionGranted(); | |
} | |
}).catch(sensor.onPermissionDenied); | |
} | |
else { | |
if (window.DeviceMotionEvent === null) { | |
sensor.onUnsupportedBrowserDetected(); | |
} | |
else { | |
sensor.onPermissionGranted(); | |
} | |
} | |
}, | |
// Stop collecting and analyzing data, and update UI to reflect the change | |
stopAnalysis: () => { | |
sensor.status = 'completed'; | |
sensor.dom.$sensorStatus.className = 'inactive'; | |
sensor.dom.$sensorStatus.innerText = '😴 Analysis inactive'; | |
sensor.dom.$sensorControl.innerText = 'Start Analyzing'; | |
window.removeEventListener('devicemotion', sensor.handleMotion); | |
}, | |
}; | |
window.onload = () => { | |
window.sensor = sensor; // For debugging | |
sensor.attachEventHandlers(); | |
}; | |
</script> | |
</head> | |
<body> | |
<div class="content-container"> | |
<div class="item-wrapper" id="title-wrapper"> | |
<button id="sensor-control">Start Analyzing</button> | |
</div> | |
<div class="item-wrapper" id="sensor-wrapper"> | |
<div id="sensor-status" class="inactive"> | |
😴 Analysis inactive | |
</div> | |
</div> | |
<div class="item-wrapper" id="log-wrapper"> | |
<h3>Log</h3> | |
<pre id="log"></pre> | |
</div> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment