Spaces:
Paused
Paused
File size: 4,908 Bytes
606de33 08f9027 d07a6aa 606de33 d07a6aa 606de33 d07a6aa 606de33 d07a6aa 606de33 d07a6aa 606de33 d07a6aa bf2247c d07a6aa bf2247c 606de33 bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c d07a6aa bf2247c 606de33 bf2247c 08f9027 bf2247c d07a6aa 606de33 d07a6aa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | // 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); }); |