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); });