├── .env ├── .gitignore ├── README.md ├── assets ├── adafruit-logo.svg └── bunny.glb ├── css ├── colorjoe.css ├── dark.css ├── light.css └── style.css ├── index.html ├── js ├── colorjoe.min.js ├── graph.js ├── scale.fix.js └── script.js └── license.md /.env: -------------------------------------------------------------------------------- 1 | # Scrubbed by Glitch 2020-01-28T20:10:15+0000 2 | # Scrubbed by Glitch 2020-02-18T23:29:56+0000 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adafruit WebBluetooth Dashboard 2 | A Web Bluetooth Dashboard for easily testing sensors. Source files for the Adafruit WebBluetooth Dashboard available at: https://adafruit.github.io/Adafruit_WebBluetooth_Dashboard/. 3 | 4 | ## Adafruit Learn Guide 5 | To learn how to use the Web Bluetooth Dashboard, check out the learn guide at https://learn.adafruit.com/using-connection-based-web-bluetooth-in-chrome. 6 | -------------------------------------------------------------------------------- /assets/adafruit-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /assets/bunny.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_WebBluetooth_Dashboard/d0cf85f20e98c27a92f1759b1d07dd849c884663/assets/bunny.glb -------------------------------------------------------------------------------- /css/colorjoe.css: -------------------------------------------------------------------------------- 1 | #rgbValue, #hslaValue { 2 | float: right; 3 | } 4 | 5 | .container { 6 | overflow: auto; 7 | } 8 | 9 | #showPicker { 10 | float: left; 11 | } 12 | 13 | .colorPicker { 14 | display: inline-block; 15 | width: 100%; 16 | } 17 | 18 | .colorPicker .extras { 19 | float: right; 20 | margin: 0.5em; 21 | } 22 | 23 | .colorPicker .extras .currentColorContainer { 24 | overflow: hidden; 25 | } 26 | 27 | .colorPicker .extras .currentColor { 28 | float: right; 29 | width: 65px; 30 | height: 30px; 31 | border: 1px solid #bbb; 32 | -moz-border-radius: .3em; 33 | border-radius: .3em; 34 | } 35 | 36 | .colorPicker .extras .colorFields { 37 | margin-top: 0.5em; 38 | margin-bottom: 0.5em; 39 | } 40 | 41 | .colorPicker .extras .color { 42 | text-align: right; 43 | } 44 | 45 | .colorPicker .extras .colorFields input { 46 | width: 40px; 47 | margin-left: 0.5em; 48 | } 49 | 50 | .colorPicker .extras .hex { 51 | float: right; 52 | } 53 | 54 | .colorPicker .extras .hex input { 55 | width: 60px; 56 | } 57 | 58 | .colorPicker .twod { 59 | float: left; 60 | margin: 10px 10px 10px 20px; 61 | } 62 | 63 | /* main dimensions */ 64 | .colorPicker .twod, .colorPicker .twod .bg { 65 | width: 170px; 66 | height: 170px; 67 | } 68 | .colorPicker .oned, .colorPicker .oned .bg { 69 | height: 170px; 70 | } 71 | .colorPicker .oned, .colorPicker .oned .bg, .colorPicker .oned .pointer .shape { 72 | width: 20px; 73 | } 74 | 75 | .colorPicker .twod .bg { 76 | position: absolute; 77 | border: 1px solid #bbb; 78 | } 79 | .colorPicker .twod .pointer { 80 | position: relative; 81 | z-index: 2; 82 | width: 8px; 83 | } 84 | .colorPicker .twod .pointer .shape { 85 | position: absolute; 86 | } 87 | .colorPicker .twod .pointer .shape1 { 88 | -webkit-transform: translate(-50%, -50%); 89 | -ms-transform: translate(-50%, -50%); 90 | transform: translate(-50%, -50%); 91 | width: 7px; 92 | height: 6px; 93 | border: 1px solid black; 94 | -moz-border-radius: 5px; 95 | border-radius: 5px; 96 | } 97 | .colorPicker .twod .pointer .shape2 { 98 | -webkit-transform: translate(-50%, -50%); 99 | -ms-transform: translate(-50%, -50%); 100 | transform: translate(-50%, -50%); 101 | width: 5px; 102 | height: 5px; 103 | border: 1px solid white; 104 | -moz-border-radius: 4px; 105 | border-radius: 4px; 106 | } 107 | 108 | .colorPicker .oned { 109 | float: left; 110 | margin: 1vh 0; 111 | } 112 | 113 | .colorPicker .oned .bg { 114 | border: 1px solid #bbb; 115 | } 116 | .colorPicker .oned .pointer { 117 | position: relative; 118 | z-index: 2; 119 | } 120 | .colorPicker .oned .pointer .shape { 121 | position: absolute; 122 | margin-left: -4px; 123 | margin-top: -2px; 124 | height: 0; 125 | border-width: 3px 4px; 126 | border-style: solid; 127 | border-top-color: transparent; 128 | border-bottom-color: transparent; 129 | width: 23px; 130 | } 131 | /* gradients, tweak as needed based on which browsers you want to support */ 132 | .colorPicker .oned .bg { 133 | background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 66%, #ff00ff 83%, #ff0000 100%); 134 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ff0000), color-stop(17%,#ffff00), color-stop(33%,#00ff00), color-stop(50%,#00ffff), color-stop(66%,#0000ff), color-stop(83%,#ff00ff), color-stop(100%,#ff0000)); 135 | background: -webkit-linear-gradient(top, #ff0000 0%,#ffff00 17%,#00ff00 33%,#00ffff 50%,#0000ff 66%,#ff00ff 83%,#ff0000 100%); 136 | background: -o-linear-gradient(top, #ff0000 0%,#ffff00 17%,#00ff00 33%,#00ffff 50%,#0000ff 66%,#ff00ff 83%,#ff0000 100%); 137 | background: linear-gradient(to bottom, #ff0000 0%,#ffff00 17%,#00ff00 33%,#00ffff 50%,#0000ff 66%,#ff00ff 83%,#ff0000 100%); 138 | } 139 | 140 | .colorPicker .twod .bg1 { 141 | z-index: 0; 142 | background: -moz-linear-gradient(left, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%); 143 | background: -webkit-gradient(linear, left top, right top, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(255,255,255,0))); 144 | background: -webkit-linear-gradient(left, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%); 145 | background: -o-linear-gradient(left, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%); 146 | background: linear-gradient(to right, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%); 147 | } 148 | .colorPicker .twod .bg2 { 149 | z-index: 1; 150 | background: -moz-linear-gradient(top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 100%); 151 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,1))); 152 | background: -webkit-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%); 153 | background: -o-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%); 154 | background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,1) 100%); 155 | } 156 | 157 | #hslPicker .twod .bg1 { 158 | background: -moz-linear-gradient(left, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 66%, #ff00ff 83%, #ff0000 100%); 159 | background: -webkit-gradient(linear, left top, right top, color-stop(0%,#ff0000), color-stop(17%,#ffff00), color-stop(33%,#00ff00), color-stop(50%,#00ffff), color-stop(66%,#0000ff), color-stop(83%,#ff00ff), color-stop(100%,#ff0000)); 160 | background: -webkit-linear-gradient(left, #ff0000 0%,#ffff00 17%,#00ff00 33%,#00ffff 50%,#0000ff 66%,#ff00ff 83%,#ff0000 100%); 161 | background: -o-linear-gradient(left, #ff0000 0%,#ffff00 17%,#00ff00 33%,#00ffff 50%,#0000ff 66%,#ff00ff 83%,#ff0000 100%); 162 | background: linear-gradient(to right, #ff0000 0%,#ffff00 17%,#00ff00 33%,#00ffff 50%,#0000ff 66%,#ff00ff 83%,#ff0000 100%); 163 | } 164 | 165 | #hslPicker .twod .bg2 { 166 | background: -moz-linear-gradient(top, rgba(0,0,0,0) 0%, rgba(127,127,127,1) 100%); 167 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,0,0,0)), color-stop(100%,rgba(127,127,127,1))); 168 | background: -webkit-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(127,127,127,1) 100%); 169 | background: -o-linear-gradient(top, rgba(0,0,0,0) 0%,rgba(127,127,127,1) 100%); 170 | background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(127,127,127,1) 100%); 171 | } 172 | 173 | #hslPicker .oned .bg { 174 | z-index: 1; 175 | background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(0,0,0,0) 50%, rgba(0,0,0,1) 100%); 176 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,1)), color-stop(50%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,1))); 177 | background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(0,0,0,0),rgba(0,0,0,1) 100%); 178 | background: -o-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(0,0,0,0) 50%,rgba(0,0,0,1) 100%); 179 | background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(0,0,0,0) 50%,rgba(0,0,0,1) 100%); 180 | } 181 | 182 | #hslPicker .extras { 183 | width: 100px; 184 | } 185 | 186 | #hslPicker .oned.alpha { 187 | margin: 0; 188 | } 189 | 190 | #hslPicker .oned.alpha .bg { 191 | background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(0,0,0,1) 100%); /* FF3.6+ */ 192 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,1)), color-stop(100%,rgba(0,0,0,1))); /* Chrome,Safari4+ */ 193 | background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(0,0,0,1) 100%); /* Chrome10+,Safari5.1+ */ 194 | background: -o-linear-gradient(top, rgba(255,255,255,1) 0%,rgba(0,0,0,1) 100%); /* Opera 11.10+ */ 195 | background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(0,0,0,1) 100%); /* W3C */ 196 | } 197 | -------------------------------------------------------------------------------- /css/dark.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: #000; 3 | color: #fff; 4 | } 5 | 6 | body { 7 | background-color: #333; 8 | color: #fff; 9 | } 10 | 11 | input, select, button { 12 | background-color: #333; 13 | color: #fff; 14 | } 15 | 16 | #dashboard { 17 | background-color: #333; 18 | } 19 | 20 | #dashboard > div { 21 | border-color: #535353; 22 | } 23 | 24 | #dashboard .title { 25 | background-color: #333; 26 | } 27 | 28 | .footer button { 29 | border-color: #fff; 30 | background-color: #333; 31 | color: #fff; 32 | } 33 | 34 | .footer button:hover { 35 | background-color: #fff; 36 | color: #333; 37 | } 38 | 39 | .remix button { 40 | border-color: #fff; 41 | color: #fff; 42 | } 43 | 44 | .remix button:hover { 45 | background-color: #fff; 46 | } 47 | 48 | #notSupported { 49 | background-color: red; 50 | color: white; 51 | } 52 | 53 | .timestamp { 54 | color: #8ec641; 55 | } 56 | 57 | #log { 58 | background-color: #000; 59 | color: #cecece; 60 | } 61 | 62 | #dashboard .battery-level .battery { 63 | border-color: #fff; 64 | } 65 | 66 | #dashboard .battery-level .battery:after { 67 | background: #fff; 68 | } 69 | 70 | #dashboard .play-button .button span:hover { 71 | border-left-color: #333; 72 | } 73 | 74 | .colorPicker .oned .pointer .shape { 75 | border-left-color: #fff; 76 | border-right-color: #fff; 77 | } 78 | -------------------------------------------------------------------------------- /css/light.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: #000; 3 | color: #fff; 4 | } 5 | 6 | body { 7 | background-color: #fff; 8 | color: #000; 9 | } 10 | 11 | input, select, button { 12 | background-color: #fff; 13 | color: #000; 14 | } 15 | 16 | #dashboard { 17 | background-color: #fff; 18 | } 19 | 20 | #dashboard > div { 21 | border-color: #cecece; 22 | } 23 | 24 | #dashboard .title { 25 | background-color: #fff; 26 | } 27 | 28 | #notSupported { 29 | background-color: red; 30 | color: white; 31 | } 32 | 33 | .footer button { 34 | border-color: #63338f; 35 | background-color: #fff; 36 | color: #63338f; 37 | } 38 | 39 | .footer button:hover { 40 | background-color: #63338f; 41 | color: #fff; 42 | } 43 | 44 | .remix button { 45 | border-color: #333; 46 | color: #333; 47 | } 48 | 49 | .remix button:hover { 50 | background-color: #333; 51 | } 52 | 53 | .timestamp { 54 | color: #8ec641; 55 | } 56 | 57 | #log { 58 | background-color: #000; 59 | color: #cecece; 60 | } 61 | 62 | #dashboard .battery-level .battery { 63 | border-color: #333; 64 | } 65 | 66 | #dashboard .battery-level .battery:after { 67 | background: #333; 68 | } 69 | 70 | #dashboard .play-button .button span:hover { 71 | border-left-color: #63338f; 72 | } 73 | .colorPicker .oned .pointer .shape { 74 | border-left-color: #666; 75 | border-right-color: #666; 76 | } 77 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Header 3 | */ 4 | 5 | .header { 6 | box-sizing: border-box; 7 | font-size: 16px; 8 | height: 85px; 9 | line-height: 40px; 10 | padding: 20px 70px 0px 70px; /* TRouBLe */ 11 | position: fixed; 12 | width: 100%; 13 | z-index: 1000; 14 | margin: 0; 15 | border-bottom: 5px solid #00a7e9; 16 | } 17 | 18 | .header h1 { 19 | flex: 1; 20 | font-size: 20px; 21 | font-weight: 400; 22 | } 23 | 24 | .header button { 25 | border: solid 2px #fff; 26 | background-color: #000; 27 | color: #fff; 28 | margin-left: 20px; 29 | height: 30px; 30 | } 31 | 32 | .header button:hover { 33 | background-color: #fff; 34 | color: #000; 35 | } 36 | 37 | button { 38 | height: 25px; 39 | font-size: 16px; 40 | border-radius: 15px; 41 | padding-left: 20px; 42 | padding-right: 20px; 43 | border-width: 2px; 44 | } 45 | 46 | body { 47 | font-family: proxima-nova, sans-serif; 48 | font-style: normal; 49 | font-weight: 400; 50 | margin: 0; 51 | } 52 | 53 | p { 54 | margin: 0; 55 | } 56 | 57 | input, select, button, label { 58 | font-weight: 600; 59 | outline: none; 60 | } 61 | 62 | div.left { 63 | float: left; 64 | display: flex; 65 | align-items: center; 66 | } 67 | 68 | div.right { 69 | float: right; 70 | display: flex; 71 | align-items: center; 72 | } 73 | 74 | div.clear { 75 | clear: both; 76 | } 77 | 78 | .Adafruit-Logo { 79 | width: 115px; 80 | height: 40px; 81 | object-fit: contain; 82 | } 83 | 84 | .main { 85 | overflow-x: hidden; 86 | overflow-y: auto; 87 | padding-top: 80px; 88 | } 89 | 90 | .hidden { 91 | display: none; 92 | } 93 | 94 | .notSupported { 95 | padding: 1em; 96 | margin-top: 1em; 97 | margin-bottom: 1em; 98 | } 99 | 100 | .subheader { 101 | height: 100px; 102 | line-height: 100px; 103 | padding-left: 70px; 104 | padding-right: 70px; 105 | } 106 | 107 | .subheader .title { 108 | font-size: 36px; 109 | font-weight: 500; 110 | } 111 | 112 | #dashboard { 113 | position: relative; 114 | height: calc(60vh - 190px); 115 | margin: 0 auto; 116 | overflow-y: auto; 117 | padding: 0 60px; 118 | } 119 | 120 | #dashboard > div { 121 | width: 244px; 122 | height: 244px; 123 | margin: 0 10px 20px; 124 | border-width: 1px; 125 | border-style: solid; 126 | border-radius: 4px; 127 | float: left; 128 | } 129 | 130 | #dashboard .title { 131 | text-align: left; 132 | font-size: 18px; 133 | font-weight: 600; 134 | line-height: 44px; 135 | padding-left: 10px; 136 | text-overflow: ellipsis; 137 | border-top-left-radius: 6px; 138 | border-top-right-radius: 6px; 139 | white-space: nowrap; 140 | overflow: hidden; 141 | } 142 | 143 | #dashboard .content { 144 | height: 200px; 145 | width: 244px; 146 | font-size: 24px; 147 | font-weight: 600; 148 | } 149 | 150 | /* Text Panel */ 151 | #dashboard .text .content { 152 | text-overflow: ellipsis; 153 | white-space: nowrap; 154 | overflow: hidden; 155 | display: flex; 156 | justify-content: center; 157 | align-items: center; 158 | } 159 | 160 | #dashboard .text .content p { 161 | padding: 10px; 162 | } 163 | 164 | #dashboard .graph .content, 165 | #dashboard .color .content, 166 | #dashboard .model3d .content { 167 | position: relative; 168 | } 169 | 170 | /* Graph Panel */ 171 | #dashboard .graph .content .text { 172 | height: 95px; 173 | } 174 | 175 | #dashboard .graph .content .chart { 176 | height: 105px; 177 | } 178 | 179 | #dashboard .graph .text { 180 | text-overflow: ellipsis; 181 | white-space: nowrap; 182 | overflow: hidden; 183 | display: flex; 184 | justify-content: center; 185 | align-items: flex-end; 186 | } 187 | 188 | #dashboard .graph .text p { 189 | padding: 10px; 190 | } 191 | 192 | #log { 193 | height: calc(40vh - 190px); 194 | width: 100%; 195 | font-family: pt-mono, monospace; 196 | font-style: normal; 197 | font-weight: 400; 198 | font-size: 16px; 199 | overflow-x: hidden; 200 | overflow-x: auto; 201 | transition : color 0.1s linear; 202 | padding: 0 50px; 203 | border: 20px solid #000; 204 | } 205 | 206 | .footer { 207 | height: 45px; 208 | line-height: 45px; 209 | padding-left: 70px; 210 | padding-right: 70px; 211 | } 212 | 213 | .footer button { 214 | font-size: 14px; 215 | margin: 0 10px; 216 | } 217 | 218 | .remix { 219 | display: flex; 220 | justify-content: center; 221 | height: 110px; 222 | position: relative; 223 | } 224 | 225 | .remix button { 226 | position: absolute; 227 | bottom: 11px; 228 | } 229 | 230 | #templates { 231 | display: none; 232 | } 233 | 234 | /* On/Off Switch Widget */ 235 | .onoffswitch { 236 | display: inline-block; 237 | position: relative; 238 | width: 50px; 239 | -webkit-user-select: none; 240 | -moz-user-select: none; 241 | -ms-user-select: none; 242 | margin-left: 10px; 243 | } 244 | 245 | .onoffswitch-checkbox { 246 | display: none; 247 | } 248 | 249 | .onoffswitch-label { 250 | display: block; 251 | overflow: hidden; 252 | cursor: pointer; 253 | border: 1px solid #900; 254 | border-radius: 15px; 255 | transition: border 0.3s ease-in 0s; 256 | } 257 | 258 | .onoffswitch-inner { 259 | display: block; 260 | width: 200%; 261 | margin-left: -100%; 262 | transition: margin 0.3s ease-in 0s; 263 | } 264 | 265 | .onoffswitch-inner:before, 266 | .onoffswitch-inner:after { 267 | display: block; 268 | float: left; 269 | width: 50%; 270 | height: 25px; 271 | padding: 0; 272 | line-height: 25px; 273 | font-size: 14px; 274 | color: white; 275 | font-family: proxima-nova, sans-serif; 276 | font-style: normal; 277 | font-weight: 600; 278 | box-sizing: border-box; 279 | } 280 | 281 | .onoffswitch-inner:before { 282 | content: "on"; 283 | padding-left: 6px; 284 | background-color: #8ec641; 285 | color: #fff; 286 | } 287 | 288 | .onoffswitch-inner:after { 289 | content: "off"; 290 | padding-right: 6px; 291 | background-color: #c64141; 292 | color: #fff; 293 | text-align: right; 294 | } 295 | 296 | .onoffswitch-switch { 297 | display: block; 298 | width: 19px; 299 | margin: 3px; 300 | background: #fff; 301 | position: absolute; 302 | top: 0; 303 | bottom: 0; 304 | right: 23px; 305 | border: 1px solid #900; 306 | border-radius: 15px; 307 | transition: all 0.3s ease-in 0s; 308 | } 309 | 310 | .onoffswitch-checkbox:checked + .onoffswitch-label { 311 | border-color: #71ae1e; 312 | } 313 | 314 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { 315 | margin-left: 0; 316 | } 317 | 318 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { 319 | right: 0px; 320 | border-color: #67ac38; 321 | } 322 | 323 | .footer .onoffswitch { 324 | margin-right: 20px; 325 | } 326 | 327 | #fpsCounter { 328 | margin-right: 10px; 329 | } 330 | 331 | /* Tone Panel */ 332 | #dashboard .play-button .content { 333 | display: flex; 334 | flex-direction: column; 335 | justify-content: center; 336 | align-items: center; 337 | } 338 | 339 | #dashboard .play-button .button { 340 | display: flex; 341 | justify-content: center; 342 | align-items: center; 343 | background-color: #f89521; 344 | width: 75px; 345 | height: 75px; 346 | border-radius: 75px; 347 | } 348 | 349 | #dashboard .play-button .button span { 350 | width: 0; 351 | height: 0; 352 | border-top: 19px solid transparent; 353 | border-bottom: 19px solid transparent; 354 | border-left: 29px solid #fff; 355 | margin-left: 6px; 356 | display: block; 357 | } 358 | 359 | /* Battery Level Panel */ 360 | #dashboard .battery-level .content { 361 | display: flex; 362 | flex-direction: column; 363 | justify-content: center; 364 | align-items: center; 365 | } 366 | 367 | #dashboard .battery-level .battery { 368 | border-width: 5px; 369 | border-style: solid; 370 | width: 134px; 371 | height: 61px; 372 | padding: 3px; 373 | border-radius: 10px; 374 | position: relative; 375 | } 376 | 377 | #dashboard .battery-level .battery:after { 378 | content:""; 379 | width: 12px; 380 | height: 20px; 381 | background: #333; 382 | display: block; 383 | position: absolute; 384 | right: -12px; 385 | top: 22px; 386 | border-radius: 0 2px 2px 0; 387 | } 388 | 389 | #dashboard .battery-level .battery:hover {cursor:pointer;} 390 | 391 | #dashboard .battery-level .level { 392 | display: block; 393 | background: #8ec641; 394 | height: 100%; 395 | } 396 | 397 | #dashboard .battery-level .battery-alert .level { 398 | background: #c64141; 399 | } 400 | 401 | #dashboard .battery-level .battery-middle .level { 402 | background: #f89521; 403 | } 404 | 405 | #dashboard .battery-level .percentage { 406 | margin-top: 10px; 407 | } 408 | 409 | /* Switch Panel */ 410 | #dashboard .onboard-switch .content { 411 | display: flex; 412 | justify-content: center; 413 | align-items: center; 414 | } 415 | 416 | #dashboard .onboard-switch .onoffswitch { 417 | width: 150px; 418 | margin-left: 0; 419 | } 420 | 421 | #dashboard .onboard-switch .onoffswitch-label { 422 | border-radius: 38px; 423 | } 424 | 425 | #dashboard .onboard-switch .onoffswitch-inner:before, 426 | #dashboard .onboard-switch .onoffswitch-inner:after { 427 | height: 75px; 428 | line-height: 75px; 429 | font-size: 24px; 430 | } 431 | 432 | #dashboard .onboard-switch .onoffswitch-inner:before { 433 | padding-left: 28px; 434 | } 435 | 436 | #dashboard .onboard-switch .onoffswitch-inner:after { 437 | padding-right: 28px; 438 | } 439 | 440 | #dashboard .onboard-switch .onoffswitch-switch { 441 | width: 63px; 442 | margin: 6px; 443 | right: 72px; 444 | border-radius: 32px; 445 | } 446 | 447 | #dashboard .onboard-switch .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { 448 | right: 0px; 449 | } 450 | 451 | /* Buttons Panel */ 452 | #dashboard .onboard-buttons .content { 453 | display: flex; 454 | justify-content: space-evenly; 455 | flex-wrap: wrap; 456 | align-items: center; 457 | } 458 | 459 | #dashboard .onboard-buttons .buttonpanel { 460 | display: flex; 461 | align-items: center; 462 | justify-content: center; 463 | background-color: #b0b0b0; 464 | border: 1px solid #878787; 465 | border-radius: 4px; 466 | width: 94px; 467 | height: 86px; 468 | } 469 | 470 | /* Round Button Widget */ 471 | #dashboard .onboard-buttons .roundbtn { 472 | width: 50px; 473 | height: 50px; 474 | border: 5px solid #333; 475 | display: inline-block; 476 | background-color: #000; 477 | -moz-border-radius: 50px; 478 | -webkit-border-radius: 50x; 479 | border-radius: 50px; 480 | -moz-transition: all 35ms linear; 481 | -o-transition: all 35ms linear; 482 | -webkit-transition: all 35ms linear; 483 | transition: all 35ms linear; 484 | -ms-transition: all 35ms linear; 485 | -moz-user-select: -moz-none; 486 | -ms-user-select: none; 487 | -webkit-user-select: none; 488 | user-select: none; 489 | } 490 | 491 | #dashboard .onboard-buttons .roundbtn .inner { 492 | display: flex; 493 | align-items: center; 494 | justify-content: center; 495 | position: relative; 496 | width: 50px; 497 | height: 50px; 498 | background-color: #454545; 499 | margin-top: -8px; 500 | border: 1px solid #676767; 501 | -moz-box-sizing: border-box; 502 | -webkit-box-sizing: border-box; 503 | box-sizing: border-box; 504 | -moz-border-radius: 50%; 505 | -webkit-border-radius: 50%; 506 | border-radius: 50%; 507 | -moz-box-shadow: none; 508 | -webkit-box-shadow: none; 509 | box-shadow: none; 510 | -moz-transition: margin 35ms 35ms linear; 511 | -o-transition: margin 35ms 35ms linear; 512 | -webkit-transition: margin 35ms 35ms; 513 | -webkit-transition-delay: linear, 0s; 514 | transition: margin 35ms 35ms linear; 515 | -ms-transition: margin 35ms 35ms linear; 516 | } 517 | 518 | #dashboard .onboard-buttons .roundbtn.pressed .inner { 519 | margin-top: 0; 520 | 521 | -moz-transition: margin 35ms linear; 522 | -o-transition: margin 35ms linear; 523 | -webkit-transition: margin 35ms linear; 524 | -webkit-transition-delay: 0s, linear; 525 | transition: margin 35ms linear; 526 | -ms-transition: margin 35ms linear; 527 | } 528 | 529 | #dashboard .onboard-buttons .text { 530 | color: white; 531 | text-shadow: rgba(0, 0, 0, 0.5) 0 0 5px; 532 | } 533 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Adafruit Web Bluetooth Dashboard 5 | 6 | 7 | 8 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 |
44 |
45 | 48 |
49 | 50 | 54 |
55 | 56 |
57 |
58 |
59 |
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 | --------------------------------------------------------------------------------