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