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>
84 lines
3.3 KiB
JavaScript
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() };
|
|
}
|