Files
blelock/generated/bleMonitor.ts
T
2026-04-30 00:57:24 +02:00

223 lines
7.1 KiB
TypeScript

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import { classifySignal, SignalClass, ThresholdConfig } from './thresholds.js';
const BLUEZ_SERVICE = 'org.bluez';
const ADAPTER_PATH = '/org/bluez/hci0';
const ADAPTER_IFACE = 'org.bluez.Adapter1';
const DEVICE_IFACE = 'org.bluez.Device1';
const OBJECT_MANAGER_IFACE = 'org.freedesktop.DBus.ObjectManager';
function addressToPath(address: string): string {
return `${ADAPTER_PATH}/dev_${address.replace(/:/g, '_')}`;
}
export class BleMonitor extends GObject.Object {
static {
GObject.registerClass({
GTypeName: 'BleUnlockMonitor',
Signals: {
'rssi-changed': { param_types: [GObject.TYPE_INT] },
'devices-changed': { param_types: [] },
},
}, this);
}
private _deviceAddress: string = '';
private _config: ThresholdConfig = { rssiLockThreshold: -75, rssiUnlockThreshold: -65 };
private _pollIntervalSec: number = 3;
private _timerId: number | null = null;
private _deviceProxy: Gio.DBusProxy | null = null;
private _adapterProxy: Gio.DBusProxy | null = null;
private _lastClass: SignalClass | null = null;
private _knownDevices: Map<string, string> = new Map();
/**
* Start monitoring the given device address.
*/
startMonitoring(deviceAddress: string, config: ThresholdConfig, pollIntervalSec: number): void {
this._deviceAddress = deviceAddress;
this._config = config;
this._pollIntervalSec = pollIntervalSec;
const bus = Gio.DBus.system;
try {
this._adapterProxy = Gio.DBusProxy.new_sync(
bus,
Gio.DBusProxyFlags.NONE,
null,
BLUEZ_SERVICE,
ADAPTER_PATH,
ADAPTER_IFACE,
null,
);
} catch (e) {
console.error(`[BleUnlock] Failed to create adapter proxy: ${e}`);
return;
}
try {
this._deviceProxy = Gio.DBusProxy.new_sync(
bus,
Gio.DBusProxyFlags.NONE,
null,
BLUEZ_SERVICE,
addressToPath(deviceAddress),
DEVICE_IFACE,
null,
);
} catch (e) {
console.warn(`[BleUnlock] Failed to create device proxy for ${deviceAddress}: ${e}`);
}
this._startDiscovery();
this._refreshDevices();
this._timerId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT,
this._pollIntervalSec,
() => this._poll(),
);
}
/**
* Stop monitoring and clean up D-Bus resources.
*/
stopMonitoring(): void {
if (this._timerId !== null) {
GLib.Source.remove(this._timerId);
this._timerId = null;
}
this._stopDiscovery();
this._deviceProxy = null;
this._adapterProxy = null;
this._lastClass = null;
}
/** Returns the map of known BT devices (address → name). */
get devices(): Map<string, string> {
return this._knownDevices;
}
/** Called when RSSI drops below lock threshold. Wired by extension.ts. */
onSignalBelowThreshold(): void {}
/** Called when RSSI rises above unlock threshold. Wired by extension.ts. */
onSignalAboveThreshold(): void {}
private _startDiscovery(): void {
if (!this._adapterProxy) return;
try {
this._adapterProxy.call(
'StartDiscovery',
null,
Gio.DBusCallFlags.NONE,
-1,
null,
null,
);
} catch (e) {
console.warn(`[BleUnlock] StartDiscovery failed: ${e}`);
}
}
private _stopDiscovery(): void {
if (!this._adapterProxy) return;
try {
this._adapterProxy.call(
'StopDiscovery',
null,
Gio.DBusCallFlags.NONE,
-1,
null,
null,
);
} catch (e) {
console.warn(`[BleUnlock] StopDiscovery failed: ${e}`);
}
}
private _refreshDevices(): void {
const bus = Gio.DBus.system;
try {
const result = bus.call_sync(
BLUEZ_SERVICE,
'/',
OBJECT_MANAGER_IFACE,
'GetManagedObjects',
null,
null,
Gio.DBusCallFlags.NONE,
-1,
null,
);
if (!result) return;
const objects = result.get_child_value(0);
const nObjects = objects.n_children();
this._knownDevices.clear();
for (let i = 0; i < nObjects; i++) {
const entry = objects.get_child_value(i);
const ifaces = entry.get_child_value(1);
const nIfaces = ifaces.n_children();
for (let j = 0; j < nIfaces; j++) {
const ifaceEntry = ifaces.get_child_value(j);
const ifaceName = ifaceEntry.get_child_value(0).get_string()[0];
if (ifaceName !== DEVICE_IFACE) continue;
const props = ifaceEntry.get_child_value(1);
const nProps = props.n_children();
let address: string | null = null;
let name: string = '';
for (let k = 0; k < nProps; k++) {
const prop = props.get_child_value(k);
const propName = prop.get_child_value(0).get_string()[0];
const propValue = prop.get_child_value(1).get_child_value(0);
if (propName === 'Address') address = propValue.get_string()[0];
if (propName === 'Name') name = propValue.get_string()[0];
}
if (address) this._knownDevices.set(address, name);
}
}
this.emit('devices-changed');
} catch (e) {
console.warn(`[BleUnlock] GetManagedObjects failed: ${e}`);
}
}
private _poll(): boolean {
let rssi: number | null = null;
if (this._deviceProxy) {
const variant = this._deviceProxy.get_cached_property('RSSI');
if (variant) {
rssi = variant.get_int16();
}
}
const currentClass = classifySignal(rssi, this._config);
this.emit('rssi-changed', rssi ?? 0);
if (this._lastClass !== currentClass) {
if (currentClass === 'absent' && this._lastClass !== null) {
console.debug(`[BleUnlock] Signal below threshold (RSSI=${rssi}), locking`);
this.onSignalBelowThreshold();
} else if (currentClass === 'strong' && this._lastClass !== 'strong') {
console.debug(`[BleUnlock] Signal above threshold (RSSI=${rssi}), unlocking`);
this.onSignalAboveThreshold();
}
this._lastClass = currentClass;
}
return GLib.SOURCE_CONTINUE;
}
}