1import {Capture, Chip, Device as ProtoDevice, State} from './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 this.device.visible ?? true; 94 } 95 96 set visible(value: boolean) { 97 this.device.visible = value; 98 } 99 100 toggleChipState(chip: Chip, btType?: string) { 101 if ('bt' in chip && chip.bt) { 102 if (typeof (btType) === 'undefined') { 103 // eslint-disable-next-line 104 console.log( 105 'netsim-ui: must specify lowEnergy or classic for Bluetooth'); 106 return; 107 } 108 if (btType === 'lowEnergy' && 'lowEnergy' in chip.bt && 109 chip.bt.lowEnergy) { 110 if ('state' in chip.bt.lowEnergy) { 111 chip.bt.lowEnergy.state = 112 chip.bt.lowEnergy.state === State.ON ? State.OFF : State.ON; 113 } 114 } 115 if (btType === 'classic' && 'classic' in chip.bt && chip.bt.classic) { 116 if ('state' in chip.bt.classic) { 117 chip.bt.classic.state = 118 chip.bt.classic.state === State.ON ? State.OFF : State.ON; 119 } 120 } 121 } 122 if ('wifi' in chip && chip.wifi) { 123 if ('state' in chip.wifi) { 124 chip.wifi.state = chip.wifi.state === State.ON ? State.OFF : State.ON; 125 } 126 } 127 if ('uwb' in chip && chip.uwb) { 128 if ('state' in chip.uwb) { 129 chip.uwb.state = chip.uwb.state === State.ON ? State.OFF : State.ON; 130 } 131 } 132 } 133 134 toggleCapture(device: Device, chip: Chip) { 135 if ('capture' in chip && chip.capture) { 136 chip.capture = chip.capture === State.ON ? State.OFF : State.ON; 137 simulationState.patchDevice({ 138 device: { 139 name: device.name, 140 chips: device.chips, 141 } 142 }); 143 } 144 } 145} 146 147/** 148 * The most recent state of the simulation. 149 * Subscribed observers must refer to this info and patch accordingly. 150 */ 151export interface SimulationInfo { 152 devices: Device[]; 153 captures: Capture[]; 154 selectedId: string; 155 dimension: {x: number; y: number; z: number;}; 156} 157 158interface Observable { 159 registerObserver(elem: Notifiable): void; 160 removeObserver(elem: Notifiable): void; 161} 162 163class SimulationState implements Observable { 164 private observers: Notifiable[] = []; 165 166 private simulationInfo: SimulationInfo = { 167 devices: [], 168 captures: [], 169 selectedId: '', 170 dimension: {x: 10, y: 10, z: 0}, 171 }; 172 173 constructor() { 174 // initial GET 175 this.invokeGetDevice(); 176 this.invokeListCaptures(); 177 } 178 179 invokeGetDevice() { 180 fetch(DEVICES_URL, { 181 method: 'GET', 182 }) 183 .then(response => response.json()) 184 .then(data => { 185 this.fetchDevice(data.devices); 186 }) 187 .catch(error => { 188 // eslint-disable-next-line 189 console.log('Cannot connect to netsim web server', error); 190 }); 191 } 192 193 invokeListCaptures() { 194 fetch(CAPTURES_URL, { 195 method: 'GET', 196 }) 197 .then(response => response.json()) 198 .then(data => { 199 this.simulationInfo.captures = data.captures; 200 }) 201 .catch(error => { 202 console.log('Cannot connect to netsim web server', error); 203 }) 204 } 205 206 fetchDevice(devices: ProtoDevice[]) { 207 this.simulationInfo.devices = []; 208 for (const device of devices) { 209 this.simulationInfo.devices.push(new Device(device)); 210 } 211 this.notifyObservers(); 212 } 213 214 patchSelected(id: string) { 215 this.simulationInfo.selectedId = id; 216 this.notifyObservers(); 217 } 218 219 handleDrop(id: string, x: number, y: number) { 220 for (const device of this.simulationInfo.devices) { 221 if (id === device.name) { 222 device.position = {x, y, z: device.position.z}; 223 this.patchDevice({ 224 device: { 225 name: device.name, 226 position: device.position, 227 }, 228 }); 229 break; 230 } 231 } 232 } 233 234 patchCapture(id: string, state: string) { 235 fetch(CAPTURES_URL + '/' + id, { 236 method: 'PATCH', 237 headers: { 238 'Content-Type': 'text/plain', 239 'Content-Length': state.length.toString(), 240 }, 241 body: state, 242 }); 243 this.notifyObservers(); 244 } 245 246 patchDevice(obj: object) { 247 const jsonBody = JSON.stringify(obj); 248 fetch(DEVICES_URL, { 249 method: 'PATCH', 250 headers: { 251 'Content-Type': 'application/json', 252 'Content-Length': jsonBody.length.toString(), 253 }, 254 body: jsonBody, 255 }) 256 .then(response => response.json()) 257 .catch(error => { 258 // eslint-disable-next-line 259 console.error('Error:', error); 260 }); 261 this.notifyObservers(); 262 } 263 264 registerObserver(elem: Notifiable) { 265 this.observers.push(elem); 266 elem.onNotify(this.simulationInfo); 267 } 268 269 removeObserver(elem: Notifiable) { 270 const index = this.observers.indexOf(elem); 271 this.observers.splice(index, 1); 272 } 273 274 notifyObservers() { 275 for (const observer of this.observers) { 276 observer.onNotify(this.simulationInfo); 277 } 278 } 279 280 getDeviceList() { 281 return this.simulationInfo.devices; 282 } 283} 284 285/** Subscribed observers must register itself to the simulationState */ 286export const simulationState = new SimulationState(); 287 288async function subscribe() { 289 const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); 290 while (true) { 291 simulationState.invokeGetDevice(); 292 simulationState.invokeListCaptures(); 293 await delay(1000); 294 } 295} 296 297subscribe(); 298