60 | Sorry, Web Bluetooth is not supported on this device, make sure you're
61 | running Chrome 56 or later. If you are running linux, make sure you have enabled the
62 | #enable-experimental-web-platform-features flag in
63 | chrome://flags
64 |
65 |
66 |
67 | Web Bluetooth Dashboard
68 |
69 |
70 |
73 |
74 |
75 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
109 |
110 |
111 |
112 | title
113 |
114 |
115 |
116 | content
117 |
118 |
119 |
120 |
121 |
122 | title
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | title
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | title
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | title
152 |
153 |
154 |
155 |
156 |
157 |
158 | 100%
159 |
160 |
161 |
162 |
163 |
164 | title
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | title
175 |
176 |
177 |
178 |
179 |
180 |
181 | title
182 |
183 |
184 |
185 |
186 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
--------------------------------------------------------------------------------
/js/colorjoe.min.js:
--------------------------------------------------------------------------------
1 | /*! colorjoe - v4.1.1 - Juho Vepsalainen - MIT
2 | https://bebraw.github.com/colorjoe - 2020-01-27 */
3 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):e.colorjoe=n()}(this,function(){"use strict";"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function e(e,n){return e(n={exports:{}},n.exports),n.exports}var p=e(function(e,n){e.exports=function(){function r(e,n){e?(t(e,n,"touchstart","touchmove","touchend"),t(e,n,"mousedown","mousemove","mouseup")):console.warn("drag is missing elem!")}return r.xyslider=function(e){var n=i(e.class||"",e.parent),t=i("pointer",n);return i("shape shape1",t),i("shape shape2",t),i("bg bg1",n),i("bg bg2",n),r(n,a(e.cbs,t)),{background:n,pointer:t}},r.slider=function(e){var n=i(e.class,e.parent),t=i("pointer",n);return i("shape",t),i("bg",n),r(n,a(e.cbs,t)),{background:n,pointer:t}},r;function a(e,t){var n={};for(var r in e)n[r]=a(e[r]);function a(n){return function(e){e.pointer=t,n(e)}}return n}function i(e,n){return t="div",r=e,a=n,i=document.createElement(t),r&&(i.className=r),a.appendChild(i),i;var t,r,a,i}function t(r,e,n,a,i){var t,o,u,s=(e=(t=e)?{begin:t.begin||p,change:t.change||p,end:t.end||p}:{begin:function(e){o={x:e.elem.offsetLeft,y:e.elem.offsetTop},u=e.cursor},change:function(e){d(e.elem,"left",o.x+e.cursor.x-u.x+"px"),d(e.elem,"top",o.y+e.cursor.y-u.y+"px")},end:p}).begin,l=e.change,f=e.end;c(r,n,function(n){var t=function(e){var n=Array.prototype.slice,t=n.apply(arguments,[1]);return function(){return e.apply(null,t.concat(n.apply(arguments)))}}(g,l,r);c(document,a,t),c(document,i,function e(){h(document,a,t),h(document,i,e),g(f,r,n)}),g(s,r,n)})}function c(e,n,t){var r=!1;try{var a=Object.defineProperty({},"passive",{get:function(){r=!0}});window.addEventListener("testPassive",null,a),window.removeEventListener("testPassive",null,a)}catch(e){}e.addEventListener(n,t,!!r&&{passive:!1})}function h(e,n,t){e.removeEventListener(n,t,!1)}function d(e,n,t){e.style[n]=t}function p(){}function g(e,n,t){t.preventDefault();var r,a,i,o={x:(r=n.getBoundingClientRect()).left,y:r.top},u=n.clientWidth,s=n.clientHeight,l={x:(i=t,(i.touches?i.touches[i.touches.length-1]:i).clientX),y:(a=t,(a.touches?a.touches[a.touches.length-1]:a).clientY)},f=(l.x-o.x)/u,c=(l.y-o.y)/s;e({x:isNaN(f)?0:f,y:isNaN(c)?0:c,cursor:l,elem:n,e:t})}}()}),a=e(function(e,n){e.exports=function(){function c(e){if(Array.isArray(e)){if("string"==typeof e[0]&&"function"==typeof c[e[0]])return new c[e[0]](e.slice(1,e.length));if(4===e.length)return new c.RGB(e[0]/255,e[1]/255,e[2]/255,e[3]/255)}else if("string"==typeof e){var n=e.toLowerCase();c.namedColors[n]&&(e="#"+c.namedColors[n]),"transparent"===n&&(e="rgba(0,0,0,0)");var t=e.match(p);if(t){var r=t[1].toUpperCase(),a=h(t[8])?t[8]:parseFloat(t[8]),i="H"===r[0],o=t[3]?100:i?360:255,u=t[5]||i?100:255,s=t[7]||i?100:255;if(h(c[r]))throw new Error("color."+r+" is not installed.");return new c[r](parseFloat(t[2])/o,parseFloat(t[4])/u,parseFloat(t[6])/s,a)}e.length<6&&(e=e.replace(/^#?([0-9a-f])([0-9a-f])([0-9a-f])$/i,"$1$1$2$2$3$3"));var l=e.match(/^#?([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$/i);if(l)return new c.RGB(parseInt(l[1],16)/255,parseInt(l[2],16)/255,parseInt(l[3],16)/255);if(c.CMYK){var f=e.match(new RegExp("^cmyk\\("+d.source+","+d.source+","+d.source+","+d.source+"\\)$","i"));if(f)return new c.CMYK(parseFloat(f[1])/100,parseFloat(f[2])/100,parseFloat(f[3])/100,parseFloat(f[4])/100)}}else if("object"==typeof e&&e.isColor)return e;return!1}var u=[],h=function(e){return void 0===e},e=/\s*(\.\d+|\d+(?:\.\d+)?)(%)?\s*/,d=/\s*(\.\d+|100|\d?\d(?:\.\d+)?)%\s*/,p=new RegExp("^(rgb|hsl|hsv)a?\\("+e.source+","+e.source+","+e.source+"(?:,"+/\s*(\.\d+|\d+(?:\.\d+)?)\s*/.source+")?\\)$","i");c.namedColors={},c.installColorSpace=function(a,i,e){function n(e,r){var n={};for(var t in n[r.toLowerCase()]=function(){return this.rgb()[r.toLowerCase()]()},c[r].propertyNames.forEach(function(t){var e="black"===t?"k":t.charAt(0);n[t]=n[e]=function(e,n){return this[r.toLowerCase()]()[t](e,n)}}),n)n.hasOwnProperty(t)&&void 0===c[e].prototype[t]&&(c[e].prototype[t]=n[t])}(c[a]=function(e){var r=Array.isArray(e)?e:arguments;i.forEach(function(e,n){var t=r[n];if("alpha"===e)this._alpha=isNaN(t)||1n)return!1;return!0},r.toJSON=function(){return[a].concat(i.map(function(e){return this["_"+e]},this))},e)if(e.hasOwnProperty(t)){var o=t.match(/^from(.*)$/);o?c[o[1].toUpperCase()].prototype[a.toLowerCase()]=e[t]:r[t]=e[t]}return r[a.toLowerCase()]=function(){return this},r.toString=function(){return"["+a+" "+i.map(function(e){return this["_"+e]},this).join(", ")+"]"},i.forEach(function(t){var e="black"===t?"k":t.charAt(0);r[t]=r[e]=function(n,e){return void 0===n?this["_"+t]:e?new this.constructor(i.map(function(e){return this["_"+e]+(t===e?n:0)},this)):new this.constructor(i.map(function(e){return t===e?n:this["_"+e]},this))}}),u.forEach(function(e){n(a,e),n(e,a)}),u.push(a),c},c.pluginList=[],c.use=function(e){return-1===c.pluginList.indexOf(e)&&(this.pluginList.push(e),e(c)),c},c.installMethod=function(n,t){return u.forEach(function(e){c[e].prototype[n]=t}),this},c.installColorSpace("RGB",["red","green","blue","alpha"],{hex:function(){var e=(65536*Math.round(255*this._red)+256*Math.round(255*this._green)+Math.round(255*this._blue)).toString(16);return"#"+"00000".substr(0,6-e.length)+e},hexa:function(){var e=Math.round(255*this._alpha).toString(16);return"#"+"00".substr(0,2-e.length)+e+this.hex().substr(1,6)},css:function(){return"rgb("+Math.round(255*this._red)+","+Math.round(255*this._green)+","+Math.round(255*this._blue)+")"},cssa:function(){return"rgba("+Math.round(255*this._red)+","+Math.round(255*this._green)+","+Math.round(255*this._blue)+","+this._alpha+")"}});var n=function(a){a.installColorSpace("XYZ",["x","y","z","alpha"],{fromRgb:function(){var e=function(e){return.04045t[e]?r[e]=(n[e]-t[e])/(1-t[e]):n[e]>t[e]?r[e]=(t[e]-n[e])/t[e]:r[e]=0}),r._red>r._green?r._red>r._blue?n._alpha=r._red:n._alpha=r._blue:r._green>r._blue?n._alpha=r._green:n._alpha=r._blue,n._alpha<1e-10||(a.forEach(function(e){n[e]=(n[e]-t[e])/n._alpha+t[e]}),n._alpha*=r._alpha),n})})}()}),g=n(b,"div");function b(e,n,t){var r=document.createElement(e);return r.className=n,t.appendChild(r),r}function n(e){var n=Array.prototype.slice,t=n.apply(arguments,[1]);return function(){return e.apply(null,t.concat(n.apply(arguments)))}}function t(e,n,t){return Math.min(Math.max(e,n),t)}var v={clamp:t,e:b,div:g,partial:n,labelInput:function(e,n,t,r){var a="colorPickerInput"+Math.floor(1001*Math.random()),i=g(e,t);return{label:(c=n,h=i,d=a,p=b("label","",h),p.innerHTML=c,d&&p.setAttribute("for",d),p),input:(o="text",u=i,s=r,l=a,f=b("input","",u),f.type=o,s&&(f.maxLength=s),l&&f.setAttribute("id",l),s&&(f.maxLength=s),f)};var o,u,s,l,f;var c,h,d,p},X:function(e,n){e.style.left=t(100*n,0,100)+"%"},Y:function(e,n){e.style.top=t(100*n,0,100)+"%"},BG:function(e,n){e.style.background=n}};var r={currentColor:function(e){var n=v.div("currentColorContainer",e),t=v.div("currentColor",n);return{change:function(e){v.BG(t,e.cssa())}}},fields:function(e,t,n){var r=n.space,a=n.limit||255,i=0<=n.fix?n.fix:0,o=(""+a).length+i;o=i?o+1:o;var u=r.split(""),s="A"==r[r.length-1];if(r=s?r.slice(0,-1):r,["RGB","HSL","HSV","CMYK"].indexOf(r)<0)return console.warn("Invalid field names",r);var l=v.div("colorFields",e),f=u.map(function(e){e=e.toLowerCase();var n=v.labelInput("color "+e,e,l,o);return n.input.onblur=c,n.input.onkeydown=h,n.input.onkeyup=d,{name:e,e:n}});function c(){t.done()}function h(e){e.ctrlKey||e.altKey||!/^[a-zA-Z]$/.test(e.key)||e.preventDefault()}function d(){var n=[r];f.forEach(function(e){n.push(e.e.input.value/a)}),s||n.push(t.getAlpha()),t.set(n)}return{change:function(n){f.forEach(function(e){e.e.input.value=(n[e.name]()*a).toFixed(i)})}}},hex:function(e,r,n){var t=v.labelInput("hex",n.label||"",e,7);return t.input.value="#",t.input.onkeyup=function(e){var n=e.keyCode||e.which,t=e.target.value;t=function(e,n,t){for(var r=e,a=e.length;a {
142 | if (dataset.data.length > this.maxBufferSize) {
143 | dataset.data.shift();
144 | dataset.pointRadius = this.pointRadiusLast(5, dataset.data.length);
145 | }
146 | }
147 | )
148 | this.update();
149 | },
150 | dataset: function(dataSetIndex) {
151 | return this.adaChart.data.datasets[dataSetIndex];
152 | },
153 | setBufferSize: function(size) {
154 | this.maxBufferSize = size;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/js/scale.fix.js:
--------------------------------------------------------------------------------
1 | let fixScale = function(doc) {
2 |
3 | var addEvent = 'addEventListener',
4 | type = 'gesturestart',
5 | qsa = 'querySelectorAll',
6 | scales = [1, 1],
7 | meta = qsa in doc ? doc[qsa]('meta[name=viewport]') : [];
8 |
9 | function fix() {
10 | meta.content = 'width=device-width,minimum-scale=' + scales[0] + ',maximum-scale=' + scales[1];
11 | doc.removeEventListener(type, fix, true);
12 | }
13 |
14 | if ((meta = meta[meta.length - 1]) && addEvent in doc) {
15 | fix();
16 | scales = [.25, 1.6];
17 | doc[addEvent](type, fix, true);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/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 Chart */
6 | /* global Graph */
7 | /* global numeral */
8 | /* global colorjoe */
9 |
10 | 'use strict';
11 |
12 | import * as THREE from 'three';
13 | import {GLTFLoader} from 'gltfloader';
14 |
15 | let device;
16 |
17 | const bufferSize = 64;
18 | const colors = ['#00a7e9', '#f89521', '#be1e2d'];
19 | const measurementPeriodId = '0001';
20 |
21 | const maxLogLength = 500;
22 | const log = document.getElementById('log');
23 | const butConnect = document.getElementById('butConnect');
24 | const butClear = document.getElementById('butClear');
25 | const autoscroll = document.getElementById('autoscroll');
26 | const showTimestamp = document.getElementById('showTimestamp');
27 | const lightSS = document.getElementById('light');
28 | const darkSS = document.getElementById('dark');
29 | const darkMode = document.getElementById('darkmode');
30 | const dashboard = document.getElementById('dashboard');
31 | const fpsCounter = document.getElementById("fpsCounter");
32 | const knownOnly = document.getElementById("knownonly");
33 |
34 | let colorIndex = 0;
35 | let activePanels = [];
36 | let bytesReceived = 0;
37 | let currentBoard;
38 | let buttonState = 0;
39 |
40 | document.addEventListener('DOMContentLoaded', async () => {
41 | butConnect.addEventListener('click', clickConnect);
42 | butClear.addEventListener('click', clickClear);
43 | autoscroll.addEventListener('click', clickAutoscroll);
44 | showTimestamp.addEventListener('click', clickTimestamp);
45 | darkMode.addEventListener('click', clickDarkMode);
46 | knownOnly.addEventListener('click', clickKnownOnly);
47 |
48 | if ('bluetooth' in navigator) {
49 | const notSupported = document.getElementById('notSupported');
50 | notSupported.classList.add('hidden');
51 | }
52 |
53 | loadAllSettings();
54 | updateTheme();
55 | await updateAllPanels();
56 | //createMockPanels();
57 | });
58 |
59 | const boards = {
60 | CLUE: {
61 | colorOrder: 'GRB',
62 | neopixels: 1,
63 | hasSwitch: false,
64 | buttons: 2,
65 | },
66 | CPlay: {
67 | colorOrder: 'GRB',
68 | neopixels: 10,
69 | hasSwitch: true,
70 | buttons: 2,
71 | },
72 | Sense: {
73 | colorOrder: 'GRB',
74 | neopixels: 1,
75 | hasSwitch: false,
76 | buttons: 1,
77 | },
78 | unknown: {
79 | colorOrder: 'GRB',
80 | neopixels: 1,
81 | hasSwitch: false,
82 | buttons: 1,
83 | }
84 | }
85 |
86 | let panels = {
87 | battery: {
88 | title: 'Battery Level',
89 | serviceId: 'battery_service',
90 | characteristicId: 'battery_level',
91 | panelType: "custom",
92 | structure: ['Uint8'],
93 | data: {battery:[]},
94 | properties: ['notify'],
95 | textFormat: function(value) {
96 | return numeral(value).format('0.0') + '%';
97 | },
98 | create: function(panelId) {
99 | let panelTemplate = loadPanelTemplate(panelId, 'battery-level');
100 | this.update(panelId);
101 | },
102 | update: function(panelId) {
103 | let panelElement = document.querySelector("#dashboard > #" + panelId);
104 | let value = null;
105 | if (panels[panelId].data.battery.length > 0) {
106 | value = panels[panelId].data.battery.pop();
107 | panels[panelId].data.battery = [];
108 | }
109 |
110 | if (value != null && value <= 25) { // Show Red
111 | panelElement.querySelector(".content .battery").classList.remove("battery-middle");
112 | panelElement.querySelector(".content .battery").classList.add("battery-alert");
113 | } else if (value == null || value <= 50) { // Show Yellow
114 | panelElement.querySelector(".content .battery").classList.remove("battery-alert");
115 | panelElement.querySelector(".content .battery").classList.add("battery-middle");
116 | } else { // Show Green
117 | panelElement.querySelector(".content .battery").classList.remove("battery-middle");
118 | panelElement.querySelector(".content .battery").classList.remove("battery-alert");
119 | }
120 |
121 | if (value == null) {
122 | panelElement.querySelector(".content .percentage").innerHTML = '?';
123 | panelElement.querySelector(".content .battery .level").style.width = '100%';
124 | panelElement.querySelector(".content .battery").title = 'Battery Level: ?';
125 | } else {
126 | panelElement.querySelector(".content .battery .level").style.width = value + '%';
127 | value = panels[panelId].textFormat(value);
128 | panelElement.querySelector(".content .percentage").innerHTML = value;
129 | panelElement.querySelector(".content .battery").title = 'Battery Level: ' + value;
130 | }
131 | },
132 | },
133 | temperature: {
134 | serviceId: '0100',
135 | characteristicId: '0101',
136 | panelType: "graph",
137 | structure: ['Float32'],
138 | data: {temperature:[]},
139 | properties: ['notify'],
140 | textFormat: function(value) {
141 | return numeral((9 / 5 * value) + 32).format('0.00') + '° F';
142 | },
143 | },
144 | light: {
145 | serviceId: '0300',
146 | characteristicId: '0301',
147 | panelType: "graph",
148 | structure: ['Float32'],
149 | data: {light:[]},
150 | properties: ['notify'],
151 | },
152 | accelerometer: {
153 | serviceId: '0200',
154 | characteristicId: '0201',
155 | panelType: "graph",
156 | structure: ['Float32', 'Float32', 'Float32'],
157 | data: {x:[], y:[], z:[]},
158 | properties: ['notify'],
159 | textFormat: function(value) {
160 | return numeral(value).format('0.00');
161 | },
162 | measurementPeriod: 500,
163 | },
164 | gyroscope: {
165 | serviceId: '0400',
166 | characteristicId: '0401',
167 | panelType: "graph",
168 | structure: ['Float32', 'Float32', 'Float32'],
169 | data: {x:[], y:[], z:[]},
170 | properties: ['notify'],
171 | textFormat: function(value) {
172 | return numeral(value).format('0.00');
173 | },
174 | measurementPeriod: 500,
175 | },
176 | magnetometer: {
177 | serviceId: '0500',
178 | characteristicId: '0501',
179 | panelType: "graph",
180 | structure: ['Float32', 'Float32', 'Float32'],
181 | data: {x:[], y:[], z:[]},
182 | properties: ['notify'],
183 | textFormat: function(value) {
184 | return numeral(value).format('0.00') + ' µT';
185 | },
186 | measurementPeriod: 500,
187 | },
188 | buttons: {
189 | serviceId: '0600',
190 | characteristicId: '0601',
191 | panelType: "custom",
192 | structure: ['Uint32'],
193 | data: {buttonState:[]},
194 | properties: ['notify'],
195 | create: function(panelId) {
196 | let panelTemplate = loadPanelTemplate(panelId, 'onboard-buttons');
197 | for (let i = 0; i < currentBoard.buttons; i++) {
198 | let buttonTemplate = document.querySelector("#templates > .roundbutton").cloneNode(true);
199 | buttonTemplate.id = "button_" + (i + 1);
200 | buttonTemplate.querySelector(".text").innerHTML = String.fromCharCode(65 + i);
201 | panelTemplate.querySelector(".content").appendChild(buttonTemplate);
202 | }
203 | },
204 | update: function(panelId) {
205 | let panelElement = document.querySelector("#dashboard > #" + panelId);
206 | buttonState = panels[panelId].data.buttonState.pop();
207 | if (panels.switch.condition()) {
208 | panels.switch.update('switch'); // Update the switch because we aren't doing 2 notifys
209 | }
210 | // Match the buttons to the values
211 | for (let i = 1; i <= currentBoard.buttons; i++) {
212 | if (buttonState & (1 << i)) {
213 | panelElement.querySelector("#button_" + i + " .roundbtn").classList.add("pressed");
214 | } else {
215 | panelElement.querySelector("#button_" + i + " .roundbtn").classList.remove("pressed");
216 | }
217 | }
218 | },
219 | },
220 | switch: {
221 | serviceId: '0600',
222 | characteristicId: '0601',
223 | panelType: "custom",
224 | structure: ['Uint32'],
225 | data: {buttonState:[]},
226 | properties: [],
227 | condition: function() {
228 | return currentBoard.hasSwitch;
229 | },
230 | create: function(panelId) {
231 | let panelTemplate = loadPanelTemplate(panelId, 'onboard-switch');
232 | this.update(panelId);
233 | },
234 | update: function(panelId) {
235 | // UI Only Update
236 | let panelElement = document.querySelector("#dashboard > #" + panelId);
237 | panelElement.querySelector(".content #onboardSwitch").checked = buttonState & 1;
238 | },
239 | },
240 | humidity: {
241 | serviceId: '0700',
242 | characteristicId: '0701',
243 | panelType: "graph",
244 | structure: ['Float32'],
245 | data: {humidity:[]},
246 | properties: ['notify'],
247 | textFormat: function(value) {
248 | return numeral(value).format('0.0') + '%';
249 | },
250 | },
251 | barometric_pressure: {
252 | serviceId: '0800',
253 | characteristicId: '0801',
254 | panelType: "graph",
255 | structure: ['Float32'],
256 | data: {barometric:[]},
257 | properties: ['notify'],
258 | textFormat: function(value) {
259 | return numeral(value).format('0.00') + ' hPA';
260 | },
261 | },
262 | tone: {
263 | serviceId: '0c00',
264 | characteristicId: '0c01',
265 | panelType: "custom",
266 | create: function(panelId) {
267 | let panelTemplate = loadPanelTemplate(panelId, 'play-button');
268 | panelTemplate.querySelector(".content .button").onclick = function() {
269 | let button = this;
270 | button.disabled = true;
271 | playSound(440, 1000, function() {button.disabled = false;})
272 | }
273 | this.packetSequence = this.structure;
274 | },
275 | structure: ['Uint16', 'Uint32'],
276 | properties: ['write'],
277 | },
278 | neopixel: {
279 | serviceId: '0900',
280 | characteristicId: '0903',
281 | panelType: "color",
282 | structure: ['Uint16', 'Uint8', 'Uint8[]'],
283 | data: {R:[],G:[],B:[]},
284 | properties: ['write'],
285 | },
286 | 'model3d': {
287 | title: '3D Model',
288 | serviceId: '0d00',
289 | characteristicId: '0d01',
290 | panelType: "model3d",
291 | structure: ['Float32', 'Float32', 'Float32', 'Float32'],
292 | data: {w:[],x:[], y:[], z:[]},
293 | style: "font-size: 16px;",
294 | properties: ['notify'],
295 | textFormat: function(value) {
296 | return numeral(value).format('0.00') + ' rad';
297 | },
298 | measurementPeriod: 200,
299 | },
300 | };
301 |
302 | function playSound(frequency, duration, callback) {
303 | if (callback === undefined) {
304 | callback = function() {};
305 | }
306 |
307 | let value = encodePacket('tone', [frequency, duration]);
308 | panels.tone.characteristic.writeValue(value)
309 | .catch(error => {console.log(error);})
310 | .then(callback);
311 | }
312 |
313 | function encodePacket(panelId, values) {
314 | const typeMap = {
315 | "Uint8": {fn: DataView.prototype.setUint8, bytes: 1},
316 | "Uint16": {fn: DataView.prototype.setUint16, bytes: 2},
317 | "Uint32": {fn: DataView.prototype.setUint32, bytes: 4},
318 | "Int32": {fn: DataView.prototype.setInt32, bytes: 4},
319 | "Float32": {fn: DataView.prototype.setFloat32, bytes: 4},
320 | };
321 |
322 | if (values.length != panels[panelId].packetSequence.length) {
323 | logMsg("Error in encodePacket(): Number of arguments must match structure");
324 | return false;
325 | }
326 |
327 | let bufferSize = 0, packetPointer = 0;
328 | panels[panelId].packetSequence.forEach(function(dataType) {
329 | bufferSize += typeMap[dataType].bytes;
330 | });
331 |
332 | let view = new DataView(new ArrayBuffer(bufferSize));
333 |
334 | for (var i = 0; i < values.length; i++) {
335 | let dataType = panels[panelId].packetSequence[i];
336 | let dataViewFn = typeMap[dataType].fn.bind(view);
337 | dataViewFn(packetPointer, values[i], true);
338 | packetPointer += typeMap[dataType].bytes;
339 | }
340 |
341 | return view.buffer;
342 | }
343 |
344 | /**
345 | * @name connect
346 | * Opens a Web Serial connection to a micro:bit and sets up the input and
347 | * output stream.
348 | */
349 | async function connect() {
350 | // - Request a port and open a connection.
351 | if (!device) {
352 | logMsg('Connecting to device ...');
353 | let services = [];
354 | for (let panelId of Object.keys(panels)) {
355 | services.push(getFullId(panels[panelId].serviceId));
356 | }
357 | if (knownOnly.checked) {
358 | let knownBoards = Object.keys(boards);
359 | knownBoards.pop();
360 | let filters = [];
361 | for(let board of knownBoards) {
362 | filters.push({name: board});
363 | }
364 | device = await navigator.bluetooth.requestDevice({
365 | filters: filters,
366 | optionalServices: services,
367 | });
368 | } else {
369 | device = await navigator.bluetooth.requestDevice({
370 | acceptAllDevices: true,
371 | optionalServices: services,
372 | });
373 | }
374 | }
375 | if (device) {
376 | logMsg("Connected to device " + device.name);
377 | if (boards.hasOwnProperty(device.name)) {
378 | currentBoard = boards[device.name];
379 | } else {
380 | currentBoard = boards.unknown;
381 | }
382 | device.addEventListener('gattserverdisconnected', onDisconnected);
383 | let server = await device.gatt.connect();
384 | const availableServices = await server.getPrimaryServices();
385 |
386 | // Create the panels only if service available
387 | for (let panelId of Object.keys(panels)) {
388 | if (panels[panelId].condition == undefined || panels[panelId].condition()) {
389 | if (getFullId(panels[panelId].serviceId).substr(0, 4) == "adaf") {
390 | for (const service of availableServices) {
391 | if (getFullId(panels[panelId].serviceId) == service.uuid) {
392 | createPanel(panelId);
393 | }
394 | }
395 | } else {
396 | // Non-custom ones such as battery are always active
397 | createPanel(panelId);
398 | }
399 | }
400 | }
401 |
402 | reset();
403 |
404 | for (let panelId of activePanels) {
405 | let service = await server.getPrimaryService(getFullId(panels[panelId].serviceId)).catch(error => {console.log(error);});
406 | if (service) {
407 | panels[panelId].characteristic = await service.getCharacteristic(getFullId(panels[panelId].characteristicId)).catch(error => {console.log(error);});
408 | logMsg('');
409 | logMsg('Characteristic Information');
410 | logMsg('---------------------------');
411 | logMsg('> Sensor: ' + ucWords(panelId));
412 | logMsg('> Characteristic UUID: ' + panels[panelId].characteristic.uuid);
413 | logMsg('> Broadcast: ' + panels[panelId].characteristic.properties.broadcast);
414 | logMsg('> Read: ' + panels[panelId].characteristic.properties.read);
415 | logMsg('> Write w/o response: ' + panels[panelId].characteristic.properties.writeWithoutResponse);
416 | logMsg('> Write: ' + panels[panelId].characteristic.properties.write);
417 | logMsg('> Notify: ' + panels[panelId].characteristic.properties.notify);
418 | logMsg('> Indicate: ' + panels[panelId].characteristic.properties.indicate);
419 | logMsg('> Signed Write: ' + panels[panelId].characteristic.properties.authenticatedSignedWrites);
420 | logMsg('> Queued Write: ' + panels[panelId].characteristic.properties.reliableWrite);
421 | logMsg('> Writable Auxiliaries: ' + panels[panelId].characteristic.properties.writableAuxiliaries);
422 |
423 | if (panels[panelId].properties.includes("notify")) {
424 | if (panels[panelId].measurementPeriod !== undefined) {
425 | let mpChar = await service.getCharacteristic(getFullId(measurementPeriodId)).catch(error => {console.log(error);});
426 | let view = new DataView(new ArrayBuffer(4));
427 | view.setInt32(0, panels[panelId].measurementPeriod, true);
428 | mpChar.writeValue(view.buffer)
429 | .catch(error => {console.log(error);})
430 | .then(_ => {
431 | logMsg("Changed measurement period for " + ucWords(panelId) + " to " + panels[panelId].measurementPeriod + "ms");
432 | });
433 | }
434 | logMsg('Starting notifications for ' + ucWords(panelId));
435 | await panels[panelId].characteristic.startNotifications();
436 | panels[panelId].characteristic.addEventListener('characteristicvaluechanged', function(event){handleIncoming(panelId, event.target.value);});
437 | }
438 | if (panels[panelId].properties.includes("read")) {
439 | let intervalPeriod = 1000;
440 | if (panels[panelId].measurementPeriod !== undefined) {
441 | intervalPeriod = panels[panelId].measurementPeriod;
442 | }
443 | panels[panelId].polling = setInterval(function() {
444 | if (!panels[panelId].readInProgress) {
445 | panels[panelId].readInProgress = true;
446 | panels[panelId].characteristic.readValue()
447 | .then(function(data) {
448 | handleIncoming(panelId, data);
449 | }).catch(error => {});
450 | panels[panelId].readInProgress = false;
451 | }
452 | }, intervalPeriod);
453 | }
454 | }
455 | }
456 | readActiveSensors();
457 | }
458 | }
459 |
460 | async function readActiveSensors() {
461 | for (let panelId of activePanels) {
462 | let panel = panels[panelId];
463 | if (panels[panelId].properties.includes("read") || panels[panelId].properties.includes("notify")) {
464 | await panels[panelId].characteristic.readValue().then(function(data){handleIncoming(panelId, data);});
465 | }
466 | }
467 | }
468 |
469 | function handleIncoming(panelId, value) {
470 | const columns = Object.keys(panels[panelId].data);
471 | const typeMap = {
472 | "Uint8": {fn: DataView.prototype.getUint8, bytes: 1},
473 | "Uint16": {fn: DataView.prototype.getUint16, bytes: 2},
474 | "Uint32": {fn: DataView.prototype.getUint32, bytes: 4},
475 | "Float32": {fn: DataView.prototype.getFloat32, bytes: 4}
476 | };
477 |
478 | let packetPointer = 0, i = 0;
479 | panels[panelId].structure.forEach(function(dataType) {
480 | let dataViewFn = typeMap[dataType].fn.bind(value);
481 | let unpackedValue = dataViewFn(packetPointer, true);
482 | panels[panelId].data[columns[i]].push(unpackedValue);
483 | if (panels[panelId].data[columns[i]].length > bufferSize) {
484 | panels[panelId].data[columns[i]].shift();
485 | }
486 | packetPointer += typeMap[dataType].bytes;
487 | bytesReceived += typeMap[dataType].bytes;
488 | i++;
489 | });
490 |
491 | panels[panelId].rendered = false;
492 | }
493 |
494 | /**
495 | * @name disconnect
496 | * Closes the Web Bluetooth connection.
497 | */
498 | async function disconnect() {
499 | if (device && device.gatt.connected) {
500 | device.gatt.disconnect();
501 | }
502 | }
503 |
504 | function getFullId(shortId) {
505 | if (shortId.length == 4) {
506 | return 'adaf' + shortId + '-c332-42a8-93bd-25e905756cb8';
507 | }
508 | return shortId;
509 | }
510 |
511 | function logMsg(text) {
512 | // Update the Log
513 | if (showTimestamp.checked) {
514 | let d = new Date();
515 | let timestamp = d.getHours() + ":" + `${d.getMinutes()}`.padStart(2, 0) + ":" +
516 | `${d.getSeconds()}`.padStart(2, 0) + "." + `${d.getMilliseconds()}`.padStart(3, 0);
517 | log.innerHTML += '' + timestamp + ' -> ';
518 | d = null;
519 | }
520 | log.innerHTML += text+ " ";
521 |
522 | // Remove old log content
523 | if (log.textContent.split("\n").length > maxLogLength + 1) {
524 | let logLines = log.innerHTML.replace(/(\n)/gm, "").split(" ");
525 | log.innerHTML = logLines.splice(-maxLogLength).join(" \n");
526 | }
527 |
528 | if (autoscroll.checked) {
529 | log.scrollTop = log.scrollHeight
530 | }
531 | }
532 |
533 | /**
534 | * @name updateTheme
535 | * Sets the theme to Adafruit (dark) mode. Can be refactored later for more themes
536 | */
537 | function updateTheme() {
538 | // Disable all themes
539 | document
540 | .querySelectorAll('link[rel=stylesheet].alternate')
541 | .forEach((styleSheet) => {
542 | enableStyleSheet(styleSheet, false);
543 | });
544 |
545 | if (darkMode.checked) {
546 | enableStyleSheet(darkSS, true);
547 | } else {
548 | enableStyleSheet(lightSS, true);
549 | }
550 | }
551 |
552 | function enableStyleSheet(node, enabled) {
553 | node.disabled = !enabled;
554 | }
555 |
556 | /**
557 | * @name reset
558 | * Reset the Panels, Log, and associated data
559 | */
560 | async function reset() {
561 | // Clear the data
562 | clearGraphData();
563 |
564 | // Clear all Panel Data
565 | for (let panelId of activePanels) {
566 | let panel = panels[panelId];
567 | if (panels[panelId].data !== undefined) {
568 | Object.entries(panels[panelId].data).forEach(([field, item], index) => {
569 | panels[panelId].data[field] = [];
570 | });
571 | }
572 | panels[panelId].rendered = false;
573 | }
574 |
575 | bytesReceived = 0;
576 | colorIndex = 0;
577 |
578 | // Clear the log
579 | log.innerHTML = "";
580 | }
581 |
582 | /**
583 | * @name clickConnect
584 | * Click handler for the connect/disconnect button.
585 | */
586 | async function clickConnect() {
587 | if (device && device.gatt.connected) {
588 | await disconnect();
589 | return;
590 | }
591 |
592 | await connect().then(_ => {toggleUIConnected(true);}).catch(() => {});
593 | }
594 |
595 | async function onDisconnected(event) {
596 | let disconnectedDevice = event.target;
597 |
598 | for (let panelId of activePanels) {
599 | if (typeof panels[panelId].polling !== 'undefined') {
600 | clearInterval(panels[panelId].polling);
601 | }
602 | }
603 |
604 | // Loop through activePanels and remove them
605 | destroyPanels();
606 |
607 | toggleUIConnected(false);
608 | logMsg('Device ' + disconnectedDevice.name + ' is disconnected.');
609 |
610 | device = undefined;
611 | currentBoard = undefined;
612 | }
613 |
614 | /**
615 | * @name clickAutoscroll
616 | * Change handler for the Autoscroll checkbox.
617 | */
618 | async function clickAutoscroll() {
619 | saveSetting('autoscroll', autoscroll.checked);
620 | }
621 |
622 | /**
623 | * @name clickTimestamp
624 | * Change handler for the Show Timestamp checkbox.
625 | */
626 | async function clickTimestamp() {
627 | saveSetting('timestamp', showTimestamp.checked);
628 | }
629 |
630 | /**
631 | * @name clickDarkMode
632 | * Change handler for the Dark Mode checkbox.
633 | */
634 | async function clickDarkMode() {
635 | updateTheme();
636 | saveSetting('darkmode', darkMode.checked);
637 | }
638 |
639 |
640 |
641 | /**
642 | * @name clickKnownOnly
643 | * Change handler for the Show Only Known Devices checkbox.
644 | */
645 | async function clickKnownOnly() {
646 | saveSetting('knownonly', knownOnly.checked);
647 | }
648 |
649 | /**
650 | * @name clickClear
651 | * Click handler for the clear button.
652 | */
653 | async function clickClear() {
654 | reset();
655 | }
656 |
657 | function convertJSON(chunk) {
658 | try {
659 | let jsonObj = JSON.parse(chunk);
660 | return jsonObj;
661 | } catch (e) {
662 | return chunk;
663 | }
664 | }
665 |
666 | function toggleUIConnected(connected) {
667 | let lbl = 'Connect';
668 | if (connected) {
669 | lbl = 'Disconnect';
670 | }
671 | butConnect.textContent = lbl;
672 | }
673 |
674 | function loadAllSettings() {
675 | // Load all saved settings or defaults
676 | autoscroll.checked = loadSetting('autoscroll', true);
677 | showTimestamp.checked = loadSetting('timestamp', false);
678 | darkMode.checked = loadSetting('darkmode', false);
679 | knownOnly.checked = loadSetting('knownonly', true);
680 | }
681 |
682 | function loadSetting(setting, defaultValue) {
683 | let value = JSON.parse(window.localStorage.getItem(setting));
684 | if (value == null) {
685 | return defaultValue;
686 | }
687 |
688 | return value;
689 | }
690 |
691 | function saveSetting(setting, value) {
692 | window.localStorage.setItem(setting, JSON.stringify(value));
693 | }
694 |
695 | async function finishDrawing() {
696 | return new Promise(requestAnimationFrame);
697 | }
698 |
699 | async function sleep(ms) {
700 | return new Promise(resolve => setTimeout(resolve, ms));
701 | }
702 |
703 | async function updateAllPanels() {
704 | for (let panelId of activePanels) {
705 | updatePanel(panelId);
706 | }
707 |
708 | // wait for frame to finish and request another frame
709 | await finishDrawing();
710 | await updateAllPanels();
711 | }
712 |
713 | function updatePanel(panelId) {
714 | if (!panels[panelId].rendered) {
715 | if (panels[panelId].panelType == "text") {
716 | updateTextPanel(panelId);
717 | } else if (panels[panelId].panelType == "graph") {
718 | updateGraphPanel(panelId);
719 | } else if (panels[panelId].panelType == "model3d") {
720 | update3dPanel(panelId);
721 | } else if (panels[panelId].panelType == "custom") {
722 | updateCustomPanel(panelId);
723 | }
724 | panels[panelId].rendered = true;
725 | }
726 | }
727 |
728 | function createPanel(panelId) {
729 | if (panels.hasOwnProperty(panelId)) {
730 | if (panels[panelId].panelType == "text") {
731 | createTextPanel(panelId);
732 | } else if (panels[panelId].panelType == "graph") {
733 | createGraphPanel(panelId);
734 | } else if (panels[panelId].panelType == "color") {
735 | createColorPanel(panelId);
736 | } else if (panels[panelId].panelType == "model3d") {
737 | create3dPanel(panelId);
738 | } else if (panels[panelId].panelType == "custom") {
739 | createCustomPanel(panelId);
740 | }
741 | panels[panelId].rendered = true;
742 | activePanels.push(panelId);
743 | }
744 | }
745 |
746 | function destroyPanels() {
747 | let activePanelCount = activePanels.length;
748 | for (let i = 0; i < activePanelCount; i++) {
749 | let itemToRemove = activePanels.pop();
750 | document.querySelector("#dashboard > #" + itemToRemove).remove();
751 | }
752 | }
753 |
754 | function clearGraphData() {
755 | for (let panelId of activePanels) {
756 | let panel = panels[panelId];
757 | if (panel.panelType == "graph") {
758 | panel.graph.clear();
759 | }
760 | }
761 | }
762 |
763 | function ucWords(text) {
764 | return text.replace('_', ' ').toLowerCase().replace(/(?<= )[^\s]|^./g, a=>a.toUpperCase())
765 | }
766 |
767 | function loadPanelTemplate(panelId, templateId) {
768 | if (templateId == undefined) {
769 | templateId = panels[panelId].panelType;
770 | }
771 | // Create Panel from Template
772 | let panelTemplate = document.querySelector("#templates > ." + templateId).cloneNode(true);
773 | panelTemplate.id = panelId;
774 | if (panels[panelId].title !== undefined) {
775 | panelTemplate.querySelector(".title").innerHTML = panels[panelId].title;
776 | } else {
777 | panelTemplate.querySelector(".title").innerHTML = ucWords(panelId);
778 | }
779 |
780 | dashboard.appendChild(panelTemplate)
781 |
782 | return panelTemplate;
783 | }
784 |
785 | /* Text Panel */
786 | function createTextPanel(panelId) {
787 | // Create Panel from Template
788 | let panelTemplate = loadPanelTemplate(panelId);
789 | panelTemplate.querySelector(".content p").innerHTML = "-";
790 | if (panels[panelId].style !== undefined) {
791 | panelTemplate.querySelector(".content").style = panels[panelId].style;
792 | }
793 | }
794 |
795 | function updateTextPanel(panelId) {
796 | let panelElement = document.querySelector("#dashboard > #" + panelId);
797 | let panelContent = [];
798 | Object.entries(panels[panelId].data).forEach(([field, item], index) => {
799 | let value = "";
800 | if (panels[panelId].data[field].length > 0) {
801 | value = panels[panelId].data[field].pop(); // Show only the last piece of data
802 | panels[panelId].data[field] = [];
803 | if (panels[panelId].textFormat !== undefined) {
804 | value = panels[panelId].textFormat(value);
805 | }
806 | }
807 | if (value !== "") {
808 | panelContent.push(value);
809 | }
810 | });
811 | if (panelContent.length == 0) {
812 | panelContent = "-";
813 | } else {
814 | panelContent = panelContent.join(" ");
815 | }
816 | panelElement.querySelector(".content p").innerHTML = panelContent;
817 | }
818 |
819 | /* Graph Panel */
820 | function createGraphPanel(panelId) {
821 | // Create Panel from Template
822 | let panelTemplate = loadPanelTemplate(panelId);
823 | let canvas = panelTemplate.querySelector(".content canvas");
824 |
825 | // Create a canvas
826 | panels[panelId].graph = new Graph(canvas);
827 | panels[panelId].graph.create(false);
828 |
829 | // Setup graph
830 | Object.entries(panels[panelId].data).forEach(([field, item], index) => {
831 | panels[panelId].graph.addDataSet(field, colors[(colorIndex + index) % colors.length]);
832 | // Create text spans for each dataset and set the color here
833 | let textField = document.createElement('div');
834 | textField.style.color = colors[(colorIndex + index) % colors.length];
835 | textField.id = field;
836 | panelTemplate.querySelector(".content .text p").appendChild(textField);
837 | });
838 | colorIndex += Object.entries(panels[panelId].data).length;
839 |
840 | panels[panelId].graph.update();
841 | }
842 |
843 | function updateGraphPanel(panelId) {
844 | let panelElement = document.querySelector("#dashboard > #" + panelId);
845 | let panelContent = [];
846 | let multipleEntries = Object.entries(panels[panelId].data).length > 1;
847 |
848 | // Set Graph Data to match
849 | Object.entries(panels[panelId].data).forEach(([field, item], index) => {
850 | if (panels[panelId].data[field].length > 0) {
851 | let value = null;
852 | while(panels[panelId].data[field].length > 0) {
853 | value = panels[panelId].data[field].shift();
854 | panels[panelId].graph.addValue(index, value, false);
855 | }
856 | if (panels[panelId].textFormat !== undefined) {
857 | value = panels[panelId].textFormat(value);
858 | }
859 | if (value !== null) {
860 | if (multipleEntries) {
861 | value = ucWords(field) + ": " + value;
862 | }
863 | panelElement.querySelector(".content .text p #" + field).innerHTML = value;
864 | }
865 | } else {
866 | panels[panelId].graph.clearValues(index);
867 | if (multipleEntries) {
868 | panelElement.querySelector(".content .text p #" + field).innerHTML = ucWords(field) + ': -';
869 | } else {
870 | panelElement.querySelector(".content .text p #" + field).innerHTML = '-';
871 | }
872 | }
873 |
874 | });
875 |
876 | panels[panelId].graph.flushBuffer();
877 | }
878 |
879 | /* Color Panel */
880 | function createColorPanel(panelId) {
881 | // Create Panel from Template
882 | let panelTemplate = loadPanelTemplate(panelId);
883 |
884 | let container = panelTemplate.querySelector('.content div');
885 | panels[panelId].colorPicker = colorjoe.rgb(container, 'red');
886 |
887 | // Update the panel packet sequence to match the number of LEDs on board
888 | panels[panelId].packetSequence = panels[panelId].structure.slice(0, 2);
889 | let dataType = panels[panelId].structure[2].replace(/\[\]/, '');
890 | for (let i = 0; i < currentBoard.neopixels * 3; i++) {
891 | panels[panelId].packetSequence.push(dataType);
892 | }
893 |
894 | // RGB Color Picker
895 | function updateModelLed(color) {
896 | logMsg("Changing neopixel to " + color.hex());
897 | let orderedColors = adjustColorOrder(Math.round(color.r() * 255),
898 | Math.round(color.g() * 255),
899 | Math.round(color.b() * 255));
900 | let values = [0, 1].concat(new Array(currentBoard.neopixels).fill(orderedColors).flat());
901 | let packet = encodePacket(panelId, values);
902 | panels[panelId].characteristic.writeValue(packet)
903 | .catch(error => {console.log(error);})
904 | .then(_ => {});
905 | }
906 |
907 | function adjustColorOrder(red, green, blue) {
908 | // Add more as needed
909 | switch(currentBoard.colorOrder) {
910 | case 'GRB':
911 | return [green, red, blue];
912 | default:
913 | return [red, green, blue];
914 | }
915 | }
916 |
917 | panels[panelId].colorPicker.on('done', updateModelLed);
918 | }
919 |
920 | /* 3D Panel */
921 | function create3dPanel(panelId) {
922 | let panelTemplate = loadPanelTemplate(panelId);
923 | let canvas = panelTemplate.querySelector(".content canvas");
924 |
925 | // Make it visually fill the positioned parent
926 | canvas.style.width ='100%';
927 | canvas.style.height='100%';
928 | // ...then set the internal size to match
929 | canvas.width = canvas.offsetWidth;
930 | canvas.height = canvas.offsetHeight;
931 |
932 | // Create a 3D renderer and camera
933 | panels[panelId].renderer = new THREE.WebGLRenderer({canvas});
934 |
935 | panels[panelId].camera = new THREE.PerspectiveCamera(45, canvas.width/canvas.height, 0.1, 100);
936 | panels[panelId].camera.position.set(0, -5, 30);
937 |
938 | // Set up the Scene
939 | panels[panelId].scene = new THREE.Scene();
940 | panels[panelId].scene.background = new THREE.Color('black');
941 | {
942 | const skyColor = 0xB1E1FF; // light blue
943 | const groundColor = 0x999999; // gray
944 | const intensity = 1;
945 | const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
946 | panels[panelId].scene.add(light);
947 | }
948 |
949 | {
950 | const color = 0xFFFFFF;
951 | const intensity = 3;
952 | const light = new THREE.DirectionalLight(color, intensity);
953 | light.position.set(0, 10, 0);
954 | light.target.position.set(-5, 0, 0);
955 | panels[panelId].scene.add(light);
956 | panels[panelId].scene.add(light.target);
957 | }
958 |
959 | {
960 | const color = 0xFFFFFF;
961 | const intensity = 1;
962 | const light = new THREE.DirectionalLight(color, intensity);
963 | light.position.set(0, -10, 0);
964 | light.target.position.set(5, 0, 0);
965 | panels[panelId].scene.add(light);
966 | panels[panelId].scene.add(light.target);
967 | }
968 |
969 | function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) {
970 | const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
971 | const halfFovY = THREE.MathUtils.degToRad(camera.fov * 0.5);
972 | const distance = halfSizeToFitOnScreen / Math.tan(halfFovY);
973 | // compute a unit vector that points in the direction the camera is now
974 | // in the xz plane from the center of the box
975 | const direction = (new THREE.Vector3())
976 | .subVectors(camera.position, boxCenter)
977 | .multiply(new THREE.Vector3(1, 0, 1))
978 | .normalize();
979 |
980 | // move the camera to a position distance units way from the center
981 | // in whatever direction the camera was from the center already
982 | camera.position.copy(direction.multiplyScalar(distance).add(boxCenter));
983 |
984 | // pick some near and far values for the frustum that
985 | // will contain the box.
986 | camera.near = boxSize / 100;
987 | camera.far = boxSize * 100;
988 |
989 | camera.updateProjectionMatrix();
990 |
991 | // point the camera to look at the center of the box
992 | camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z);
993 | }
994 |
995 | {
996 | const gltfLoader = new GLTFLoader();
997 | gltfLoader.load('https://cdn.glitch.com/eeed3166-9759-4ba5-ba6b-aed272d6db80%2Fbunny.glb', (gltf) => {
998 | const root = gltf.scene;
999 | panels[panelId].model = root;
1000 | panels[panelId].scene.add(root);
1001 |
1002 | const box = new THREE.Box3().setFromObject(root);
1003 |
1004 | const boxSize = box.getSize(new THREE.Vector3()).length();
1005 | const boxCenter = box.getCenter(new THREE.Vector3());
1006 |
1007 | frameArea(boxSize * 1.25, boxSize, boxCenter, panels[panelId].camera);
1008 | });
1009 | }
1010 | }
1011 |
1012 | function update3dPanel(panelId) {
1013 | let panelElement = document.querySelector("#dashboard > #" + panelId);
1014 |
1015 | function resizeRendererToDisplaySize(renderer) {
1016 | const canvas = renderer.domElement;
1017 | const width = canvas.clientWidth;
1018 | const height = canvas.clientHeight;
1019 | const needResize = canvas.width !== width || canvas.height !== height;
1020 | if (needResize) {
1021 | renderer.setSize(width, height, false);
1022 | }
1023 | return needResize;
1024 | }
1025 | // Set Graph Data to match
1026 | if (resizeRendererToDisplaySize(panels[panelId].renderer)) {
1027 | const canvas = panels[panelId].renderer.domElement;
1028 | panels[panelId].camera.aspect = canvas.clientWidth / canvas.clientHeight;
1029 | panels[panelId].camera.updateProjectionMatrix();
1030 | }
1031 |
1032 | let quaternion = {w: 1, x: 0, y: 0, z:0};
1033 | Object.entries(panels[panelId].data).forEach(([field, item], index) => {
1034 | if (panels[panelId].data[field].length > 0) {
1035 | let value = panels[panelId].data[field].pop(); // Show only the last piece of data
1036 | quaternion[field] = value;
1037 | panels[panelId].data[field] = [];
1038 | }
1039 | });
1040 |
1041 | if (panels[panelId].model != undefined) {
1042 | let rotObjectMatrix = new THREE.Matrix4();
1043 | let rotationQuaternion = new THREE.Quaternion(quaternion.y, quaternion.z, quaternion.x, quaternion.w);
1044 | rotObjectMatrix.makeRotationFromQuaternion(rotationQuaternion);
1045 | panels[panelId].model.quaternion.setFromRotationMatrix(rotObjectMatrix);
1046 | }
1047 |
1048 | panels[panelId].renderer.render(panels[panelId].scene, panels[panelId].camera);
1049 | }
1050 |
1051 | function createCustomPanel(panelId) {
1052 | if (panels[panelId].condition === undefined || panels[panelId].condition()) {
1053 | if (panels[panelId].create != undefined) {
1054 | panels[panelId].create(panelId);
1055 | }
1056 | }
1057 | }
1058 |
1059 | function updateCustomPanel(panelId) {
1060 | if (panels[panelId].condition === undefined || panels[panelId].condition()) {
1061 | if (panels[panelId].update != undefined) {
1062 | panels[panelId].update(panelId);
1063 | }
1064 | }
1065 | }
1066 |
1067 | function createMockPanels() {
1068 | currentBoard = boards.CLUE;
1069 | for (let panelId of Object.keys(panels)) {
1070 | if (panels[panelId].condition == undefined || panels[panelId].condition()) {
1071 | // Non-custom ones such as battery are always active
1072 | createPanel(panelId);
1073 | }
1074 | }
1075 | }
1076 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------