├── .env ├── .gitignore ├── README.md ├── assets └── bunny.obj ├── css ├── dark.css ├── light.css └── style.css ├── index.html └── js └── script.js /.env: -------------------------------------------------------------------------------- 1 | # Scrubbed by Glitch 2020-01-28T20:10:15+0000 2 | # Scrubbed by Glitch 2020-02-07T01:37:47+0000 3 | # Scrubbed by Glitch 2020-02-14T21:36:52+0000 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adafruit WebSerial 3D Model Viewer 2 | Source files for the Adafruit WebSerial 3D Model Viewer available at: https://adafruit.github.io/Adafruit_WebSerial_3DModelViewer/. This is the web end for the Adafruit AHRS calibrated_orientation sketch. 3 | 4 | ## Adafruit Learn Guide 5 | To learn how to use the 3D Model Viewer, check out the learn guide at https://learn.adafruit.com/how-to-fuse-motion-sensor-data-into-ahrs-orientation-euler-quaternions 6 | -------------------------------------------------------------------------------- /css/dark.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: #000; 3 | color: #fff; 4 | } 5 | 6 | body { 7 | background-color: #282828; 8 | color: #fff; 9 | } 10 | 11 | canvas { 12 | border-color: #666; 13 | background-color: #383838; 14 | } 15 | 16 | input, select, button { 17 | background-color: #454545; 18 | color: #fff; 19 | } 20 | 21 | .serial-input input { 22 | background-color: #383838; 23 | border-color: #666; 24 | } 25 | 26 | .serial-input input:disabled, 27 | .serial-input button:disabled { 28 | border-color: #333; 29 | color: #666; 30 | } 31 | 32 | .timestamp { 33 | color: #888; 34 | } 35 | 36 | #notSupported { 37 | background-color: red; 38 | color: white; 39 | } 40 | 41 | .log { 42 | border-color: #666; 43 | background-color: #383838; 44 | color: #ccc; 45 | } 46 | 47 | .calibration-container { 48 | border-color: #666; 49 | background-color: #383838; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /css/light.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: #000; 3 | color: #fff; 4 | } 5 | 6 | body { 7 | background-color: #efefef; 8 | } 9 | 10 | canvas { 11 | border-color: purple; 12 | background-color: #fff; 13 | } 14 | 15 | input, select, button { 16 | background-color: #fff; 17 | color: #000; 18 | } 19 | 20 | .serial-input input { 21 | border-color: purple; 22 | } 23 | 24 | .serial-input input:disabled { 25 | border-color: #ccc; 26 | } 27 | 28 | .timestamp { 29 | color: #999; 30 | } 31 | 32 | #notSupported { 33 | background-color: red; 34 | color: white; 35 | } 36 | 37 | .log { 38 | border-color: purple; 39 | background-color: #fff; 40 | color: #333; 41 | } 42 | 43 | .calibration-container { 44 | border-color: purple; 45 | background-color: #fff; 46 | } 47 | 48 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Header 3 | */ 4 | 5 | .header { 6 | align-content: center; 7 | align-items: stretch; 8 | box-shadow: 9 | 0 4px 5px 0 rgba(0, 0, 0, 0.14), 10 | 0 2px 9px 1px rgba(0, 0, 0, 0.12), 11 | 0 4px 2px -2px rgba(0, 0, 0, 0.2); 12 | display: flex; 13 | flex-direction: row; 14 | flex-wrap: nowrap; 15 | font-size: 20px; 16 | height: 5vh; 17 | min-height: 50px; 18 | justify-content: flex-start; 19 | padding: 16px 16px 0 16px; 20 | position: fixed; 21 | transition: transform 0.233s cubic-bezier(0, 0, 0.21, 1) 0.1s; 22 | width: 100%; 23 | will-change: transform; 24 | z-index: 1000; 25 | margin: 0; 26 | } 27 | 28 | .header h1 { 29 | flex: 1; 30 | font-size: 20px; 31 | font-weight: 400; 32 | } 33 | 34 | body { 35 | font-family: "Benton Sans", "Helvetica Neue", helvetica, arial, sans-serif; 36 | margin: 0; 37 | } 38 | 39 | canvas { 40 | border-width: 1px; 41 | border-style: solid; 42 | } 43 | 44 | p { 45 | margin: 0.2em; 46 | } 47 | 48 | span.remix { 49 | float: right; 50 | } 51 | 52 | button { 53 | font-size: 0.9em; 54 | margin: 5px 10px; 55 | } 56 | 57 | .serial-input { 58 | margin: 10px 0; 59 | height: 40px; 60 | line-height: 40px; 61 | } 62 | 63 | .serial-input input { 64 | font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; 65 | font-size: 0.8em; 66 | width: 90%; 67 | border-width: 1px; 68 | border-style: solid; 69 | } 70 | 71 | .serial-input input:disabled { 72 | border-width: 1px; 73 | border-style: solid; 74 | } 75 | 76 | .serial-input button { 77 | width: 8%; 78 | margin: 0 auto; 79 | } 80 | 81 | .main { 82 | flex: 1; 83 | overflow-x: hidden; 84 | overflow-y: auto; 85 | padding-top: 80px; 86 | padding-left: 1em; 87 | padding-right: 1em; 88 | } 89 | 90 | .hidden { 91 | display: none; 92 | } 93 | 94 | .controls { 95 | height: 40px; 96 | line-height: 40px; 97 | } 98 | 99 | .controls span { 100 | margin-left: 8px; 101 | } 102 | 103 | .chart-container { 104 | position: relative; 105 | height: 40vh; 106 | margin: 10px auto; 107 | } 108 | 109 | .notSupported { 110 | padding: 1em; 111 | margin-top: 1em; 112 | margin-bottom: 1em; 113 | } 114 | 115 | .row { 116 | display: flex; 117 | align-items: center; 118 | } 119 | 120 | .log { 121 | height: calc(50vh - 120px); 122 | width: 100vw; 123 | font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; 124 | font-size: 0.8em; 125 | border-width: 1px; 126 | border-style: solid; 127 | overflow-x: hidden; 128 | overflow-x: auto; 129 | transition : color 0.1s linear; 130 | } 131 | 132 | .show-calibration .log { 133 | width: 70vw ; 134 | } 135 | 136 | .calibration-container { 137 | display: none; 138 | position: relative; 139 | width: calc(30vw - 10px); 140 | height: calc(50vh - 120px); 141 | margin-left: 10px; 142 | border-width: 1px; 143 | border-style: solid; 144 | align-items: center; 145 | justify-content: center; 146 | } 147 | 148 | .show-calibration .calibration-container { 149 | display: grid; 150 | } 151 | 152 | .animation-container { 153 | position: relative; 154 | height:40vh; 155 | width: 100%; 156 | margin: 10px auto; 157 | } 158 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Adafruit 3D Model Viewer 5 | 6 | 7 | 8 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |

Adafruit 3D Model Viewer

33 |
34 |
35 |
36 | Sorry, Web Serial is not supported on this device, make sure you're 37 | running Chrome 78 or later and have enabled the 38 | #enable-experimental-web-platform-features flag in 39 | chrome://flags 40 |
41 |
42 | Sorry, WebGL is not supported on this device. 43 |
44 | 45 |
46 | 47 | 48 | 49 | 53 | 54 | Dark Mode 55 |
56 |
57 | 58 |
59 |
60 |
61 |
62 |
63 |
64 | Autoscroll 65 | Show Timestamp 66 | 67 |
68 | 69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /js/script.js: -------------------------------------------------------------------------------- 1 | // let the editor know that `Chart` is defined by some code 2 | // included in another file (in this case, `index.html`) 3 | // Note: the code will still work without this line, but without it you 4 | // will see an error in the editor 5 | /* global THREE */ 6 | /* global TransformStream */ 7 | /* global TextEncoderStream */ 8 | /* global TextDecoderStream */ 9 | 'use strict'; 10 | 11 | import * as THREE from 'three'; 12 | import {OBJLoader} from 'objloader'; 13 | 14 | let port; 15 | let reader; 16 | let inputDone; 17 | let outputDone; 18 | let inputStream; 19 | let outputStream; 20 | let showCalibration = false; 21 | 22 | let orientation = [0, 0, 0]; 23 | let quaternion = [1, 0, 0, 0]; 24 | let calibration = [0, 0, 0, 0]; 25 | 26 | const maxLogLength = 100; 27 | const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 74880, 115200, 230400, 250000, 500000, 1000000, 2000000]; 28 | const log = document.getElementById('log'); 29 | const butConnect = document.getElementById('butConnect'); 30 | const butClear = document.getElementById('butClear'); 31 | const baudRate = document.getElementById('baudRate'); 32 | const autoscroll = document.getElementById('autoscroll'); 33 | const showTimestamp = document.getElementById('showTimestamp'); 34 | const angleType = document.getElementById('angle_type'); 35 | const lightSS = document.getElementById('light'); 36 | const darkSS = document.getElementById('dark'); 37 | const darkMode = document.getElementById('darkmode'); 38 | const canvas = document.querySelector('#canvas'); 39 | const calContainer = document.getElementById('calibration'); 40 | const logContainer = document.getElementById("log-container"); 41 | 42 | fitToContainer(canvas); 43 | 44 | function fitToContainer(canvas){ 45 | // Make it visually fill the positioned parent 46 | canvas.style.width ='100%'; 47 | canvas.style.height='100%'; 48 | // ...then set the internal size to match 49 | canvas.width = canvas.offsetWidth; 50 | canvas.height = canvas.offsetHeight; 51 | } 52 | 53 | document.addEventListener('DOMContentLoaded', async () => { 54 | butConnect.addEventListener('click', clickConnect); 55 | butClear.addEventListener('click', clickClear); 56 | autoscroll.addEventListener('click', clickAutoscroll); 57 | showTimestamp.addEventListener('click', clickTimestamp); 58 | baudRate.addEventListener('change', changeBaudRate); 59 | angleType.addEventListener('change', changeAngleType); 60 | darkMode.addEventListener('click', clickDarkMode); 61 | 62 | if ('serial' in navigator) { 63 | const notSupported = document.getElementById('notSupported'); 64 | notSupported.classList.add('hidden'); 65 | } 66 | 67 | if (isWebGLAvailable()) { 68 | const webGLnotSupported = document.getElementById('webGLnotSupported'); 69 | webGLnotSupported.classList.add('hidden'); 70 | } 71 | 72 | initBaudRate(); 73 | loadAllSettings(); 74 | updateTheme(); 75 | await finishDrawing(); 76 | await render(); 77 | }); 78 | 79 | /** 80 | * @name connect 81 | * Opens a Web Serial connection to a micro:bit and sets up the input and 82 | * output stream. 83 | */ 84 | async function connect() { 85 | // - Request a port and open a connection. 86 | port = await navigator.serial.requestPort(); 87 | // - Wait for the port to open.toggleUIConnected 88 | await port.open({ baudRate: baudRate.value }); 89 | 90 | let decoder = new TextDecoderStream(); 91 | inputDone = port.readable.pipeTo(decoder.writable); 92 | inputStream = decoder.readable 93 | .pipeThrough(new TransformStream(new LineBreakTransformer())); 94 | 95 | reader = inputStream.getReader(); 96 | readLoop().catch(async function(error) { 97 | toggleUIConnected(false); 98 | await disconnect(); 99 | }); 100 | } 101 | 102 | /** 103 | * @name disconnect 104 | * Closes the Web Serial connection. 105 | */ 106 | async function disconnect() { 107 | if (reader) { 108 | await reader.cancel(); 109 | await inputDone.catch(() => {}); 110 | reader = null; 111 | inputDone = null; 112 | } 113 | 114 | if (outputStream) { 115 | await outputStream.getWriter().close(); 116 | await outputDone; 117 | outputStream = null; 118 | outputDone = null; 119 | } 120 | 121 | await port.close(); 122 | port = null; 123 | showCalibration = false; 124 | } 125 | 126 | /** 127 | * @name readLoop 128 | * Reads data from the input stream and displays it on screen. 129 | */ 130 | async function readLoop() { 131 | while (true) { 132 | const {value, done} = await reader.read(); 133 | if (value) { 134 | let plotdata; 135 | if (value.substr(0, 12) == "Orientation:") { 136 | orientation = value.substr(12).trim().split(",").map(x=>+x); 137 | } 138 | if (value.substr(0, 11) == "Quaternion:") { 139 | quaternion = value.substr(11).trim().split(",").map(x=>+x); 140 | } 141 | if (value.substr(0, 12) == "Calibration:") { 142 | calibration = value.substr(12).trim().split(",").map(x=>+x); 143 | if (!showCalibration) { 144 | showCalibration = true; 145 | updateTheme(); 146 | } 147 | } 148 | } 149 | if (done) { 150 | console.log('[readLoop] DONE', done); 151 | reader.releaseLock(); 152 | break; 153 | } 154 | } 155 | } 156 | 157 | function logData(line) { 158 | // Update the Log 159 | if (showTimestamp.checked) { 160 | let d = new Date(); 161 | let timestamp = d.getHours() + ":" + `${d.getMinutes()}`.padStart(2, 0) + ":" + 162 | `${d.getSeconds()}`.padStart(2, 0) + "." + `${d.getMilliseconds()}`.padStart(3, 0); 163 | log.innerHTML += '' + timestamp + ' -> '; 164 | d = null; 165 | } 166 | log.innerHTML += line+ "
"; 167 | 168 | // Remove old log content 169 | if (log.textContent.split("\n").length > maxLogLength + 1) { 170 | let logLines = log.innerHTML.replace(/(\n)/gm, "").split("
"); 171 | log.innerHTML = logLines.splice(-maxLogLength).join("
\n"); 172 | } 173 | 174 | if (autoscroll.checked) { 175 | log.scrollTop = log.scrollHeight 176 | } 177 | } 178 | 179 | /** 180 | * @name updateTheme 181 | * Sets the theme to Adafruit (dark) mode. Can be refactored later for more themes 182 | */ 183 | function updateTheme() { 184 | // Disable all themes 185 | document 186 | .querySelectorAll('link[rel=stylesheet].alternate') 187 | .forEach((styleSheet) => { 188 | enableStyleSheet(styleSheet, false); 189 | }); 190 | 191 | if (darkMode.checked) { 192 | enableStyleSheet(darkSS, true); 193 | } else { 194 | enableStyleSheet(lightSS, true); 195 | } 196 | 197 | if (showCalibration && !logContainer.classList.contains('show-calibration')) { 198 | logContainer.classList.add('show-calibration') 199 | } else if (!showCalibration && logContainer.classList.contains('show-calibration')) { 200 | logContainer.classList.remove('show-calibration') 201 | } 202 | } 203 | 204 | function enableStyleSheet(node, enabled) { 205 | node.disabled = !enabled; 206 | } 207 | 208 | 209 | /** 210 | * @name reset 211 | * Reset the Plotter, Log, and associated data 212 | */ 213 | async function reset() { 214 | // Clear the data 215 | log.innerHTML = ""; 216 | } 217 | 218 | /** 219 | * @name clickConnect 220 | * Click handler for the connect/disconnect button. 221 | */ 222 | async function clickConnect() { 223 | if (port) { 224 | await disconnect(); 225 | toggleUIConnected(false); 226 | return; 227 | } 228 | 229 | await connect(); 230 | 231 | reset(); 232 | 233 | toggleUIConnected(true); 234 | } 235 | 236 | /** 237 | * @name clickAutoscroll 238 | * Change handler for the Autoscroll checkbox. 239 | */ 240 | async function clickAutoscroll() { 241 | saveSetting('autoscroll', autoscroll.checked); 242 | } 243 | 244 | /** 245 | * @name clickTimestamp 246 | * Change handler for the Show Timestamp checkbox. 247 | */ 248 | async function clickTimestamp() { 249 | saveSetting('timestamp', showTimestamp.checked); 250 | } 251 | 252 | /** 253 | * @name changeBaudRate 254 | * Change handler for the Baud Rate selector. 255 | */ 256 | async function changeBaudRate() { 257 | saveSetting('baudrate', baudRate.value); 258 | } 259 | 260 | 261 | /** 262 | * @name changeAngleType 263 | * Change handler for the Baud Rate selector. 264 | */ 265 | async function changeAngleType() { 266 | saveSetting('angletype', angleType.value); 267 | } 268 | 269 | /** 270 | * @name clickDarkMode 271 | * Change handler for the Dark Mode checkbox. 272 | */ 273 | async function clickDarkMode() { 274 | updateTheme(); 275 | saveSetting('darkmode', darkMode.checked); 276 | } 277 | 278 | /** 279 | * @name clickClear 280 | * Click handler for the clear button. 281 | */ 282 | async function clickClear() { 283 | reset(); 284 | } 285 | 286 | async function finishDrawing() { 287 | return new Promise(requestAnimationFrame); 288 | } 289 | 290 | async function sleep(ms) { 291 | return new Promise(resolve => setTimeout(resolve, ms)); 292 | } 293 | 294 | /** 295 | * @name LineBreakTransformer 296 | * TransformStream to parse the stream into lines. 297 | */ 298 | class LineBreakTransformer { 299 | constructor() { 300 | // A container for holding stream data until a new line. 301 | this.container = ''; 302 | } 303 | 304 | transform(chunk, controller) { 305 | this.container += chunk; 306 | const lines = this.container.split('\n'); 307 | this.container = lines.pop(); 308 | lines.forEach(line => { 309 | controller.enqueue(line) 310 | logData(line); 311 | }); 312 | } 313 | 314 | flush(controller) { 315 | controller.enqueue(this.container); 316 | } 317 | } 318 | 319 | function convertJSON(chunk) { 320 | try { 321 | let jsonObj = JSON.parse(chunk); 322 | jsonObj._raw = chunk; 323 | return jsonObj; 324 | } catch (e) { 325 | return chunk; 326 | } 327 | } 328 | 329 | function toggleUIConnected(connected) { 330 | let lbl = 'Connect'; 331 | if (connected) { 332 | lbl = 'Disconnect'; 333 | } 334 | butConnect.textContent = lbl; 335 | updateTheme() 336 | } 337 | 338 | function initBaudRate() { 339 | for (let rate of baudRates) { 340 | var option = document.createElement("option"); 341 | option.text = rate + " Baud"; 342 | option.value = rate; 343 | baudRate.add(option); 344 | } 345 | } 346 | 347 | function loadAllSettings() { 348 | // Load all saved settings or defaults 349 | autoscroll.checked = loadSetting('autoscroll', true); 350 | showTimestamp.checked = loadSetting('timestamp', false); 351 | baudRate.value = loadSetting('baudrate', 9600); 352 | angleType.value = loadSetting('angletype', 'quaternion'); 353 | darkMode.checked = loadSetting('darkmode', false); 354 | } 355 | 356 | function loadSetting(setting, defaultValue) { 357 | let value = JSON.parse(window.localStorage.getItem(setting)); 358 | if (value == null) { 359 | return defaultValue; 360 | } 361 | 362 | return value; 363 | } 364 | 365 | let isWebGLAvailable = function() { 366 | try { 367 | var canvas = document.createElement( 'canvas' ); 368 | return !! (window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))); 369 | } catch (e) { 370 | return false; 371 | } 372 | } 373 | 374 | 375 | function updateCalibration() { 376 | // Update the Calibration Container with the values from calibration 377 | const calMap = [ 378 | {caption: "Uncalibrated", color: "#CC0000"}, 379 | {caption: "Partially Calibrated", color: "#FF6600"}, 380 | {caption: "Mostly Calibrated", color: "#FFCC00"}, 381 | {caption: "Fully Calibrated", color: "#009900"}, 382 | ]; 383 | const calLabels = [ 384 | "System", "Gyro", "Accelerometer", "Magnetometer" 385 | ] 386 | 387 | calContainer.innerHTML = ""; 388 | for (var i = 0; i < calibration.length; i++) { 389 | let calInfo = calMap[calibration[i]]; 390 | let element = document.createElement("div"); 391 | element.innerHTML = calLabels[i] + ": " + calInfo.caption; 392 | element.style = "color: " + calInfo.color; 393 | calContainer.appendChild(element); 394 | } 395 | } 396 | 397 | function saveSetting(setting, value) { 398 | window.localStorage.setItem(setting, JSON.stringify(value)); 399 | } 400 | 401 | let bunny; 402 | 403 | const renderer = new THREE.WebGLRenderer({canvas}); 404 | 405 | const camera = new THREE.PerspectiveCamera(45, canvas.width/canvas.height, 0.1, 100); 406 | camera.position.set(0, 0, 30); 407 | 408 | const scene = new THREE.Scene(); 409 | scene.background = new THREE.Color('black'); 410 | { 411 | const skyColor = 0xB1E1FF; // light blue 412 | const groundColor = 0x666666; // black 413 | const intensity = 0.5; 414 | const light = new THREE.HemisphereLight(skyColor, groundColor, intensity); 415 | scene.add(light); 416 | } 417 | 418 | { 419 | const color = 0xFFFFFF; 420 | const intensity = 1; 421 | const light = new THREE.DirectionalLight(color, intensity); 422 | light.position.set(0, 10, 0); 423 | light.target.position.set(-5, 0, 0); 424 | scene.add(light); 425 | scene.add(light.target); 426 | } 427 | 428 | { 429 | const objLoader = new OBJLoader(); 430 | objLoader.load('assets/bunny.obj', (root) => { 431 | bunny = root; 432 | scene.add(root); 433 | }); 434 | } 435 | 436 | function resizeRendererToDisplaySize(renderer) { 437 | const canvas = renderer.domElement; 438 | const width = canvas.clientWidth; 439 | const height = canvas.clientHeight; 440 | const needResize = canvas.width !== width || canvas.height !== height; 441 | if (needResize) { 442 | renderer.setSize(width, height, false); 443 | } 444 | return needResize; 445 | } 446 | 447 | async function render() { 448 | if (resizeRendererToDisplaySize(renderer)) { 449 | const canvas = renderer.domElement; 450 | camera.aspect = canvas.clientWidth / canvas.clientHeight; 451 | camera.updateProjectionMatrix(); 452 | } 453 | 454 | if (bunny != undefined) { 455 | if (angleType.value == "euler") { 456 | if (showCalibration) { 457 | // BNO055 458 | let rotationEuler = new THREE.Euler( 459 | THREE.MathUtils.degToRad(360 - orientation[2]), 460 | THREE.MathUtils.degToRad(orientation[0]), 461 | THREE.MathUtils.degToRad(orientation[1]), 462 | 'YZX' 463 | ); 464 | bunny.setRotationFromEuler(rotationEuler); 465 | } else { 466 | let rotationEuler = new THREE.Euler( 467 | THREE.MathUtils.degToRad(orientation[2]), 468 | THREE.MathUtils.degToRad(orientation[0]-180), 469 | THREE.MathUtils.degToRad(-orientation[1]), 470 | 'YZX' 471 | ); 472 | bunny.setRotationFromEuler(rotationEuler); 473 | } 474 | } else { 475 | let rotationQuaternion = new THREE.Quaternion(quaternion[1], quaternion[3], -quaternion[2], quaternion[0]); 476 | bunny.setRotationFromQuaternion(rotationQuaternion); 477 | } 478 | } 479 | 480 | renderer.render(scene, camera); 481 | updateCalibration(); 482 | await sleep(10); // Allow 10ms for UI updates 483 | await finishDrawing(); 484 | await render(); 485 | } 486 | --------------------------------------------------------------------------------