Files
Claude f9e1eb82c4 feat: komplette Codebase vom Board ins Repo gespiegelt
app/ = /opt/callrec (Node-Backend, Vite-Frontend, Python-Inferenz,
Audio-Skripte, rkllm_server.cpp), system/ = systemd-Units,
WirePlumber/PipeWire-Drop-ins, sudoers. Modelle, Binaries, Aufnahmen
und node_modules bleiben draussen (.gitignore).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:18:29 +02:00

84 lines
3.3 KiB
JavaScript

import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const pexec = promisify(execFile);
// Run bluetoothctl non-interactively. Returns combined stdout even on nonzero exit.
async function btctl(args, timeoutSec = 15) {
try {
const { stdout } = await pexec('bluetoothctl', args, { timeout: timeoutSec * 1000, maxBuffer: 1 << 20 });
return stdout;
} catch (e) {
return (e.stdout || '') + (e.stderr || '');
}
}
function parseDevices(out) {
const res = [];
for (const line of out.split('\n')) {
const m = line.match(/Device\s+([0-9A-F:]{17})\s+(.*)/i);
if (m) res.push({ mac: m[1].toUpperCase(), name: m[2].trim() });
}
return res;
}
// Scan for `seconds`, return all discovered devices.
export async function scan(seconds = 10) {
await btctl(['--timeout', '2', 'power', 'on'], 5);
await btctl(['--timeout', String(seconds), 'scan', 'on'], seconds + 6);
return enrich(parseDevices(await btctl(['devices'])));
}
// Known devices with paired/connected status.
export async function knownDevices() {
return enrich(parseDevices(await btctl(['devices'])));
}
async function enrich(devices) {
const pairedMacs = new Set(parseDevices(await btctl(['paired-devices'])).map(d => d.mac));
for (const d of devices) {
d.paired = pairedMacs.has(d.mac);
const info = await btctl(['info', d.mac]);
d.connected = /Connected:\s*yes/i.test(info);
d.audio = /Icon:\s*audio|UUID:.*(Headset|Handsfree|Audio Sink)/i.test(info);
}
return devices;
}
export async function pair(mac) {
const r1 = await btctl(['--timeout', '25', 'pair', mac], 30);
const r2 = await btctl(['trust', mac], 5);
const r3 = await btctl(['--timeout', '15', 'connect', mac], 20);
const info = await btctl(['info', mac]);
return { pair: r1.trim(), trust: r2.trim(), connect: r3.trim(), connected: /Connected:\s*yes/i.test(info) };
}
// Full re-pair. realme (and many cheap earbuds) DISCARD their stored bond when
// put back in pairing mode, so the board's kept bond goes stale → plain connect
// or pair fails (AuthenticationFailed). Recipe: drop stale bond, rediscover
// (scan must be its own step), then pair+trust+connect fresh. All via
// bluetoothctl, so the callrec user can run it through the polkit rule.
export async function repair(mac) {
await btctl(['disconnect', mac], 10);
await btctl(['remove', mac], 10);
await btctl(['--timeout', '2', 'power', 'on'], 5);
await btctl(['--timeout', '12', 'scan', 'on'], 18); // rediscover after removal
const found = parseDevices(await btctl(['devices'])).some(d => d.mac === mac.toUpperCase());
if (!found) return { ok: false, step: 'scan', msg: 'Gerät nicht gefunden — im Pairing-Modus?' };
const r1 = await btctl(['--timeout', '25', 'pair', mac], 30);
await btctl(['trust', mac], 5);
const r3 = await btctl(['--timeout', '15', 'connect', mac], 20);
const info = await btctl(['info', mac]);
const connected = /Connected:\s*yes/i.test(info);
return { ok: connected, pair: r1.trim(), connect: r3.trim(), connected };
}
export async function connect(mac) {
const out = await btctl(['--timeout', '15', 'connect', mac], 20);
const info = await btctl(['info', mac]);
return { out: out.trim(), connected: /Connected:\s*yes/i.test(info) };
}
export async function disconnect(mac) {
return { out: (await btctl(['disconnect', mac], 10)).trim() };
}