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