Skip to content

Instantly share code, notes, and snippets.

@zachrattner
Created February 4, 2022 19:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zachrattner/dd2e00693cb2dbf8accbe7a7786c68d9 to your computer and use it in GitHub Desktop.
Save zachrattner/dd2e00693cb2dbf8accbe7a7786c68d9 to your computer and use it in GitHub Desktop.
Device Motion Analysis
<!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