dispatch-console / server /simulator.js
secutorpro's picture
fix: remove stray duplicate tone declaration
08f9027 verified
Raw History Blame Contribute Delete
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); });