mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
slop
This commit is contained in:
@@ -16,6 +16,7 @@ function createBalanceBoardHardware({ logger, address = '', simulate = false } =
|
||||
let worker = null;
|
||||
let stdoutBuffer = '';
|
||||
let stopped = false;
|
||||
let restarting = false;
|
||||
let restartTimer = null;
|
||||
let lastStderrLogAt = 0;
|
||||
let suppressedStderrLines = 0;
|
||||
@@ -100,7 +101,11 @@ function createBalanceBoardHardware({ logger, address = '', simulate = false } =
|
||||
child.on('close', (code, signal) => {
|
||||
if (worker === child) worker = null;
|
||||
if (!stopped) {
|
||||
emitProtocolError(`balance board worker exited (${signal || code})`);
|
||||
// Admin unpair deliberately replaces the worker with an empty address.
|
||||
// Do not turn that expected exit into a red hardware-error state while
|
||||
// still using the normal restart scheduler for the replacement.
|
||||
if (!restarting) emitProtocolError(`balance board worker exited (${signal || code})`);
|
||||
restarting = false;
|
||||
scheduleRestart();
|
||||
}
|
||||
});
|
||||
@@ -108,6 +113,7 @@ function createBalanceBoardHardware({ logger, address = '', simulate = false } =
|
||||
|
||||
function stop() {
|
||||
stopped = true;
|
||||
restarting = false;
|
||||
if (restartTimer) {
|
||||
clearTimeout(restartTimer);
|
||||
restartTimer = null;
|
||||
@@ -129,10 +135,36 @@ function createBalanceBoardHardware({ logger, address = '', simulate = false } =
|
||||
}, 1500).unref();
|
||||
}
|
||||
|
||||
function restart() {
|
||||
if (stopped) return;
|
||||
if (!worker) {
|
||||
start();
|
||||
return;
|
||||
}
|
||||
|
||||
const child = worker;
|
||||
restarting = true;
|
||||
try {
|
||||
// An admin forget changes the address used in the child environment. A
|
||||
// controlled restart lets the replacement worker start with that new
|
||||
// value, while the existing close handler remains the single owner of
|
||||
// delayed respawn and avoids overlapping Bluetooth listeners.
|
||||
child.stdin.write(`${JSON.stringify({ command: 'stop' })}\n`);
|
||||
} catch (_err) {
|
||||
// The child may have already closed stdin; SIGTERM below still guarantees
|
||||
// that it cannot keep listening for the address that was just forgotten.
|
||||
}
|
||||
child.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (child.exitCode == null && child.signalCode == null) child.kill('SIGKILL');
|
||||
}, 1500).unref();
|
||||
}
|
||||
|
||||
return {
|
||||
events,
|
||||
start,
|
||||
stop,
|
||||
restart,
|
||||
setAddress(nextAddress) {
|
||||
// The factory can be created before first commissioning. Preserve the
|
||||
// newly paired address for later bridge restarts in the same Node process
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
// Purpose: Exposes one Wii Balance Board as a self-pairing Bluetooth scale.
|
||||
// Scope: Stores the paired address, applies a small automatic tare, and publishes simple status plus live weight.
|
||||
const fs = require('fs');
|
||||
const { execFile } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const EventEmitter = require('events');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('balanceBoardService');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { resolveDataDir, resolveDataPath } = require('../../helpers/dataPaths');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
const { isAdmin } = require('../roleService');
|
||||
const { sendAlert } = require('../alertService');
|
||||
const { createBalanceBoardHardware } = require('./hardware');
|
||||
|
||||
const events = new EventEmitter();
|
||||
@@ -18,6 +22,14 @@ const STORE_PATH = resolveDataPath('balance-board.json');
|
||||
const FRAME_ROOM = 'balance-board-viewers';
|
||||
const TARE_SAMPLE_COUNT = 20;
|
||||
const MAX_AUTOMATIC_TARE_KG = 2;
|
||||
const execFileAsync = promisify(execFile);
|
||||
const ALERT_COLORS = {
|
||||
connected: '#10b981',
|
||||
sleeping: '#64748b',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
info: '#38bdf8',
|
||||
};
|
||||
|
||||
function loadStore() {
|
||||
try {
|
||||
@@ -49,6 +61,18 @@ function rawWeightKg(corners = {}) {
|
||||
.reduce((total, key) => total + Math.max(0, Number(corners[key]) || 0), 0) / 100;
|
||||
}
|
||||
|
||||
function cornerWeightsKg(corners = {}) {
|
||||
// Preserve all four factory-calibrated load cells in kilograms. Their
|
||||
// relative distribution drives the center-of-pressure dot in the panel,
|
||||
// while the existing tare continues to apply only to the displayed total.
|
||||
return {
|
||||
topRight: roundedWeight((Number(corners.topRight) || 0) / 100),
|
||||
bottomRight: roundedWeight((Number(corners.bottomRight) || 0) / 100),
|
||||
topLeft: roundedWeight((Number(corners.topLeft) || 0) / 100),
|
||||
bottomLeft: roundedWeight((Number(corners.bottomLeft) || 0) / 100),
|
||||
};
|
||||
}
|
||||
|
||||
let store = enabled ? loadStore() : { address: '' };
|
||||
let hardware = null;
|
||||
let status = enabled ? (store.address ? 'waiting' : 'starting') : 'disabled';
|
||||
@@ -60,6 +84,55 @@ let batteryPercent = null;
|
||||
let latestFrame = null;
|
||||
let tareSamples = [];
|
||||
let tareKg = 0;
|
||||
let previousWorkerState = '';
|
||||
let lastAlertKey = '';
|
||||
let unpairing = false;
|
||||
|
||||
function sendStatusAlert(workerState, message = '') {
|
||||
let alert = null;
|
||||
if (workerState === 'connected') {
|
||||
alert = {
|
||||
color: ALERT_COLORS.connected,
|
||||
title: 'Balance Board connected',
|
||||
message: 'Live weight is available.',
|
||||
};
|
||||
} else if (workerState === 'sleeping') {
|
||||
alert = {
|
||||
color: ALERT_COLORS.sleeping,
|
||||
title: 'Balance Board sleeping',
|
||||
message: 'Press the front power button when you are ready to use it.',
|
||||
};
|
||||
} else if (workerState === 'waiting' && previousWorkerState === 'connected') {
|
||||
alert = {
|
||||
color: ALERT_COLORS.warning,
|
||||
title: 'Balance Board disconnected',
|
||||
message: message || 'Press the front power button to reconnect it.',
|
||||
};
|
||||
} else if (workerState === 'connection-failed') {
|
||||
alert = {
|
||||
color: ALERT_COLORS.error,
|
||||
title: 'Balance Board connection failed',
|
||||
message: message || 'The Bluetooth connection did not complete.',
|
||||
};
|
||||
} else if (workerState === 'error') {
|
||||
alert = {
|
||||
color: ALERT_COLORS.error,
|
||||
title: 'Balance Board needs attention',
|
||||
message: message || 'The hardware worker stopped.',
|
||||
};
|
||||
}
|
||||
|
||||
previousWorkerState = workerState;
|
||||
if (!alert) return;
|
||||
|
||||
// Native reconnect retries may repeat the same result while hardware is out
|
||||
// of range. Collapse identical status/message pairs so feed alerts remain
|
||||
// meaningful state changes instead of turning into a transport log.
|
||||
const key = `${workerState}:${alert.message}`;
|
||||
if (key === lastAlertKey) return;
|
||||
lastAlertKey = key;
|
||||
sendAlert(alert);
|
||||
}
|
||||
|
||||
function getState() {
|
||||
return {
|
||||
@@ -103,6 +176,7 @@ function processFrame(message = {}) {
|
||||
);
|
||||
latestFrame = {
|
||||
totalKg: roundedWeight(rawKg - tareKg),
|
||||
corners: cornerWeightsKg(message.corners),
|
||||
batteryPercent,
|
||||
};
|
||||
io.to(FRAME_ROOM).emit('balanceBoard:frame', latestFrame);
|
||||
@@ -121,12 +195,19 @@ function handleWorkerMessage(message = {}) {
|
||||
persistStore();
|
||||
}
|
||||
hardware?.setAddress(address);
|
||||
lastAlertKey = 'paired';
|
||||
sendAlert({
|
||||
color: ALERT_COLORS.info,
|
||||
title: 'Balance Board paired',
|
||||
message: 'Bluetooth setup completed.',
|
||||
});
|
||||
updateStatus('connecting', 'Paired. Connecting to the board now.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type !== 'status') return;
|
||||
const workerState = String(message.state || 'unknown');
|
||||
sendStatusAlert(workerState, message.error || '');
|
||||
if (workerState === 'commissioning') {
|
||||
updateStatus('starting', 'Starting Bluetooth discovery.');
|
||||
} else if (workerState === 'discovering') {
|
||||
@@ -169,6 +250,59 @@ io.on('connection', (socket) => {
|
||||
cb({ success: true });
|
||||
});
|
||||
socket.on('balanceBoard:unsubscribe', () => socket.leave(FRAME_ROOM));
|
||||
socket.on('balanceBoard:unpair', async (_payload = {}, cb = () => {}) => {
|
||||
if (!isAdmin(socket)) {
|
||||
cb({ error: 'Admin access required' });
|
||||
return;
|
||||
}
|
||||
if (unpairing) {
|
||||
cb({ error: 'The Balance Board is already being unpaired' });
|
||||
return;
|
||||
}
|
||||
|
||||
unpairing = true;
|
||||
const address = store.address;
|
||||
let bluetoothWarning = '';
|
||||
try {
|
||||
if (address) {
|
||||
try {
|
||||
// A complete forget removes both sources of remembered identity. If
|
||||
// only the JSON address or only the BlueZ bond were removed, the next
|
||||
// red-Sync attempt could inherit half of the previous relationship.
|
||||
await execFileAsync('bluetoothctl', ['remove', address], { timeout: 10000 });
|
||||
} catch (err) {
|
||||
bluetoothWarning = String(
|
||||
err?.stderr || err?.message || 'BlueZ did not remove the bond',
|
||||
).trim();
|
||||
logger.warn('Balance Board BlueZ bond removal failed', bluetoothWarning);
|
||||
}
|
||||
}
|
||||
|
||||
store.address = '';
|
||||
persistStore();
|
||||
connected = false;
|
||||
batteryPercent = null;
|
||||
latestFrame = null;
|
||||
tareSamples = [];
|
||||
tareKg = 0;
|
||||
previousWorkerState = '';
|
||||
lastAlertKey = 'unpaired';
|
||||
hardware?.setAddress('');
|
||||
hardware?.restart();
|
||||
updateStatus('starting', 'Starting Bluetooth discovery.');
|
||||
sendAlert({
|
||||
color: ALERT_COLORS.warning,
|
||||
title: 'Balance Board unpaired',
|
||||
message: 'Press the red Sync button underneath the board to pair it again.',
|
||||
});
|
||||
cb({ success: true, warning: bluetoothWarning || null });
|
||||
} catch (err) {
|
||||
logger.error('Failed to unpair Balance Board', err);
|
||||
cb({ error: err.message || 'Failed to unpair the Balance Board' });
|
||||
} finally {
|
||||
unpairing = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (enabled) {
|
||||
|
||||
Reference in New Issue
Block a user