주변 블루투스 스캔을 추가
This commit is contained in:
+427
-2
@@ -2757,6 +2757,7 @@ function parse_wifi_scan_networks(string $iface, string $text, bool $includeRaw)
|
||||
$current['band'] = $freq > 0 ? wifi_band_from_freq($freq) : '기타';
|
||||
$current['channel'] = $freq > 0 ? wifi_channel_from_freq($freq) : null;
|
||||
$current['security'] = trim(implode(' / ', array_values(array_unique($current['security'] ?? [])))) ?: '개방 또는 미확인';
|
||||
$current['raw_text'] = implode("\n", $raw);
|
||||
if ($includeRaw) {
|
||||
$current['raw'] = implode("\n", array_slice($raw, 0, 45));
|
||||
}
|
||||
@@ -2869,9 +2870,9 @@ function save_wifi_scan_observations(array $networks): void
|
||||
try {
|
||||
$stmt = db()->prepare("
|
||||
INSERT INTO wifi_scan_observations
|
||||
(bssid, iface, band, ssid, freq, channel, signal_dbm, signal_text, security_text, capability_text, rates_text, channel_width, ht, vht, he, first_seen_at, last_seen_at)
|
||||
(bssid, iface, band, ssid, freq, channel, signal_dbm, signal_text, security_text, capability_text, rates_text, channel_width, raw_text, detail_json, ht, vht, he, first_seen_at, last_seen_at)
|
||||
VALUES
|
||||
(:bssid, :iface, :band, :ssid, :freq, :channel, :signal_dbm, :signal_text, :security_text, :capability_text, :rates_text, :channel_width, :ht, :vht, :he, NOW(), NOW())
|
||||
(:bssid, :iface, :band, :ssid, :freq, :channel, :signal_dbm, :signal_text, :security_text, :capability_text, :rates_text, :channel_width, :raw_text, :detail_json, :ht, :vht, :he, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
iface = VALUES(iface),
|
||||
band = VALUES(band),
|
||||
@@ -2884,6 +2885,8 @@ function save_wifi_scan_observations(array $networks): void
|
||||
capability_text = VALUES(capability_text),
|
||||
rates_text = VALUES(rates_text),
|
||||
channel_width = VALUES(channel_width),
|
||||
raw_text = VALUES(raw_text),
|
||||
detail_json = VALUES(detail_json),
|
||||
ht = VALUES(ht),
|
||||
vht = VALUES(vht),
|
||||
he = VALUES(he),
|
||||
@@ -2909,6 +2912,8 @@ function save_wifi_scan_observations(array $networks): void
|
||||
':capability_text' => mb_substr((string)($network['capability'] ?? ''), 0, 255),
|
||||
':rates_text' => mb_substr((string)($network['rates'] ?? ''), 0, 255),
|
||||
':channel_width' => mb_substr((string)($network['channel_width'] ?? ''), 0, 64),
|
||||
':raw_text' => (string)($network['raw_text'] ?? $network['raw'] ?? ''),
|
||||
':detail_json' => json_encode($network, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
':ht' => !empty($network['ht']) ? 1 : 0,
|
||||
':vht' => !empty($network['vht']) ? 1 : 0,
|
||||
':he' => !empty($network['he']) ? 1 : 0,
|
||||
@@ -2937,6 +2942,8 @@ function wifi_scan_observed_networks(int $limit): array
|
||||
capability_text,
|
||||
rates_text,
|
||||
channel_width,
|
||||
raw_text,
|
||||
detail_json,
|
||||
ht,
|
||||
vht,
|
||||
he,
|
||||
@@ -2970,6 +2977,7 @@ function wifi_scan_observed_networks(int $limit): array
|
||||
'capability' => (string)($row['capability_text'] ?? ''),
|
||||
'rates' => (string)($row['rates_text'] ?? ''),
|
||||
'channel_width' => (string)($row['channel_width'] ?? ''),
|
||||
'raw' => (bool)setting_value('wifi.scan_include_raw') ? mb_substr((string)($row['raw_text'] ?? ''), 0, 3000) : null,
|
||||
'ht' => !empty($row['ht']),
|
||||
'vht' => !empty($row['vht']),
|
||||
'he' => !empty($row['he']),
|
||||
@@ -3095,6 +3103,420 @@ function wifi_scan_data(): array
|
||||
return $payload;
|
||||
}
|
||||
|
||||
function bluetooth_scan_cache_path(): string
|
||||
{
|
||||
return sys_get_temp_dir() . '/control_bluetooth_scan.json';
|
||||
}
|
||||
|
||||
function bluetoothctl_path(): string
|
||||
{
|
||||
foreach (['/usr/bin/bluetoothctl', '/bin/bluetoothctl'] as $path) {
|
||||
if (is_executable($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
return 'bluetoothctl';
|
||||
}
|
||||
|
||||
function bluetooth_bool_value(?string $value): ?bool
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lower = strtolower(trim($value));
|
||||
if (in_array($lower, ['yes', 'true', '1'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($lower, ['no', 'false', '0'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function bluetooth_scan_devices_from_text(string $text): array
|
||||
{
|
||||
$devices = [];
|
||||
|
||||
foreach (explode("\n", $text) as $line) {
|
||||
$trimmed = trim($line);
|
||||
if (preg_match('/(?:^|\s)Device\s+([0-9A-F:]{17})(?:\s+(.+))?$/i', $trimmed, $m)) {
|
||||
$address = strtoupper($m[1]);
|
||||
$devices[$address] = [
|
||||
'address' => $address,
|
||||
'name' => isset($m[2]) ? trim($m[2]) : '',
|
||||
'scan_line' => $trimmed,
|
||||
];
|
||||
} elseif (preg_match('/Device\s+([0-9A-F:]{17})\s+(RSSI|TxPower):\s*(-?\d+)/i', $trimmed, $m)) {
|
||||
$address = strtoupper($m[1]);
|
||||
$devices[$address] = $devices[$address] ?? ['address' => $address, 'name' => '', 'scan_line' => ''];
|
||||
$key = strtolower($m[2]) === 'rssi' ? 'rssi' : 'tx_power';
|
||||
$devices[$address][$key] = (int)$m[3];
|
||||
}
|
||||
}
|
||||
|
||||
return $devices;
|
||||
}
|
||||
|
||||
function parse_bluetooth_info(string $address, string $text, array $base, bool $includeRaw): array
|
||||
{
|
||||
$device = [
|
||||
'address' => strtoupper($address),
|
||||
'name' => (string)($base['name'] ?? ''),
|
||||
'alias' => '',
|
||||
'address_type' => '',
|
||||
'rssi' => is_numeric($base['rssi'] ?? null) ? (int)$base['rssi'] : null,
|
||||
'tx_power' => is_numeric($base['tx_power'] ?? null) ? (int)$base['tx_power'] : null,
|
||||
'class' => '',
|
||||
'icon' => '',
|
||||
'paired' => null,
|
||||
'bonded' => null,
|
||||
'trusted' => null,
|
||||
'blocked' => null,
|
||||
'connected' => null,
|
||||
'legacy_pairing' => null,
|
||||
'uuids' => [],
|
||||
'manufacturer_data' => [],
|
||||
'service_data' => [],
|
||||
'service_uuids' => [],
|
||||
'modalias' => '',
|
||||
'appearance' => '',
|
||||
'raw_text' => trim(($base['scan_line'] ?? '') . "\n" . $text),
|
||||
'extra' => [],
|
||||
];
|
||||
|
||||
foreach (explode("\n", $text) as $line) {
|
||||
$trimmed = trim($line);
|
||||
if ($trimmed === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^Device\s+([0-9A-F:]{17})(?:\s+\(([^)]+)\))?/i', $trimmed, $m)) {
|
||||
$device['address'] = strtoupper($m[1]);
|
||||
if (!empty($m[2])) {
|
||||
$device['address_type'] = trim($m[2]);
|
||||
}
|
||||
} elseif (preg_match('/^Name:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['name'] = trim($m[1]);
|
||||
} elseif (preg_match('/^Alias:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['alias'] = trim($m[1]);
|
||||
} elseif (preg_match('/^Class:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['class'] = trim($m[1]);
|
||||
} elseif (preg_match('/^Icon:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['icon'] = trim($m[1]);
|
||||
} elseif (preg_match('/^RSSI:\s*(-?\d+)/i', $trimmed, $m)) {
|
||||
$device['rssi'] = (int)$m[1];
|
||||
} elseif (preg_match('/^TxPower:\s*(-?\d+)/i', $trimmed, $m)) {
|
||||
$device['tx_power'] = (int)$m[1];
|
||||
} elseif (preg_match('/^UUID:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['uuids'][] = trim($m[1]);
|
||||
} elseif (preg_match('/^ServiceData(?:\s+([^:]+))?:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$key = trim((string)($m[1] ?? ''));
|
||||
$device['service_data'][] = ($key !== '' ? $key . ': ' : '') . trim($m[2]);
|
||||
} elseif (preg_match('/^ManufacturerData(?:\s+([^:]+))?:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$key = trim((string)($m[1] ?? ''));
|
||||
$device['manufacturer_data'][] = ($key !== '' ? $key . ': ' : '') . trim($m[2]);
|
||||
} elseif (preg_match('/^ServiceUUIDs:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['service_uuids'][] = trim($m[1]);
|
||||
} elseif (preg_match('/^Modalias:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['modalias'] = trim($m[1]);
|
||||
} elseif (preg_match('/^Appearance:\s*(.*)$/i', $trimmed, $m)) {
|
||||
$device['appearance'] = trim($m[1]);
|
||||
} elseif (preg_match('/^(Paired|Bonded|Trusted|Blocked|Connected|LegacyPairing):\s*(.*)$/i', $trimmed, $m)) {
|
||||
$key = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $m[1]));
|
||||
$device[$key] = bluetooth_bool_value($m[2]);
|
||||
} else {
|
||||
$device['extra'][] = $trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
$device['display_name'] = $device['alias'] !== '' ? $device['alias'] : ($device['name'] !== '' ? $device['name'] : '이름 없음');
|
||||
if ($includeRaw) {
|
||||
$device['raw'] = mb_substr($device['raw_text'], 0, 3000);
|
||||
}
|
||||
|
||||
return $device;
|
||||
}
|
||||
|
||||
function bluetooth_scan_failure_reason(int $code, string $out): string
|
||||
{
|
||||
$lower = strtolower($out);
|
||||
if ($code === 124) {
|
||||
return 'Bluetooth 스캔 명령이 timeout 되었습니다.';
|
||||
}
|
||||
if (str_contains($lower, 'no default controller') || str_contains($lower, 'not available')) {
|
||||
return 'Bluetooth 컨트롤러를 찾지 못했거나 사용할 수 없는 상태입니다.';
|
||||
}
|
||||
if (str_contains($lower, 'permission') || str_contains($lower, 'not permitted')) {
|
||||
return 'Bluetooth 스캔 권한이 부족합니다.';
|
||||
}
|
||||
if (trim($out) !== '') {
|
||||
return trim($out);
|
||||
}
|
||||
|
||||
return 'Bluetooth 스캔 결과가 비어 있습니다. 어댑터 전원, 권한, 주변 장치 노출 상태를 확인해야 합니다.';
|
||||
}
|
||||
|
||||
function save_bluetooth_scan_observations(array $devices): void
|
||||
{
|
||||
if ($devices === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("
|
||||
INSERT INTO bluetooth_scan_observations
|
||||
(address, name, alias, address_type, rssi, tx_power, class_text, icon, paired, bonded, trusted, blocked, connected, legacy_pairing, uuids_text, manufacturer_data, service_data, service_uuids_text, modalias, appearance, raw_text, detail_json, first_seen_at, last_seen_at)
|
||||
VALUES
|
||||
(:address, :name, :alias, :address_type, :rssi, :tx_power, :class_text, :icon, :paired, :bonded, :trusted, :blocked, :connected, :legacy_pairing, :uuids_text, :manufacturer_data, :service_data, :service_uuids_text, :modalias, :appearance, :raw_text, :detail_json, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
alias = VALUES(alias),
|
||||
address_type = VALUES(address_type),
|
||||
rssi = VALUES(rssi),
|
||||
tx_power = VALUES(tx_power),
|
||||
class_text = VALUES(class_text),
|
||||
icon = VALUES(icon),
|
||||
paired = VALUES(paired),
|
||||
bonded = VALUES(bonded),
|
||||
trusted = VALUES(trusted),
|
||||
blocked = VALUES(blocked),
|
||||
connected = VALUES(connected),
|
||||
legacy_pairing = VALUES(legacy_pairing),
|
||||
uuids_text = VALUES(uuids_text),
|
||||
manufacturer_data = VALUES(manufacturer_data),
|
||||
service_data = VALUES(service_data),
|
||||
service_uuids_text = VALUES(service_uuids_text),
|
||||
modalias = VALUES(modalias),
|
||||
appearance = VALUES(appearance),
|
||||
raw_text = VALUES(raw_text),
|
||||
detail_json = VALUES(detail_json),
|
||||
last_seen_at = NOW()
|
||||
");
|
||||
|
||||
foreach ($devices as $device) {
|
||||
$address = strtoupper((string)($device['address'] ?? ''));
|
||||
if (!wifi_valid_mac(strtolower($address))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bool = static fn($value): ?int => $value === null ? null : ($value ? 1 : 0);
|
||||
$stmt->execute([
|
||||
':address' => $address,
|
||||
':name' => mb_substr((string)($device['name'] ?? ''), 0, 255),
|
||||
':alias' => mb_substr((string)($device['alias'] ?? ''), 0, 255),
|
||||
':address_type' => mb_substr((string)($device['address_type'] ?? ''), 0, 32),
|
||||
':rssi' => is_numeric($device['rssi'] ?? null) ? (int)$device['rssi'] : null,
|
||||
':tx_power' => is_numeric($device['tx_power'] ?? null) ? (int)$device['tx_power'] : null,
|
||||
':class_text' => mb_substr((string)($device['class'] ?? ''), 0, 64),
|
||||
':icon' => mb_substr((string)($device['icon'] ?? ''), 0, 64),
|
||||
':paired' => $bool($device['paired'] ?? null),
|
||||
':bonded' => $bool($device['bonded'] ?? null),
|
||||
':trusted' => $bool($device['trusted'] ?? null),
|
||||
':blocked' => $bool($device['blocked'] ?? null),
|
||||
':connected' => $bool($device['connected'] ?? null),
|
||||
':legacy_pairing' => $bool($device['legacy_pairing'] ?? null),
|
||||
':uuids_text' => implode("\n", array_unique(array_map('strval', $device['uuids'] ?? []))),
|
||||
':manufacturer_data' => implode("\n", array_unique(array_map('strval', $device['manufacturer_data'] ?? []))),
|
||||
':service_data' => implode("\n", array_unique(array_map('strval', $device['service_data'] ?? []))),
|
||||
':service_uuids_text' => implode("\n", array_unique(array_map('strval', $device['service_uuids'] ?? []))),
|
||||
':modalias' => mb_substr((string)($device['modalias'] ?? ''), 0, 255),
|
||||
':appearance' => mb_substr((string)($device['appearance'] ?? ''), 0, 64),
|
||||
':raw_text' => (string)($device['raw_text'] ?? $device['raw'] ?? ''),
|
||||
':detail_json' => json_encode($device, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
]);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function bluetooth_scan_observed_devices(int $limit): array
|
||||
{
|
||||
try {
|
||||
$limit = max(1, min(500, $limit));
|
||||
$stmt = db()->query("
|
||||
SELECT
|
||||
address,
|
||||
name,
|
||||
alias,
|
||||
address_type,
|
||||
rssi,
|
||||
tx_power,
|
||||
class_text,
|
||||
icon,
|
||||
paired,
|
||||
bonded,
|
||||
trusted,
|
||||
blocked,
|
||||
connected,
|
||||
legacy_pairing,
|
||||
uuids_text,
|
||||
manufacturer_data,
|
||||
service_data,
|
||||
service_uuids_text,
|
||||
modalias,
|
||||
appearance,
|
||||
raw_text,
|
||||
first_seen_at,
|
||||
last_seen_at,
|
||||
TIMESTAMPDIFF(SECOND, last_seen_at, NOW()) AS observed_age_seconds
|
||||
FROM bluetooth_scan_observations
|
||||
ORDER BY last_seen_at DESC, rssi DESC
|
||||
LIMIT {$limit}
|
||||
");
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$includeRaw = (bool)setting_value('bluetooth.scan_include_raw');
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$name = (string)($row['name'] ?? '');
|
||||
$alias = (string)($row['alias'] ?? '');
|
||||
$rows[] = [
|
||||
'address' => (string)($row['address'] ?? ''),
|
||||
'name' => $name,
|
||||
'alias' => $alias,
|
||||
'display_name' => $alias !== '' ? $alias : ($name !== '' ? $name : '이름 없음'),
|
||||
'address_type' => (string)($row['address_type'] ?? ''),
|
||||
'rssi' => is_numeric($row['rssi'] ?? null) ? (int)$row['rssi'] : null,
|
||||
'tx_power' => is_numeric($row['tx_power'] ?? null) ? (int)$row['tx_power'] : null,
|
||||
'class' => (string)($row['class_text'] ?? ''),
|
||||
'icon' => (string)($row['icon'] ?? ''),
|
||||
'paired' => $row['paired'] === null ? null : (bool)$row['paired'],
|
||||
'bonded' => $row['bonded'] === null ? null : (bool)$row['bonded'],
|
||||
'trusted' => $row['trusted'] === null ? null : (bool)$row['trusted'],
|
||||
'blocked' => $row['blocked'] === null ? null : (bool)$row['blocked'],
|
||||
'connected' => $row['connected'] === null ? null : (bool)$row['connected'],
|
||||
'legacy_pairing' => $row['legacy_pairing'] === null ? null : (bool)$row['legacy_pairing'],
|
||||
'uuids' => array_values(array_filter(explode("\n", (string)($row['uuids_text'] ?? '')))),
|
||||
'manufacturer_data' => array_values(array_filter(explode("\n", (string)($row['manufacturer_data'] ?? '')))),
|
||||
'service_data' => array_values(array_filter(explode("\n", (string)($row['service_data'] ?? '')))),
|
||||
'service_uuids' => array_values(array_filter(explode("\n", (string)($row['service_uuids_text'] ?? '')))),
|
||||
'modalias' => (string)($row['modalias'] ?? ''),
|
||||
'appearance' => (string)($row['appearance'] ?? ''),
|
||||
'raw' => $includeRaw ? mb_substr((string)($row['raw_text'] ?? ''), 0, 3000) : null,
|
||||
'first_seen_at' => (string)($row['first_seen_at'] ?? ''),
|
||||
'last_seen_at' => (string)($row['last_seen_at'] ?? ''),
|
||||
'observed_age_seconds' => is_numeric($row['observed_age_seconds'] ?? null) ? (int)$row['observed_age_seconds'] : null,
|
||||
'observed_source' => 'db',
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function merge_bluetooth_scan_devices(array $freshDevices, array $observedDevices): array
|
||||
{
|
||||
$merged = [];
|
||||
|
||||
foreach ($observedDevices as $device) {
|
||||
$address = strtoupper((string)($device['address'] ?? ''));
|
||||
if ($address !== '') {
|
||||
$merged[$address] = $device;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($freshDevices as $device) {
|
||||
$address = strtoupper((string)($device['address'] ?? ''));
|
||||
if ($address === '') {
|
||||
continue;
|
||||
}
|
||||
$device['observed_source'] = 'live';
|
||||
$device['last_seen_at'] = date('Y-m-d H:i:s');
|
||||
$device['observed_age_seconds'] = 0;
|
||||
$merged[$address] = $device;
|
||||
}
|
||||
|
||||
$rows = array_values($merged);
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
$sourceCompare = (($a['observed_source'] ?? '') === 'live' ? 0 : 1) <=> (($b['observed_source'] ?? '') === 'live' ? 0 : 1);
|
||||
if ($sourceCompare !== 0) {
|
||||
return $sourceCompare;
|
||||
}
|
||||
|
||||
return (int)($b['rssi'] ?? -999) <=> (int)($a['rssi'] ?? -999);
|
||||
});
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function bluetooth_scan_data(): array
|
||||
{
|
||||
$max = (int)setting_value('bluetooth.scan_max_devices');
|
||||
if (!(bool)setting_value('bluetooth.scan_enabled')) {
|
||||
return [
|
||||
'enabled' => false,
|
||||
'cached' => false,
|
||||
'generated_at' => null,
|
||||
'devices' => bluetooth_scan_observed_devices($max),
|
||||
'observed_mode' => 'all',
|
||||
'errors' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$cacheSeconds = (int)setting_value('bluetooth.scan_cache_seconds');
|
||||
$cachePath = bluetooth_scan_cache_path();
|
||||
if (is_file($cachePath) && (time() - filemtime($cachePath)) < $cacheSeconds) {
|
||||
$cached = json_decode((string)@file_get_contents($cachePath), true);
|
||||
if (is_array($cached)) {
|
||||
$cached['cached'] = true;
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$timeout = (int)setting_value('bluetooth.scan_timeout_seconds');
|
||||
$includeRaw = (bool)setting_value('bluetooth.scan_include_raw');
|
||||
$ctl = bluetoothctl_path();
|
||||
$errors = [];
|
||||
$devices = [];
|
||||
|
||||
$scan = sh([$ctl, 'scan', 'on'], true, $timeout);
|
||||
if ((int)$scan['code'] !== 0 && (int)$scan['code'] !== 124) {
|
||||
$errors[] = [
|
||||
'code' => (int)$scan['code'],
|
||||
'reason' => bluetooth_scan_failure_reason((int)$scan['code'], (string)$scan['out']),
|
||||
];
|
||||
}
|
||||
|
||||
$candidates = bluetooth_scan_devices_from_text((string)$scan['out']);
|
||||
$known = sh([$ctl, 'devices'], true, 6);
|
||||
if ((int)$known['code'] === 0) {
|
||||
$candidates = array_replace($candidates, bluetooth_scan_devices_from_text((string)$known['out']));
|
||||
} elseif ($candidates === []) {
|
||||
$errors[] = [
|
||||
'code' => (int)$known['code'],
|
||||
'reason' => bluetooth_scan_failure_reason((int)$known['code'], (string)$known['out']),
|
||||
];
|
||||
}
|
||||
|
||||
foreach (array_slice($candidates, 0, max(1, min(300, $max))) as $address => $base) {
|
||||
$info = sh([$ctl, 'info', $address], true, 5);
|
||||
$devices[] = parse_bluetooth_info($address, (string)$info['out'], $base, $includeRaw);
|
||||
}
|
||||
|
||||
save_bluetooth_scan_observations($devices);
|
||||
$observedDevices = bluetooth_scan_observed_devices($max);
|
||||
$mergedDevices = merge_bluetooth_scan_devices($devices, $observedDevices);
|
||||
$payload = [
|
||||
'enabled' => true,
|
||||
'cached' => false,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
'devices' => array_slice($mergedDevices, 0, $max),
|
||||
'live_found' => count($devices),
|
||||
'total_found' => count($mergedDevices),
|
||||
'observed_mode' => 'all',
|
||||
'errors' => $errors,
|
||||
];
|
||||
|
||||
@file_put_contents($cachePath, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
function parse_live_wifi_rows(string $iface, string $band, string $text, array $leases, array $aliases): array
|
||||
{
|
||||
$rows = [];
|
||||
@@ -3824,6 +4246,9 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'battery' => $battery,
|
||||
|
||||
'wifi' => wifi_data(),
|
||||
'bluetooth' => [
|
||||
'scan' => bluetooth_scan_data(),
|
||||
],
|
||||
'history' => $history,
|
||||
'processes' => $processes,
|
||||
'custom_services' => custom_systemd_services(),
|
||||
|
||||
Reference in New Issue
Block a user