xref: /aosp_15_r20/tools/netsim/ui/ts/device-observer.ts (revision cf78ab8cffb8fc9207af348f23af247fb04370a6)
1import {Capture, Chip, Chip_Radio, Device as ProtoDevice,} from './netsim/model.js';
2
3// URL for netsim
4const DEVICES_URL = './v1/devices';
5const CAPTURES_URL = './v1/captures';
6
7/**
8 * Interface for a method in notifying the subscribed observers.
9 * Subscribed observers must implement this interface.
10 */
11export interface Notifiable {
12  onNotify(data: {}): void;
13}
14
15/**
16 * Modularization of Device.
17 * Contains getters and setters for properties in Device interface.
18 */
19export class Device {
20  device: ProtoDevice;
21
22  constructor(device: ProtoDevice) {
23    this.device = device;
24  }
25
26  get name(): string {
27    return this.device.name;
28  }
29
30  set name(value: string) {
31    this.device.name = value;
32  }
33
34  get position(): {x: number; y: number; z: number} {
35    const result = {x: 0, y: 0, z: 0};
36    if ('position' in this.device && this.device.position &&
37        typeof this.device.position === 'object') {
38      if ('x' in this.device.position &&
39          typeof this.device.position.x === 'number') {
40        result.x = this.device.position.x;
41      }
42      if ('y' in this.device.position &&
43          typeof this.device.position.y === 'number') {
44        result.y = this.device.position.y;
45      }
46      if ('z' in this.device.position &&
47          typeof this.device.position.z === 'number') {
48        result.z = this.device.position.z;
49      }
50    }
51    return result;
52  }
53
54  set position(pos: {x: number; y: number; z: number}) {
55    this.device.position = pos;
56  }
57
58  get orientation(): {yaw: number; pitch: number; roll: number} {
59    const result = {yaw: 0, pitch: 0, roll: 0};
60    if ('orientation' in this.device && this.device.orientation &&
61        typeof this.device.orientation === 'object') {
62      if ('yaw' in this.device.orientation &&
63          typeof this.device.orientation.yaw === 'number') {
64        result.yaw = this.device.orientation.yaw;
65      }
66      if ('pitch' in this.device.orientation &&
67          typeof this.device.orientation.pitch === 'number') {
68        result.pitch = this.device.orientation.pitch;
69      }
70      if ('roll' in this.device.orientation &&
71          typeof this.device.orientation.roll === 'number') {
72        result.roll = this.device.orientation.roll;
73      }
74    }
75    return result;
76  }
77
78  set orientation(ori: {yaw: number; pitch: number; roll: number}) {
79    this.device.orientation = ori;
80  }
81
82  // TODO modularize getters and setters for Chip Interface
83  get chips(): Chip[] {
84    return this.device.chips ?? [];
85  }
86
87  // TODO modularize getters and setters for Chip Interface
88  set chips(value: Chip[]) {
89    this.device.chips = value;
90  }
91
92  get visible(): boolean {
93    return Boolean(this.device.visible);
94  }
95
96  set visible(value: boolean) {
97    this.device.visible = value;
98  }
99
100  toggleChipState(radio: Chip_Radio) {
101    radio.state = !radio.state;
102  }
103
104  toggleCapture(device: Device, chip: Chip) {
105    if ('capture' in chip && chip.capture) {
106      chip.capture = !chip.capture;
107      simulationState.patchDevice({
108        device: {
109          name: device.name,
110          chips: device.chips,
111        },
112      });
113    }
114  }
115}
116
117/**
118 * The most recent state of the simulation.
119 * Subscribed observers must refer to this info and patch accordingly.
120 */
121export interface SimulationInfo {
122  devices: Device[];
123  captures: Capture[];
124  selectedId: string;
125  dimension: {x: number; y: number; z: number};
126  lastModified: string;
127}
128
129interface Observable {
130  registerObserver(elem: Notifiable): void;
131  removeObserver(elem: Notifiable): void;
132}
133
134class SimulationState implements Observable {
135  private observers: Notifiable[] = [];
136
137  private simulationInfo: SimulationInfo = {
138    devices: [],
139    captures: [],
140    selectedId: '',
141    dimension: {x: 10, y: 10, z: 0},
142    lastModified: '',
143  };
144
145  constructor() {
146    // initial GET
147    this.invokeGetDevice();
148    this.invokeListCaptures();
149  }
150
151  async invokeGetDevice() {
152    await fetch(DEVICES_URL, {
153      method: 'GET',
154    })
155        .then(response => response.json())
156        .then(data => {
157          this.fetchDevice(data.devices);
158          this.updateLastModified(data.lastModified);
159        })
160        .catch(error => {
161          // eslint-disable-next-line
162          console.log('Cannot connect to netsim web server', error);
163        });
164  }
165
166  async invokeListCaptures() {
167    await fetch(CAPTURES_URL, {
168      method: 'GET',
169    })
170        .then(response => response.json())
171        .then(data => {
172          this.simulationInfo.captures = data.captures;
173          this.notifyObservers();
174        })
175        .catch(error => {
176          console.log('Cannot connect to netsim web server', error);
177        });
178  }
179
180  fetchDevice(devices?: ProtoDevice[]) {
181    this.simulationInfo.devices = [];
182    if (devices) {
183      this.simulationInfo.devices = devices.map(device => new Device(device));
184    }
185    this.notifyObservers();
186  }
187
188  getLastModified() {
189    return this.simulationInfo.lastModified;
190  }
191
192  updateLastModified(timestamp: string) {
193    this.simulationInfo.lastModified = timestamp;
194  }
195
196  patchSelected(id: string) {
197    this.simulationInfo.selectedId = id;
198    this.notifyObservers();
199  }
200
201  handleDrop(id: string, x: number, y: number) {
202    this.simulationInfo.selectedId = id;
203    for (const device of this.simulationInfo.devices) {
204      if (id === device.name) {
205        device.position = {x, y, z: device.position.z};
206        this.patchDevice({
207          device: {
208            name: device.name,
209            position: device.position,
210          },
211        });
212        break;
213      }
214    }
215  }
216
217  patchCapture(id: string, state: string) {
218    fetch(CAPTURES_URL + '/' + id, {
219      method: 'PATCH',
220      headers: {
221        'Content-Type': 'text/plain',
222        'Content-Length': state.length.toString(),
223      },
224      body: state,
225    });
226    this.notifyObservers();
227  }
228
229  patchDevice(obj: object) {
230    const jsonBody = JSON.stringify(obj);
231    fetch(DEVICES_URL, {
232      method: 'PATCH',
233      headers: {
234        'Content-Type': 'application/json',
235        'Content-Length': jsonBody.length.toString(),
236      },
237      body: jsonBody,
238    })
239        .then(response => response.json())
240        .catch(error => {
241          // eslint-disable-next-line
242          console.error('Error:', error);
243        });
244    this.notifyObservers();
245  }
246
247  registerObserver(elem: Notifiable) {
248    this.observers.push(elem);
249    elem.onNotify(this.simulationInfo);
250  }
251
252  removeObserver(elem: Notifiable) {
253    const index = this.observers.indexOf(elem);
254    this.observers.splice(index, 1);
255  }
256
257  notifyObservers() {
258    for (const observer of this.observers) {
259      observer.onNotify(this.simulationInfo);
260    }
261  }
262
263  getDeviceList() {
264    return this.simulationInfo.devices;
265  }
266}
267
268/** Subscribed observers must register itself to the simulationState */
269export const simulationState = new SimulationState();
270
271async function subscribeCaptures() {
272  const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
273  while (true) {
274    await simulationState.invokeListCaptures();
275    await simulationState.invokeGetDevice();
276    await delay(1000);
277  }
278}
279
280async function subscribeDevices() {
281  await simulationState.invokeGetDevice();
282  while (true) {
283    const jsonBody = JSON.stringify({
284      lastModified: simulationState.getLastModified(),
285    });
286    await fetch(DEVICES_URL, {
287      method: 'SUBSCRIBE',
288      headers: {
289        'Content-Type': 'application/json',
290        'Content-Length': jsonBody.length.toString(),
291      },
292      body: jsonBody,
293    })
294        .then(response => response.json())
295        .then(data => {
296          simulationState.fetchDevice(data.devices);
297          simulationState.updateLastModified(data.lastModified);
298        })
299        .catch(error => {
300          // eslint-disable-next-line
301          console.log('Cannot connect to netsim web server', error);
302        });
303  }
304}
305
306subscribeCaptures();
307subscribeDevices();
308