Spaces:
Paused
Paused
Download server/simulator.js from secutorpro/dispatch-console: direct link, hf CLI and curl.
- Browser
- Download file 4.91 kB
-
https://huggingface.co/spaces/secutorpro/dispatch-console/resolve/main/server/simulator.js
- Command line
-
hf download hf://spaces/secutorpro/dispatch-console/server/simulator.js
-
curl -L -o simulator.js https://huggingface.co/spaces/secutorpro/dispatch-console/resolve/main/server/simulator.js
4.91 kB
| // Simulated fleet for the dispatch console. | |
| // Usage (server running on :8080): node simulator.js | |
| // Env: SERVER_URL, SIM_UNITS, SIM_CENTER="lat,lng", ADMIN_USER, ADMIN_PASS, SIM_MINUTES | |
| import WebSocket from 'ws'; | |
| const BASE = process.env.SERVER_URL || 'http://localhost:8080'; | |
| const WS_URL = BASE.replace(/^http/, 'ws') + '/ws'; | |
| const N = Number(process.env.SIM_UNITS || 5); | |
| const CENTER = (process.env.SIM_CENTER || '48.8566,2.3522').split(',').map(Number); | |
| async function main() { | |
| const login = await fetch(`${BASE}/api/login`, { | |
| method: 'POST', | |
| headers: { 'content-type': 'application/json' }, | |
| body: JSON.stringify({ username: process.env.ADMIN_USER || 'admin', password: process.env.ADMIN_PASS || 'admin' }), | |
| }); | |
| if (!login.ok) throw new Error(`login failed: ${login.status}`); | |
| const { token } = await login.json(); | |
| const auth = { authorization: `Bearer ${token}`, 'content-type': 'application/json' }; | |
| const groups = await (await fetch(`${BASE}/api/groups`, { headers: auth })).json(); | |
| if (!groups.length) throw new Error('no talkgroups exist — create one first'); | |
| const groupId = groups[0].id; | |
| const existing = await (await fetch(`${BASE}/api/members`, { headers: auth })).json(); | |
| const units = []; | |
| for (let i = 1; i <= N; i++) { | |
| const deviceId = `sim-${i}`; | |
| let m = existing.find((x) => x.device_id === deviceId); | |
| if (!m) { | |
| const res = await fetch(`${BASE}/api/members`, { | |
| method: 'POST', | |
| headers: auth, | |
| body: JSON.stringify({ name: `SIM Unit ${i}`, callsign: `SIM-${i}`, device_id: deviceId, group_id: groupId }), | |
| }); | |
| m = await res.json(); | |
| } | |
| units.push({ | |
| m, | |
| ws: null, | |
| lat: CENTER[0] + (Math.random() - 0.5) * 0.08, | |
| lng: CENTER[1] + (Math.random() - 0.5) * 0.12, | |
| speed: 20 + Math.random() * 20, | |
| battery: 60 + Math.floor(Math.random() * 40), | |
| heading: Math.random() * 2 * Math.PI, | |
| }); | |
| } | |
| // Per-unit connection with automatic reconnect on any drop. | |
| function connectUnit(u) { | |
| const ws = new WebSocket(WS_URL); | |
| u.ws = ws; | |
| ws.on('open', () => { | |
| ws.send(JSON.stringify({ type: 'auth', token, memberId: u.m.id })); | |
| console.log(`[${u.m.name}] connected`); | |
| }); | |
| ws.on('message', (d) => { | |
| const msg = JSON.parse(d.toString()); | |
| if (msg.type === 'ptt_denied') console.log(`[${u.m.name}] ptt denied (held by ${msg.holder})`); | |
| }); | |
| ws.on('close', () => { | |
| u.ws = null; | |
| console.log(`[${u.m.name}] disconnected — reconnecting in 5 s`); | |
| setTimeout(() => connectUnit(u), 5000); | |
| }); | |
| ws.on('error', () => ws.terminate()); | |
| } | |
| units.forEach(connectUnit); | |
| const alive = (u) => u.ws && u.ws.readyState === WebSocket.OPEN; | |
| // GPS random walk every 5 s | |
| setInterval(() => { | |
| for (const u of units) { | |
| if (!alive(u)) continue; | |
| u.heading += (Math.random() - 0.5) * 0.8; | |
| u.speed = Math.max(8, Math.min(65, u.speed + (Math.random() - 0.5) * 12)); | |
| const d = (u.speed * 5) / 111000; | |
| u.lat += d * Math.cos(u.heading); | |
| u.lng += (d * Math.sin(u.heading)) / Math.cos((u.lat * Math.PI) / 180); | |
| if (Math.random() < 0.1) u.battery = Math.max(10, u.battery - 1); | |
| u.ws.send(JSON.stringify({ type: 'gps', lat: u.lat, lng: u.lng, speed: u.speed, battery: u.battery })); | |
| } | |
| }, 5000); | |
| // One SOS at ~25 s | |
| setTimeout(() => { | |
| const u = units.find(alive); | |
| if (u) { | |
| u.ws.send(JSON.stringify({ type: 'sos' })); | |
| console.log(`[${u.m.name}] SOS sent`); | |
| } | |
| }, 25000); | |
| // PTT floor turns with a 440 Hz tone, every ~29 s | |
| let turn = 0; | |
| let phase = 0; | |
| setInterval(() => { | |
| const u = units[turn % units.length]; | |
| turn++; | |
| if (!alive(u)) return; | |
| u.ws.send(JSON.stringify({ type: 'ptt_start' })); | |
| let sent = 0; | |
| const tone = setInterval(() => { | |
| if (!alive(u)) { clearInterval(tone); return; } | |
| const samples = Buffer.alloc(1600 * 2); | |
| for (let k = 0; k < 1600; k++) { | |
| samples.writeInt16LE(Math.round(12000 * Math.sin(phase + (k * 2 * Math.PI * 440) / 16000)), k * 2); | |
| } | |
| phase += (1600 * 2 * Math.PI * 440) / 16000; | |
| const head = Buffer.alloc(9); | |
| head.writeUInt8(0x01, 0); | |
| head.writeUInt32BE(groupId, 1); | |
| head.writeUInt32BE(u.m.id, 5); | |
| u.ws.send(Buffer.concat([head, samples])); | |
| if (++sent >= 40) { | |
| clearInterval(tone); | |
| if (alive(u)) u.ws.send(JSON.stringify({ type: 'ptt_stop' })); | |
| console.log(`[${u.m.name}] held the floor (4 s tone)`); | |
| } | |
| }, 100); | |
| }, 29000); | |
| const RUN_MS = (Number(process.env.SIM_MINUTES || 0) || 0) * 60 * 1000; | |
| if (RUN_MS) setTimeout(() => { | |
| console.log('sim duration reached — exiting'); | |
| process.exit(0); | |
| }, RUN_MS); | |
| console.log(`Simulating ${N} units against ${BASE} — Ctrl+C to stop.`); | |
| } | |
| main().catch((e) => { console.error(e); process.exit(1); }); |