├── .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 |
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 | Connect
47 |
48 |
49 |
50 | Quaternions
51 | Euler Angles
52 |
53 |
54 | Dark Mode
55 |
56 |
57 |
58 |
59 |
63 |
64 | Autoscroll
65 | Show Timestamp
66 | Clear
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 |
--------------------------------------------------------------------------------