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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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() };
|
||||
}
|
||||
Reference in New Issue
Block a user