-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
320 lines (290 loc) · 11.9 KB
/
Copy pathserver.js
File metadata and controls
320 lines (290 loc) · 11.9 KB
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
// serialport v10+ exports named objects
const { SerialPort } = require('serialport');
const { ReadlineParser } = require('@serialport/parser-readline');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
const PORT = process.env.PORT || 3000;
let SERIAL_PORT = process.env.SERIAL_PORT || null;
const BAUD = parseInt(process.env.BAUD || '115200', 10);
const SIMULATOR = (process.env.SIMULATOR || 'false').toLowerCase() === 'true' || process.env.SIMULATOR === '1';
// If set, force simulator even when a device is present. Default false.
const FORCE_SIMULATOR = (process.env.FORCE_SIMULATOR || 'false').toLowerCase() === 'true' || process.env.FORCE_SIMULATOR === '1';
// ONAVI mode: poll the ONavi seismic sensor which uses a binary 9-byte frame
// and responds when polled with a '*' character. Enable with ONAVI=true
const ONAVI = (process.env.ONAVI || 'false').toLowerCase() === 'true' || process.env.ONAVI === '1';
app.use(express.static('public'));
// keep a small replay buffer of recent samples so new clients see recent data
const RECENT_BUFFER = 800;
const recentSamples = [];
let lastEmitTs = 0;
// let fallbackTimer = null;
io.on('connection', (socket) => {
console.log('client connected');
// inform this client of current device status on connect
if (SERIAL_PORT) socket.emit('device-status', { connected: true, port: SERIAL_PORT });
else if (SIMULATOR) socket.emit('device-status', { connected: false, simulator: true });
else socket.emit('device-status', { connected: false, simulator: false });
// replay recent samples to the newly-connected socket so the UI is not empty
try {
if (recentSamples.length > 0) {
// send as individual 'accel' events so client-side handlers are unchanged
recentSamples.forEach(s => { try { socket.emit('accel', s); } catch (e) {} });
}
} catch (e) { /* ignore replay errors */ }
});
function emitSample(x, y, z) {
const payload = { t: Date.now(), x: Number(x), y: Number(y), z: Number(z) };
// log emitted payload for debugging
console.log('emitSample', payload);
// store in recent buffer and broadcast
try {
recentSamples.push(payload);
lastEmitTs = Date.now();
if (recentSamples.length > RECENT_BUFFER) recentSamples.shift();
} catch (e) {}
io.emit('accel', payload);
// clear fallback timer since we have data
// if (fallbackTimer) {
// clearTimeout(fallbackTimer);
// fallbackTimer = null;
// }
}
// helper to start simulator
function startSimulator() {
console.log('Starting simulator (50Hz)');
io.emit('device-status', { connected: false, simulator: true });
setInterval(() => {
const t = Date.now() / 1000;
const ax = Math.sin(t * 2) * 0.5 + (Math.random() - 0.5) * 0.05;
const ay = Math.cos(t * 1.7) * 0.6 + (Math.random() - 0.5) * 0.05;
const az = Math.sin(t * 1.3) * 0.2 + (Math.random() - 0.5) * 0.02;
emitSample(ax, ay, az);
}, 20); // 50Hz
}
async function tryStartSerialOrSimulator() {
if (SERIAL_PORT) {
console.log(`Opening serial port ${SERIAL_PORT} at ${BAUD} baud`);
// v10+ constructor takes an options object: { path, baudRate }
return new SerialPort({ path: SERIAL_PORT, baudRate: BAUD });
}
// try to auto-detect a likely ONavi/USB-CDC device if SERIAL_PORT not provided
try {
const list = await SerialPort.list();
// prefer common TTY names for CDC devices
const candidate = list.find(p => {
const path = (p.path || p.location || '').toLowerCase();
const vid = (p.vendorId || '').toLowerCase();
const pid = (p.productId || '').toLowerCase();
if (!path) return false;
if (/usbmodem|xrusbmodem|ttyacm|cu.usbmodem|cu.xrusbmodem|usbserial/.test(path)) return true;
// vendor/product heuristics (optional)
if (vid && pid) return true;
return false;
});
if (candidate && !FORCE_SIMULATOR) {
SERIAL_PORT = candidate.path || candidate.location || null;
if (SERIAL_PORT) {
console.log(`Auto-detected serial device: ${SERIAL_PORT} (will open)`);
io.emit('device-status', { connected: false, port: SERIAL_PORT, auto: true });
return new SerialPort({ path: SERIAL_PORT, baudRate: BAUD });
}
}
} catch (e) {
console.warn('Auto-detect serial list failed:', e && e.message);
}
// fall back to simulator only if requested
if (SIMULATOR) {
if (FORCE_SIMULATOR) {
console.log('FORCE_SIMULATOR set — starting simulator');
startSimulator();
return null;
}
console.log('No SERIAL_PORT found — starting simulator (SIMULATOR=true)');
startSimulator();
return null;
}
console.log('No SERIAL_PORT set and SIMULATOR not enabled — no data will be produced');
io.emit('device-status', { connected: false, simulator: false, message: 'No device and simulator disabled' });
return null;
}
// If a port has an error or closes, try to reconnect (auto-detect again).
function scheduleReconnect(port) {
try {
console.log('Scheduling reconnect attempt in 2s');
if (port && port.isOpen) {
try { port.close(); } catch (e) {}
}
} catch (e) {}
setTimeout(() => {
tryStartSerialOrSimulator().then((newPort) => {
if (newPort) console.log('Reconnected serial port');
else console.log('Reconnect attempt did not open a serial port');
}).catch(e => console.warn('Reconnect failed', e && e.message));
}, 2000);
}
// kick off serial or simulator
tryStartSerialOrSimulator().then((port) => {
if (!port) return; // simulator/no data mode
// set fallback timer: if no data after 10s, start simulator
// fallbackTimer = setTimeout(() => {
// console.log('No data received from hardware after 10s, falling back to simulator');
// startSimulator();
// }, 10000);
port.on('open', () => {
console.log('Serial port opened');
io.emit('device-status', { connected: true, port: SERIAL_PORT });
// Send break signal like in original C code
port.set({ brk: true });
setTimeout(() => port.set({ brk: false }), 100);
// Delay before starting poll
setTimeout(() => {
if (useOnavi) {
// start polling here
pollTimer = setInterval(() => {
try {
port.write('*', (err) => {
if (err) console.error('poll write error:', err && err.message);
});
} catch (e) {
console.error('poll write exception:', e && e.message);
}
}, pollIntervalMs);
}
}, 5000); // 5s delay
});
// Decide whether to use ONAVI binary parsing: honor ONAVI env, or auto-enable
// for likely CDC device paths (usbmodem, ttyACM, usbserial).
const pathLower = (SERIAL_PORT || '').toLowerCase();
const useOnavi = ONAVI || /usbmodem|xrusbmodem|ttyacm|usbserial|cu.usbmodem/.test(pathLower);
const pollIntervalMs = useOnavi ? 20 : 0; // poll at ~50Hz if ONAVI
// ONavi devices use a binary 9-byte frame returned after writing '*'.
// If useOnavi is true, poll and parse raw bytes. Otherwise use the
// readline parser (text-based lines) as before.
if (useOnavi) {
console.log('ONAVI mode enabled: polling binary frames');
const ONAVI_FRAME_LEN = 9; // two start bytes, 6 data bytes, 1 checksum
let buf = Buffer.alloc(0);
let pollTimer = null;
const onData = (chunk) => {
console.log('Serial data chunk:', chunk.toString('hex'));
buf = Buffer.concat([buf, Buffer.from(chunk)]);
// try to parse frames
while (buf.length >= ONAVI_FRAME_LEN) {
// look for start markers at position 0
const b0 = buf[0];
const b1 = buf[1];
const validStart = (b0 === 0x2A && b1 === 0x2A) || (b0 === 0x23 && b1 === 0x23) || (b0 === 0x24 && b1 === 0x24);
if (!validStart) {
// drop first byte and retry
buf = buf.slice(1);
continue;
}
const frame = buf.slice(0, ONAVI_FRAME_LEN);
console.log('ONAVI raw frame:', frame.toString('hex'));
// extract values using same math as original C code
const x = frame[2] * 255 + frame[3];
const y = frame[4] * 255 + frame[5];
const z = frame[6] * 255 + frame[7];
const cs = frame[8];
// checksum: sum of bytes 0-7, lower byte
let cs_computed = 0;
for (let i = 0; i <= 7; i++) cs_computed += frame[i];
cs_computed &= 0xFF;
if (cs_computed !== cs) {
console.log('Checksum mismatch:', cs_computed, 'vs', cs, 'dropping frame');
buf = buf.slice(1);
continue;
}
// convert to g using FLOAT_ONAVI_FACTOR (from original C code)
// NOTE: original factor yields g units; avoid multiplying by EARTH_G so
// the server emits values in g (same units as the simulator).
const FLOAT_ONAVI_FACTOR = 7.629394531250e-05; // from C headers
const gx = ((x - 32768.0) * FLOAT_ONAVI_FACTOR);
const gy = ((y - 32768.0) * FLOAT_ONAVI_FACTOR);
const gz = ((z - 32768.0) * FLOAT_ONAVI_FACTOR);
emitSample(gx, gy, gz);
// remove consumed frame
buf = buf.slice(ONAVI_FRAME_LEN);
}
if (buf.length > 0) {
console.log('Leftover buffer after parsing:', buf.toString('hex'));
}
};
port.on('data', onData);
port.on('close', () => {
console.log('Serial port closed');
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
});
port.on('error', (e) => { /* handled below */ });
} else {
const { ReadlineParser } = require('@serialport/parser-readline');
const parser = port.pipe(new ReadlineParser({ delimiter: '\n' }));
parser.on('data', (line) => {
// accept lines like: "x,y,z" or JSON
line = line.trim();
if (!line) return;
try {
if (line.startsWith('{')) {
const obj = JSON.parse(line);
if (obj.x !== undefined && obj.y !== undefined && obj.z !== undefined) {
emitSample(obj.x, obj.y, obj.z);
}
} else {
const parts = line.split(/[ ,\t]+/).map(s => s.trim()).filter(Boolean);
if (parts.length >= 3) {
emitSample(parts[0], parts[1], parts[2]);
}
}
} catch (e) {
console.warn('Failed to parse line:', line);
}
});
}
port.on('error', (err) => {
console.error('Serial port error:', err.message);
// notify clients the device has an error
io.emit('device-status', { connected: false, error: err.message });
// schedule a reconnect attempt
scheduleReconnect(port);
});
}).catch(e => {
console.error('Failed to start serial or simulator:', e && e.message);
});
server.listen(PORT, () => {
console.log(`qcn-ui server listening on http://localhost:${PORT}`);
});
// --- Lightweight debug endpoints ---
// Return recent sample buffer for quick inspection
app.get('/debug/recent', (req, res) => {
try {
const limit = Math.min(50, recentSamples.length);
const recent = recentSamples.slice(-limit);
res.json({ count: recentSamples.length, recent });
} catch (e) {
res.status(500).json({ error: String(e) });
}
});
// Return simple server stats including connected client count
app.get('/debug/stats', (req, res) => {
try {
// socket.io v4 exposes the connected sockets in io.sockets.sockets
let connected = 0;
try { connected = io && io.sockets && io.sockets.sockets ? io.sockets.sockets.size || 0 : 0; } catch (e) { connected = 0; }
const ageMs = lastEmitTs ? (Date.now() - lastEmitTs) : null;
res.json({ connectedClients: connected, recentSamples: recentSamples.length, lastEmitTs: lastEmitTs || null, lastSampleAgeMs: ageMs });
} catch (e) {
res.status(500).json({ error: String(e) });
}
});
// emit a lightweight heartbeat so clients can detect stalls quickly
setInterval(() => {
try {
const connected = io && io.sockets && io.sockets.sockets ? io.sockets.sockets.size || 0 : 0;
const lastAge = lastEmitTs ? (Date.now() - lastEmitTs) : null;
io.emit('server-heartbeat', { t: Date.now(), connectedClients: connected, lastSampleAgeMs: lastAge });
} catch (e) {}
}, 1000);