Add observed 5G WiFi connection time
This commit is contained in:
@@ -15,6 +15,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- CPU/RP1 온도, 팬 RPM, 팬 효율, CPU 전력, 배터리 상태 차트
|
- CPU/RP1 온도, 팬 RPM, 팬 효율, CPU 전력, 배터리 상태 차트
|
||||||
- WiFi client 목록과 2.4G/5G client 수 표시
|
- WiFi client 목록과 2.4G/5G client 수 표시
|
||||||
- WiFi client 상태 표시
|
- WiFi client 상태 표시
|
||||||
|
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
|
||||||
- System Notice와 process CPU/MEM 후보 표시
|
- System Notice와 process CPU/MEM 후보 표시
|
||||||
- dmesg 로그 열람
|
- dmesg 로그 열람
|
||||||
- Web Push 구독 등록, 자동 복구, 구독 기기 목록 표시
|
- Web Push 구독 등록, 자동 복구, 구독 기기 목록 표시
|
||||||
@@ -58,6 +59,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- `sensor_logs`: 온도, RPM, PWM, 배터리, load, memory, disk, uptime
|
- `sensor_logs`: 온도, RPM, PWM, 배터리, load, memory, disk, uptime
|
||||||
- `system_notice_state`: notice 활성 상태와 기준값
|
- `system_notice_state`: notice 활성 상태와 기준값
|
||||||
- `system_notice_logs`: notice 발생 이력
|
- `system_notice_logs`: notice 발생 이력
|
||||||
|
- `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간
|
||||||
- `push_subscriptions`: Web Push 구독 정보
|
- `push_subscriptions`: Web Push 구독 정보
|
||||||
- `push_event_logs`: Push 발송, 수신, 표시, 클릭, 실패 이벤트
|
- `push_event_logs`: Push 발송, 수신, 표시, 클릭, 실패 이벤트
|
||||||
- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, VAPID key
|
- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, VAPID key
|
||||||
@@ -119,18 +121,20 @@ control-push-healthcheck.service
|
|||||||
2. 대시보드는 `status` snapshot을 렌더링합니다.
|
2. 대시보드는 `status` snapshot을 렌더링합니다.
|
||||||
3. WebSocket 연결이 성공하면 `status` 메시지로 갱신하고, 실패하면 HTTP fallback을 사용합니다.
|
3. WebSocket 연결이 성공하면 `status` 메시지로 갱신하고, 실패하면 HTTP fallback을 사용합니다.
|
||||||
4. 팬 조작은 상태 저장, 정책 적용, 로그 저장 순서로 처리합니다.
|
4. 팬 조작은 상태 저장, 정책 적용, 로그 저장 순서로 처리합니다.
|
||||||
5. Push 등록은 권한 요청과 구독 저장이 끝나면 즉시 상태를 갱신합니다.
|
5. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, 5G 연결 시간이 `N/A`이면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다.
|
||||||
6. WakeLock 버튼은 활성 상태를 초록색 버튼으로 표시합니다.
|
6. Push 등록은 권한 요청과 구독 저장이 끝나면 즉시 상태를 갱신합니다.
|
||||||
7. Reboot 버튼은 `재부팅` 단어 입력과 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
|
7. WakeLock 버튼은 활성 상태를 초록색 버튼으로 표시합니다.
|
||||||
8. Translate 버튼은 최초 접속 시 디바이스 언어를 기본값으로 사용하고, 변경값은 `localStorage`의 `controlLang`에 저장합니다.
|
8. Reboot 버튼은 `재부팅` 단어 입력과 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
|
||||||
9. Theme 버튼은 기본값 `dark`로 시작하고, 변경값은 `localStorage`의 `controlTheme`에 저장합니다.
|
9. Translate 버튼은 최초 접속 시 디바이스 언어를 기본값으로 사용하고, 변경값은 `localStorage`의 `controlLang`에 저장합니다.
|
||||||
10. Push 발송, 수신, 표시, 클릭 이벤트를 기록하고 구독 DB의 건강 상태를 갱신합니다.
|
10. Theme 버튼은 기본값 `dark`로 시작하고, 변경값은 `localStorage`의 `controlTheme`에 저장합니다.
|
||||||
|
11. Push 발송, 수신, 표시, 클릭 이벤트를 기록하고 구독 DB의 건강 상태를 갱신합니다.
|
||||||
|
|
||||||
## 주요 함수/모듈
|
## 주요 함수/모듈
|
||||||
|
|
||||||
- `collect_snapshot()`: 센서와 fan 상태 snapshot 생성
|
- `collect_snapshot()`: 센서와 fan 상태 snapshot 생성
|
||||||
- `apply_fan_policy()`: 팬 목표값 계산과 적용
|
- `apply_fan_policy()`: 팬 목표값 계산과 적용
|
||||||
- `json_out()`: API JSON 응답 표준화
|
- `json_out()`: API JSON 응답 표준화
|
||||||
|
- `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용
|
||||||
- `push_subscribe.js`: Push 구독 등록과 복구
|
- `push_subscribe.js`: Push 구독 등록과 복구
|
||||||
- `push_health_summary()`: 상태별 Push 기기 수 집계
|
- `push_health_summary()`: 상태별 Push 기기 수 집계
|
||||||
- `send_push_healthcheck_if_due()`: 쿨다운 기반 헬스체크 알림 발송
|
- `send_push_healthcheck_if_due()`: 쿨다운 기반 헬스체크 알림 발송
|
||||||
@@ -150,6 +154,7 @@ control-push-healthcheck.service
|
|||||||
|
|
||||||
- 센서 수집 주기와 DB 증가량을 확인합니다.
|
- 센서 수집 주기와 DB 증가량을 확인합니다.
|
||||||
- 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다.
|
- 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다.
|
||||||
|
- 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산되므로, 장치 재시작이나 DB 초기화 후에는 다시 0부터 누적됩니다.
|
||||||
- WebSocket 장기 실행 중 DB 연결이 끊길 수 있으므로 reconnect 로그를 확인합니다.
|
- WebSocket 장기 실행 중 DB 연결이 끊길 수 있으므로 reconnect 로그를 확인합니다.
|
||||||
- Push 구독 자동 복구가 과도한 반복 등록을 만들지 않는지 확인합니다.
|
- Push 구독 자동 복구가 과도한 반복 등록을 만들지 않는지 확인합니다.
|
||||||
- Push 헬스체크 timer 상태는 `systemctl list-timers --all control-push-healthcheck.timer`로 확인합니다.
|
- Push 헬스체크 timer 상태는 `systemctl list-timers --all control-push-healthcheck.timer`로 확인합니다.
|
||||||
|
|||||||
@@ -228,6 +228,22 @@ function bootstrap_db(): void
|
|||||||
COLLATE=utf8mb4_unicode_ci
|
COLLATE=utf8mb4_unicode_ci
|
||||||
");
|
");
|
||||||
|
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS wifi_observed_sessions (
|
||||||
|
mac VARCHAR(17) NOT NULL PRIMARY KEY,
|
||||||
|
band VARCHAR(16) NOT NULL,
|
||||||
|
iface VARCHAR(32) NOT NULL,
|
||||||
|
first_seen_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
last_seen_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
last_ip VARCHAR(64) NULL,
|
||||||
|
hostname VARCHAR(255) NULL,
|
||||||
|
INDEX idx_last_seen_at (last_seen_at),
|
||||||
|
INDEX idx_band (band)
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
DEFAULT CHARSET=utf8mb4
|
||||||
|
COLLATE=utf8mb4_unicode_ci
|
||||||
|
");
|
||||||
|
|
||||||
$pdo->exec("
|
$pdo->exec("
|
||||||
CREATE TABLE IF NOT EXISTS remember_tokens (
|
CREATE TABLE IF NOT EXISTS remember_tokens (
|
||||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|||||||
+123
@@ -1458,6 +1458,128 @@ function dedupe_wifi_clients(array $clients): array
|
|||||||
return array_values($deduped);
|
return array_values($deduped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wifi_connected_time_missing(mixed $value): bool
|
||||||
|
{
|
||||||
|
$text = strtoupper(trim((string)$value));
|
||||||
|
return $text === '' || $text === 'N/A' || $text === '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function observed_wifi_duration_seconds(array $clients): array
|
||||||
|
{
|
||||||
|
$targets = [];
|
||||||
|
|
||||||
|
foreach ($clients as $client) {
|
||||||
|
$band = (string)($client['band'] ?? '');
|
||||||
|
$mac = strtolower((string)($client['mac'] ?? ''));
|
||||||
|
|
||||||
|
if ($band !== '5G'
|
||||||
|
|| !wifi_connected_time_missing($client['connected_time'] ?? null)
|
||||||
|
|| !preg_match('/^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/', $mac)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targets[$mac] = [
|
||||||
|
'mac' => $mac,
|
||||||
|
'band' => $band,
|
||||||
|
'iface' => (string)($client['iface'] ?? ''),
|
||||||
|
'ip' => (string)($client['ip'] ?? ''),
|
||||||
|
'hostname' => (string)($client['hostname'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($targets === []) {
|
||||||
|
try {
|
||||||
|
db()->exec("DELETE FROM wifi_observed_sessions WHERE last_seen_at < DATE_SUB(NOW(3), INTERVAL 2 MINUTE)");
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$upsert = $pdo->prepare("
|
||||||
|
INSERT INTO wifi_observed_sessions
|
||||||
|
(mac, band, iface, first_seen_at, last_seen_at, last_ip, hostname)
|
||||||
|
VALUES
|
||||||
|
(:mac, :band, :iface, NOW(3), NOW(3), :last_ip, :hostname)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
band = VALUES(band),
|
||||||
|
iface = VALUES(iface),
|
||||||
|
last_seen_at = NOW(3),
|
||||||
|
last_ip = VALUES(last_ip),
|
||||||
|
hostname = VALUES(hostname)
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($targets as $target) {
|
||||||
|
$upsert->execute([
|
||||||
|
':mac' => $target['mac'],
|
||||||
|
':band' => $target['band'],
|
||||||
|
':iface' => $target['iface'],
|
||||||
|
':last_ip' => $target['ip'] === 'N/A' ? null : $target['ip'],
|
||||||
|
':hostname' => $target['hostname'] === 'N/A' ? null : $target['hostname'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->exec("DELETE FROM wifi_observed_sessions WHERE last_seen_at < DATE_SUB(NOW(3), INTERVAL 2 MINUTE)");
|
||||||
|
|
||||||
|
$placeholders = implode(',', array_fill(0, count($targets), '?'));
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
mac,
|
||||||
|
GREATEST(0, TIMESTAMPDIFF(SECOND, first_seen_at, NOW(3))) AS observed_seconds
|
||||||
|
FROM wifi_observed_sessions
|
||||||
|
WHERE mac IN ($placeholders)
|
||||||
|
");
|
||||||
|
$stmt->execute(array_keys($targets));
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$seconds = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$seconds[strtolower((string)$row['mac'])] = (int)$row['observed_seconds'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $seconds;
|
||||||
|
} catch (Throwable) {
|
||||||
|
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply_observed_wifi_connected_time(array $clients): array
|
||||||
|
{
|
||||||
|
$observedSeconds = observed_wifi_duration_seconds($clients);
|
||||||
|
|
||||||
|
if ($observedSeconds === []) {
|
||||||
|
return $clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($clients as &$client) {
|
||||||
|
$mac = strtolower((string)($client['mac'] ?? ''));
|
||||||
|
|
||||||
|
if (($client['band'] ?? '') !== '5G'
|
||||||
|
|| !wifi_connected_time_missing($client['connected_time'] ?? null)
|
||||||
|
|| !array_key_exists($mac, $observedSeconds)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client['connected_time'] = (string)$observedSeconds[$mac];
|
||||||
|
$client['connected_time_source'] = 'observed';
|
||||||
|
}
|
||||||
|
unset($client);
|
||||||
|
|
||||||
|
return $clients;
|
||||||
|
}
|
||||||
|
|
||||||
function wifi_data(): array
|
function wifi_data(): array
|
||||||
{
|
{
|
||||||
$leases = dnsmasq_leases();
|
$leases = dnsmasq_leases();
|
||||||
@@ -1471,6 +1593,7 @@ function wifi_data(): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
$clients = dedupe_wifi_clients($clients);
|
$clients = dedupe_wifi_clients($clients);
|
||||||
|
$clients = apply_observed_wifi_connected_time($clients);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'clients' => $clients,
|
'clients' => $clients,
|
||||||
|
|||||||
Reference in New Issue
Block a user