1import {css, html, LitElement, TemplateResult} from 'lit'; 2import {customElement, property} from 'lit/decorators.js'; 3import {live} from 'lit/directives/live.js'; 4import {styleMap} from 'lit/directives/style-map.js'; 5 6import {Device, Notifiable, SimulationInfo, simulationState,} from './device-observer.js'; 7import {Chip, Chip_BleBeacon, Chip_Bluetooth, Chip_Radio,} from './netsim/model.js'; 8 9enum ChipKind { 10 UNSPECIFIED = 'UNSPECIFIED', 11 BLUETOOTH = 'BLUETOOTH', 12 WIFI = 'WIFI', 13 UWB = 'UWB', 14 BLUETOOTH_BEACON = 'BLUETOOTH_BEACON', 15 UNRECOGNIZED = 'UNRECOGNIZED', 16} 17 18const disabledCheckbox = html` 19 <input type="checkbox" disabled /> 20 <span 21 class="slider round" 22 style=${styleMap({ 23 opacity: '0.7', 24})} 25 ></span> 26`; 27 28@customElement('ns-device-info') 29export class DeviceInformation extends LitElement implements Notifiable { 30 // Selected Device on scene 31 @property() selectedDevice: Device|undefined; 32 33 /** 34 * the yaw value in orientation for ns-cube-sprite 35 * unit: deg 36 */ 37 @property({type: Number}) yaw = 0; 38 39 /** 40 * the pitch value in orientation for ns-cube-spriteß 41 * unit: deg 42 */ 43 @property({type: Number}) pitch = 0; 44 45 /** 46 * the roll value in orientation for ns-cube-sprite 47 * unit: deg 48 */ 49 @property({type: Number}) roll = 0; 50 51 /** 52 * The state of device info. True if edit mode. 53 */ 54 @property({type: Boolean}) editMode = false; 55 56 /** 57 * the x value in position for ns-cube-sprite 58 * unit: cm 59 */ 60 @property({type: Number}) posX = 0; 61 62 /** 63 * the y value in position for ns-cube-sprite 64 * unit: cm 65 */ 66 @property({type: Number}) posY = 0; 67 68 /** 69 * the z value in position for ns-cube-sprite 70 * unit: cm 71 */ 72 @property({type: Number}) posZ = 0; 73 74 holdRange = false; 75 76 static styles = css` 77 :host { 78 cursor: pointer; 79 display: grid; 80 place-content: center; 81 color: white; 82 font-size: 25px; 83 font-family: 'Lato', sans-serif; 84 border: 5px solid black; 85 border-radius: 12px; 86 padding: 10px; 87 background-color: #9199a5; 88 max-width: 600px; 89 } 90 91 .title { 92 font-weight: bold; 93 text-transform: uppercase; 94 text-align: center; 95 margin-bottom: 10px; 96 } 97 98 .setting { 99 display: grid; 100 grid-template-columns: auto auto; 101 margin-top: 0px; 102 margin-bottom: 30px; 103 //border: 3px solid black; 104 padding: 10px; 105 } 106 107 .setting .name { 108 grid-column: 1 / span 2; 109 text-transform: uppercase; 110 text-align: left; 111 margin-bottom: 10px; 112 font-weight: bold; 113 } 114 115 .label { 116 grid-column: 1; 117 text-align: left; 118 } 119 120 .info { 121 grid-column: 2; 122 text-align: right; 123 margin-bottom: 10px; 124 } 125 126 .switch { 127 position: relative; 128 float: right; 129 width: 60px; 130 height: 34px; 131 } 132 133 .switch input { 134 opacity: 0; 135 width: 0; 136 height: 0; 137 } 138 139 .slider { 140 position: absolute; 141 cursor: pointer; 142 top: 0; 143 left: 0; 144 right: 0; 145 bottom: 0; 146 background-color: #ccc; 147 -webkit-transition: 0.4s; 148 transition: 0.4s; 149 } 150 151 .slider:before { 152 position: absolute; 153 content: ''; 154 height: 26px; 155 width: 26px; 156 left: 4px; 157 bottom: 4px; 158 background-color: white; 159 -webkit-transition: 0.4s; 160 transition: 0.4s; 161 } 162 163 input:checked + .slider { 164 background-color: #2196f3; 165 } 166 167 input:focus + .slider { 168 box-shadow: 0 0 1px #2196f3; 169 } 170 171 input:checked + .slider:before { 172 -webkit-transform: translateX(26px); 173 -ms-transform: translateX(26px); 174 transform: translateX(26px); 175 } 176 177 /* Rounded sliders */ 178 .slider.round { 179 border-radius: 34px; 180 } 181 182 .slider.round:before { 183 border-radius: 50%; 184 } 185 186 .text { 187 display: inline-block; 188 position: relative; 189 width: 50px; 190 } 191 192 input[type='range'] { 193 width: 400px; 194 } 195 196 input[type='text'] { 197 width: 50%; 198 font-size: inherit; 199 text-align: right; 200 max-height: 25px; 201 } 202 203 input[type='text'].orientation { 204 max-width: 50px; 205 } 206 207 input[type='button'] { 208 display: inline; 209 font-size: inherit; 210 max-width: 200px; 211 } 212 `; 213 214 connectedCallback() { 215 super.connectedCallback(); // eslint-disable-line 216 simulationState.registerObserver(this); 217 } 218 219 disconnectedCallback() { 220 simulationState.removeObserver(this); 221 super.disconnectedCallback(); // eslint-disable-line 222 } 223 224 onNotify(data: SimulationInfo) { 225 if (data.selectedId && this.editMode === false) { 226 for (const device of data.devices) { 227 if (device.name === data.selectedId) { 228 this.selectedDevice = device; 229 if (!this.holdRange) { 230 this.yaw = device.orientation.yaw; 231 this.pitch = device.orientation.pitch; 232 this.roll = device.orientation.roll; 233 } 234 this.posX = Math.floor(device.position.x * 100); 235 this.posY = Math.floor(device.position.y * 100); 236 this.posZ = Math.floor(device.position.z * 100); 237 break; 238 } 239 } 240 } 241 } 242 243 private changeRange(ev: InputEvent) { 244 this.holdRange = true; 245 console.assert(this.selectedDevice !== null); // eslint-disable-line 246 const range = ev.target as HTMLInputElement; 247 const event = new CustomEvent('orientationEvent', { 248 detail: { 249 name: this.selectedDevice?.name, 250 type: range.id, 251 value: range.value, 252 }, 253 }); 254 window.dispatchEvent(event); 255 if (range.id === 'yaw') { 256 this.yaw = Number(range.value); 257 } else if (range.id === 'pitch') { 258 this.pitch = Number(range.value); 259 } else { 260 this.roll = Number(range.value); 261 } 262 } 263 264 private patchOrientation() { 265 this.holdRange = false; 266 console.assert(this.selectedDevice !== undefined); // eslint-disable-line 267 if (this.selectedDevice === undefined) return; 268 this.selectedDevice.orientation = { 269 yaw: this.yaw, 270 pitch: this.pitch, 271 roll: this.roll, 272 }; 273 simulationState.patchDevice({ 274 device: { 275 name: this.selectedDevice.name, 276 orientation: this.selectedDevice.orientation, 277 }, 278 }); 279 } 280 281 private patchRadio() { 282 console.assert(this.selectedDevice !== undefined); // eslint-disable-line 283 if (this.selectedDevice === undefined) return; 284 simulationState.patchDevice({ 285 device: { 286 name: this.selectedDevice.name, 287 chips: this.selectedDevice.chips, 288 }, 289 }); 290 } 291 292 private handleEditForm() { 293 if (this.editMode) { 294 simulationState.invokeGetDevice(); 295 this.editMode = false; 296 } else { 297 this.editMode = true; 298 } 299 } 300 301 static checkPositionBound(value: number) { 302 return value > 10 ? 10 : value < 0 ? 0 : value; // eslint-disable-line 303 } 304 305 static checkOrientationBound(value: number) { 306 return value > 90 ? 90 : value < -90 ? -90 : value; // eslint-disable-line 307 } 308 309 private handleSave() { 310 console.assert(this.selectedDevice !== undefined); // eslint-disable-line 311 if (this.selectedDevice === undefined) return; 312 const elements = this.renderRoot.querySelectorAll(`[id^="edit"]`); 313 const obj: Record<string, any> = { 314 name: this.selectedDevice.name, 315 position: this.selectedDevice.position, 316 orientation: this.selectedDevice.orientation, 317 }; 318 elements.forEach(element => { 319 const inputElement = element as HTMLInputElement; 320 if (inputElement.id === 'editName') { 321 obj.name = inputElement.value; 322 } else if (inputElement.id.startsWith('editPos')) { 323 if (!Number.isNaN(Number(inputElement.value))) { 324 obj.position[inputElement.id.slice(7).toLowerCase()] = 325 DeviceInformation.checkPositionBound( 326 Number(inputElement.value) / 100); 327 } 328 } else if (inputElement.id.startsWith('editOri')) { 329 if (!Number.isNaN(Number(inputElement.value))) { 330 obj.orientation[inputElement.id.slice(7).toLowerCase()] = 331 DeviceInformation.checkOrientationBound( 332 Number(inputElement.value)); 333 } 334 } 335 }); 336 this.selectedDevice.name = obj.name; 337 this.selectedDevice.position = obj.position; 338 this.selectedDevice.orientation = obj.orientation; 339 this.handleEditForm(); 340 simulationState.patchDevice({ 341 device: obj, 342 }); 343 } 344 345 private handleGetBleBeacon(ble_beacon: Chip_BleBeacon) { 346 const handleGetSettings = () => { 347 if (!ble_beacon.settings) { 348 return html``; 349 } 350 351 return html`<div class="setting"> 352 <div class="name">Settings</div> 353 354 ${ 355 ble_beacon.settings.advertiseMode ? 356 html`<div class="label">Advertise Mode:</div> 357 <div class="info"> 358 ${ble_beacon.settings.advertiseMode?.replace('-', ' ')} 359 </div>` : 360 html`<div class="label">Advertise Interval:</div> 361 <div class="info"> 362 ${ble_beacon.settings.milliseconds?.toString().concat(' ms')} 363 </div>`} 364 ${ 365 ble_beacon.settings.txPowerLevel ? 366 html`<div class="label">Transmit Power Level:</div> 367 <div class="info"> 368 ${ble_beacon.settings.txPowerLevel?.replace('-', ' ')} 369 </div>` : 370 html`<div class="label">Transmit Power:</div> 371 <div class="info"> 372 ${ble_beacon.settings.dbm?.toString().concat(' dBm')} 373 </div>`} 374 375 <div class="label">Scannable:</div> 376 <div class="info">${ble_beacon.settings.scannable}</div> 377 378 <div class="label">Timeout:</div> 379 <div class="info"> 380 ${ble_beacon.settings.timeout?.toString().concat(' ms')} 381 </div> 382 </div>`; 383 }; 384 385 const handleGetAdvData = () => { 386 if (!ble_beacon.advData) { 387 return html``; 388 } 389 390 return html`<div class="setting"> 391 <div class="name">Advertise Data</div> 392 393 <div class="label">Include Device Name:</div> 394 <div class="info">${ble_beacon.advData.includeDeviceName}</div> 395 396 <div class="label">Include Transmit Power:</div> 397 <div class="info">${ble_beacon.advData.includeTxPowerLevel}</div> 398 399 ${ 400 ble_beacon.advData.manufacturerData.length ? 401 html` <div class="label">Manufacturer Data Length:</div> 402 <div class="info"> 403 ${ble_beacon.advData.manufacturerData.length} 404 </div>` : 405 html``} 406 ${ 407 ble_beacon.advData.services.length ? 408 html` <div class="label">Number of Supported Services:</div> 409 <div class="info">${ble_beacon.advData.services.length}</div>` : 410 html``} 411 </div>`; 412 }; 413 414 return html`${handleGetSettings()} ${handleGetAdvData()}`; 415 } 416 417 private getRadioCheckbox = (radio: Chip_Radio, id: string) => { 418 return html`<label class="switch"> 419 <input 420 id=${id} 421 type="checkbox" 422 .checked=${live(radio.state)} 423 @click=${() => { 424 // eslint-disable-next-line 425 this.selectedDevice?.toggleChipState(radio); 426 this.patchRadio(); 427 }} 428 /> 429 <span class="slider round"></span> 430 </label> `; 431 }; 432 433 private getBluetoothRadioCheckboxes(bt: Chip_Bluetooth) { 434 let lowEnergyCheckbox = undefined; 435 let classicCheckbox = undefined; 436 437 if ('lowEnergy' in bt && bt.lowEnergy) { 438 lowEnergyCheckbox = this.getRadioCheckbox(bt.lowEnergy, 'lowEnergy'); 439 } 440 if ('classic' in bt && bt.classic) { 441 classicCheckbox = this.getRadioCheckbox(bt.classic, 'classic'); 442 } 443 444 return [lowEnergyCheckbox, classicCheckbox]; 445 } 446 447 private handleGetChip(chip: Chip, idx: number) { 448 if (chip.bleBeacon) { 449 let checkboxes: {[name: string]: undefined|TemplateResult} = {}; 450 451 if (chip.bleBeacon.bt) { 452 [checkboxes['Bluetooth LE'], checkboxes['Bluetooth Classic']] = 453 this.getBluetoothRadioCheckboxes(chip.bleBeacon.bt); 454 } 455 456 return html`<div class="title"> 457 Chip ${idx + 1}: ${chip.kind.replace('_', ' ')} 458 </div> 459 <div class="setting"> 460 <div class="name">Name</div> 461 <div class="info">${chip.name}</div> 462 </div> 463 <div class="setting"> 464 ${ 465 Object.entries(checkboxes).length ? 466 html`<div class="name">Radios</div>` : 467 html``} 468 ${ 469 Object.entries(checkboxes) 470 .map(([name, template]) => html`<div class="label">${name}</div> 471 <div class="info">${template}</div>`)} 472 </div> 473 ${this.handleGetBleBeacon(chip.bleBeacon)}`; 474 } else { 475 return ``; 476 } 477 } 478 479 private handleGetChips() { 480 if (!(this.selectedDevice && this.selectedDevice.chips)) { 481 return html``; 482 } 483 484 const isBuiltin = (chip: Chip) => 485 chip.kind === ChipKind.BLUETOOTH_BEACON && chip.bleBeacon; 486 487 // If any chip is a builtin, display individual chip information 488 if (this.selectedDevice.chips.some(chip => isBuiltin(chip))) { 489 return html`${ 490 this.selectedDevice.chips.map( 491 (chip, idx) => this.handleGetChip(chip, idx))}`; 492 } 493 494 // Otherwise, just display the radios 495 let checkboxes: {[name: string]: undefined|TemplateResult} = {}; 496 for (const chip of this.selectedDevice.chips) { 497 if (chip) { 498 if (chip.bt) { 499 [checkboxes['Bluetooth LE'], checkboxes['Bluetooth Classic']] = 500 this.getBluetoothRadioCheckboxes(chip.bt); 501 } 502 if (chip.wifi) { 503 checkboxes['WIFI'] = this.getRadioCheckbox(chip.wifi, 'wifi'); 504 } 505 if (chip.uwb) { 506 checkboxes['UWB'] = this.getRadioCheckbox(chip.uwb, 'uwb'); 507 } 508 } 509 } 510 511 if (Object.keys(checkboxes).length) { 512 return html`<div class="setting"> 513 <div class="name">Radios</div> 514 ${ 515 Object.entries(checkboxes) 516 .map(([name, template]) => html`<div class="label">${name}</div> 517 <div class="info">${template}</div>`)} 518 </div>`; 519 } else { 520 return html``; 521 } 522 } 523 524 render() { 525 return html`${ 526 this.selectedDevice ? 527 html` 528 <div class="title">Device Info</div> 529 <div class="setting"> 530 <div class="name">Name</div> 531 <div class="info">${this.selectedDevice.name}</div> 532 </div> 533 <div class="setting"> 534 <div class="name">Position</div> 535 <div class="label">X</div> 536 <div 537 class="info" 538 style=${styleMap({ 539 color: 'red', 540 })} 541 > 542 ${ 543 this.editMode ? html`<input 544 type="text" 545 id="editPosX" 546 .value=${this.posX.toString()} 547 />` : 548 html`${this.posX}`} 549 </div> 550 <div class="label">Y</div> 551 <div 552 class="info" 553 style=${styleMap({ 554 color: 'green', 555 })} 556 > 557 ${ 558 this.editMode ? html`<input 559 type="text" 560 id="editPosY" 561 .value=${this.posY.toString()} 562 />` : 563 html`${this.posY}`} 564 </div> 565 <div class="label">Z</div> 566 <div 567 class="info" 568 style=${styleMap({ 569 color: 'blue', 570 })} 571 > 572 ${ 573 this.editMode ? html`<input 574 type="text" 575 id="editPosZ" 576 .value=${this.posZ.toString()} 577 />` : 578 html`${this.posZ}`} 579 </div> 580 </div> 581 <div class="setting"> 582 <div class="name">Orientation</div> 583 <div class="label">Yaw</div> 584 <div class="info"> 585 <input 586 id="yaw" 587 type="range" 588 min="-90" 589 max="90" 590 .value=${this.yaw.toString()} 591 .disabled=${this.editMode} 592 @input=${this.changeRange} 593 @change=${this.patchOrientation} 594 /> 595 ${ 596 this.editMode ? html`<input 597 type="text" 598 id="editOriYaw" 599 class="orientation" 600 .value=${this.yaw.toString()} 601 />` : 602 html`<div class="text">(${this.yaw})</div>`} 603 </div> 604 <div class="label">Pitch</div> 605 <div class="info"> 606 <input 607 id="pitch" 608 type="range" 609 min="-90" 610 max="90" 611 .value=${this.pitch.toString()} 612 .disabled=${this.editMode} 613 @input=${this.changeRange} 614 @change=${this.patchOrientation} 615 /> 616 ${ 617 this.editMode ? html`<input 618 type="text" 619 id="editOriPitch" 620 class="orientation" 621 .value=${this.pitch.toString()} 622 />` : 623 html`<div class="text">(${this.pitch})</div>`} 624 </div> 625 <div class="label">Roll</div> 626 <div class="info"> 627 <input 628 id="roll" 629 type="range" 630 min="-90" 631 max="90" 632 .value=${this.roll.toString()} 633 .disabled=${this.editMode} 634 @input=${this.changeRange} 635 @change=${this.patchOrientation} 636 /> 637 ${ 638 this.editMode ? html`<input 639 type="text" 640 id="editOriRoll" 641 class="orientation" 642 .value=${this.roll.toString()} 643 />` : 644 html`<div class="text">(${this.roll})</div>`} 645 </div> 646 </div> 647 <div class="setting"> 648 ${ 649 this.editMode ? html` 650 <input type="button" value="Save" @click=${this.handleSave} /> 651 <input 652 type="button" 653 value="Cancel" 654 @click=${this.handleEditForm} 655 /> 656 ` : 657 html`<input 658 type="button" 659 value="Edit" 660 @click=${this.handleEditForm} 661 />`} 662 </div> 663 664 ${this.handleGetChips()} 665 ` : 666 html`<div class="title">Device Info</div>`}`; 667 } 668} 669