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 for (const device of this.simulationInfo.devices) { 203 if (id === device.name) { 204 device.position = {x, y, z: device.position.z}; 205 this.patchDevice({ 206 device: { 207 name: device.name, 208 position: device.position, 209 }, 210 }); 211 break; 212 } 213 } 214 } 215 216 patchCapture(id: string, state: string) { 217 fetch(CAPTURES_URL + '/' + id, { 218 method: 'PATCH', 219 headers: { 220 'Content-Type': 'text/plain', 221 'Content-Length': state.length.toString(), 222 }, 223 body: state, 224 }); 225 this.notifyObservers(); 226 } 227 228 patchDevice(obj: object) { 229 const jsonBody = JSON.stringify(obj); 230 fetch(DEVICES_URL, { 231 method: 'PATCH', 232 headers: { 233 'Content-Type': 'application/json', 234 'Content-Length': jsonBody.length.toString(), 235 }, 236 body: jsonBody, 237 }) 238 .then(response => response.json()) 239 .catch(error => { 240 // eslint-disable-next-line 241 console.error('Error:', error); 242 }); 243 this.notifyObservers(); 244 } 245 246 registerObserver(elem: Notifiable) { 247 this.observers.push(elem); 248 elem.onNotify(this.simulationInfo); 249 } 250 251 removeObserver(elem: Notifiable) { 252 const index = this.observers.indexOf(elem); 253 this.observers.splice(index, 1); 254 } 255 256 notifyObservers() { 257 for (const observer of this.observers) { 258 observer.onNotify(this.simulationInfo); 259 } 260 } 261 262 getDeviceList() { 263 return this.simulationInfo.devices; 264 } 265} 266 267/** Subscribed observers must register itself to the simulationState */ 268export const simulationState = new SimulationState(); 269 270async function subscribeCaptures() { 271 const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); 272 while (true) { 273 await simulationState.invokeListCaptures(); 274 await simulationState.invokeGetDevice(); 275 await delay(1000); 276 } 277} 278 279async function subscribeDevices() { 280 await simulationState.invokeGetDevice(); 281 while (true) { 282 const jsonBody = JSON.stringify({ 283 lastModified: simulationState.getLastModified(), 284 }); 285 await fetch(DEVICES_URL, { 286 method: 'SUBSCRIBE', 287 headers: { 288 'Content-Type': 'application/json', 289 'Content-Length': jsonBody.length.toString(), 290 }, 291 body: jsonBody, 292 }) 293 .then(response => response.json()) 294 .then(data => { 295 simulationState.fetchDevice(data.devices); 296 simulationState.updateLastModified(data.lastModified); 297 }) 298 .catch(error => { 299 // eslint-disable-next-line 300 console.log('Cannot connect to netsim web server', error); 301 }); 302 } 303} 304 305subscribeCaptures(); 306subscribeDevices(); 307