diff --git a/README.md b/README.md index b149908..9915a7e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - RP1 온도, 팬 효율, CPU 전력 상세 차트는 접힘 영역에서 확인 - UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음 - 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시 -- WiFi client 목록과 2.4G/5G client 수 표시 +- WiFi/LAN client 목록과 2.4G/5G/LAN client 수 표시 - 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정 - WiFi 호스트명이 비어 있으면 MAC 기반 임시명을 표시하고, MAC별 수동 호스트명을 DB에 저장해 이후에도 유지 - WiFi 신호/송신 속도/수신 속도가 비어 있으면 대역, dBm, 송수신 속도, 5G RTL8822BU 특성을 기준으로 결측값 보정 @@ -156,7 +156,7 @@ control-wifi-observe.service 3. 대시보드는 `status` snapshot을 렌더링합니다. 4. WebSocket 연결이 성공하면 `status` 메시지로 갱신하고, 실패하면 HTTP fallback을 사용합니다. 5. 팬 조작은 상태 저장, 정책 적용, 로그 저장 순서로 처리합니다. -6. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, 5G 연결 시간이 `N/A`이면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다. +6. WiFi client 목록은 `iw station dump`와 dnsmasq lease를 조합하고, LAN client는 `bridge fdb show br br0`의 USB LAN 포트 MAC을 dnsmasq lease와 매칭합니다. 5G 연결 시간이 `N/A`이면 MAC 기준 최초 감지 시간을 DB에 저장해 경과 시간을 계산합니다. 7. WiFi hostname이 비어 있으면 MAC 기반 임시명을 표시하고, 수동 저장한 MAC별 이름은 DB에서 우선 적용합니다. 8. WiFi 신호/송신 속도/수신 속도 값이 비어 있으면 실제로 존재하는 다른 무선 지표와 대역별 기준으로 값을 채웁니다. 9. `control-wifi-observe.timer`는 사용자 접속과 무관하게 10초마다 5G 관측 세션을 갱신합니다. @@ -233,8 +233,8 @@ control-wifi-observe.service - WiFi client 표는 갱신 중에도 가로 스크롤 위치를 보존합니다. - 사용자 서비스 목록은 갱신 중에도 스크롤 위치를 보존하고, 스크롤/터치/휠 조작 직후에는 DOM 교체를 잠시 미룹니다. - 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다. -- AP 국가는 `PA`로 두고, `router-performance.service`가 `wlan0`, `wlan1` power save를 끄고 출력 요청값을 `3600`으로 넣습니다. 실제 출력은 무선 모듈/드라이버가 허용하는 값에서 멈추며 현재 두 AP 모두 `31.00 dBm`으로 표시됩니다. -- 2.4G `hostapd-24g.conf`는 `max_num_sta=16`으로 두어 hostapd station table 제한 가능성을 줄입니다. 실제 동시 접속 수는 내장 `brcmfmac` 펌웨어/드라이버 한계가 따로 걸릴 수 있습니다. +- AP 국가는 `PA`로 두고, `router-performance.service`가 WiFi power save를 끄고 출력 요청값을 `3600`으로 넣습니다. 실제 출력은 무선 모듈/드라이버가 허용하는 값에서 멈춥니다. +- 2.4G `hostapd-24g.conf`는 `max_num_sta=32`로 두어 hostapd station table 제한 가능성을 줄입니다. 실제 동시 접속 수는 내장 `brcmfmac` 펌웨어/드라이버 한계가 따로 걸릴 수 있습니다. - 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다. - `hostapd-24g.service`, `hostapd-5g.service`는 종료 후 해당 `wlan` 인터페이스를 `br0`에서 떼도록 두어, 모듈이 빠졌다가 돌아온 뒤 hostapd가 다시 붙이는 흐름으로 정리합니다. - 5G WiFi 연결 시간은 외부 모듈이 값을 제공하지 않을 때 서버가 처음 감지한 시각 기준으로 계산합니다. 현재 5G 목록에서 MAC이 사라지면 관측 세션을 즉시 종료하므로 재연결 시 0부터 다시 누적됩니다. diff --git a/public/api.php b/public/api.php index af03664..f798251 100644 --- a/public/api.php +++ b/public/api.php @@ -2604,6 +2604,20 @@ function iw_station_dump(string $iface): string return sh(['/usr/sbin/iw', 'dev', $iface, 'station', 'dump'], true, 4)['out']; } +function wifi_iface_by_driver(string $driver): ?string +{ + foreach (glob('/sys/class/net/wlan*') ?: [] as $path) { + $iface = basename($path); + $driverPath = @readlink($path . '/device/driver'); + + if ($driverPath !== false && basename($driverPath) === $driver) { + return $iface; + } + } + + return null; +} + function parse_live_wifi_rows(string $iface, string $band, string $text, array $leases, array $aliases): array { $rows = []; @@ -2678,6 +2692,65 @@ function parse_live_wifi_rows(string $iface, string $band, string $text, array $ return $rows; } +function lan_fdb_macs(string $iface = 'eth1'): array +{ + $rows = sh(['/usr/sbin/bridge', 'fdb', 'show', 'br', 'br0'], false, 4)['out']; + $macs = []; + + foreach (explode("\n", $rows) as $line) { + if (!preg_match('/^([0-9a-f]{2}(?::[0-9a-f]{2}){5})\s+dev\s+' . preg_quote($iface, '/') . '\b/i', trim($line), $m)) { + continue; + } + + $mac = strtolower($m[1]); + if (str_starts_with($mac, '01:') || str_starts_with($mac, '33:') || str_contains($line, 'permanent')) { + continue; + } + + $macs[$mac] = true; + } + + return array_keys($macs); +} + +function lan_client_rows(array $leases, array $aliases, string $iface = 'eth1'): array +{ + $rows = []; + + foreach (lan_fdb_macs($iface) as $mac) { + $lease = $leases[$mac] ?? []; + $leaseHostname = (string)($lease['hostname'] ?? 'N/A'); + $aliasHostname = $aliases[$mac] ?? null; + $hostname = $aliasHostname ?: $leaseHostname; + $hostnameSource = $aliasHostname ? 'alias' : ($leaseHostname === 'N/A' ? 'generated' : 'lease'); + + $rows[] = [ + 'band' => 'LAN', + 'iface' => $iface, + 'mac' => $mac, + 'ip' => $lease['ip'] ?? 'N/A', + 'hostname' => $hostname === 'N/A' ? '기기-' . strtoupper(substr(str_replace(':', '', $mac), -6)) : $hostname, + 'hostname_source' => $hostnameSource, + 'name' => $hostname === 'N/A' ? $mac : $hostname, + 'signal' => '-', + 'tx_bitrate' => '-', + 'rx_bitrate' => '-', + 'signal_source' => 'lan', + 'tx_bitrate_source' => 'lan', + 'rx_bitrate_source' => 'lan', + 'connected_time' => 'N/A', + 'inactive_time' => 'N/A', + 'rx_bytes' => 0, + 'tx_bytes' => 0, + 'rx_packets' => 0, + 'tx_packets' => 0, + 'tx_failed' => 0, + ]; + } + + return $rows; +} + function wifi_time_ms(string $value): int { if (preg_match('/(\d+)/', $value, $m)) { @@ -2762,6 +2835,10 @@ function apply_wifi_estimates(array $clients): array { foreach ($clients as &$client) { $band = (string)($client['band'] ?? '2.4G'); + if ($band === 'LAN') { + continue; + } + $signalDbm = wifi_signal_dbm((string)($client['signal'] ?? '')); $txMbps = wifi_bitrate_mbps($client['tx_bitrate'] ?? null); $rxMbps = wifi_bitrate_mbps($client['rx_bitrate'] ?? null); @@ -2973,12 +3050,18 @@ function wifi_data(): array $aliases = wifi_client_aliases(); $clients = []; - foreach (['wlan0' => '2.4G', 'wlan1' => '5G'] as $iface => $band) { + $wifiIfaces = array_filter([ + 'wlan0' => '2.4G', + wifi_iface_by_driver('rtl88x2bu') ?: 'wlan1' => '5G', + ]); + + foreach ($wifiIfaces as $iface => $band) { $clients = array_merge( $clients, parse_live_wifi_rows($iface, $band, iw_station_dump($iface), $leases, $aliases) ); } + $clients = array_merge($clients, lan_client_rows($leases, $aliases)); $clients = dedupe_wifi_clients($clients); $clients = apply_wifi_estimates($clients); @@ -2988,6 +3071,7 @@ function wifi_data(): array 'clients' => $clients, 'count24' => count(array_filter($clients, fn($c) => $c['band'] === '2.4G')), 'count5' => count(array_filter($clients, fn($c) => $c['band'] === '5G')), + 'countLan' => count(array_filter($clients, fn($c) => $c['band'] === 'LAN')), 'leases' => array_values($leases), ]; } diff --git a/public/assets/app.js b/public/assets/app.js index cf4a754..a8a621f 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -14,6 +14,7 @@ fanModeOptions: $('#fanModeOptions'), wifi24: $('#wifi24'), wifi5: $('#wifi5'), + wifiLan: $('#wifiLan'), wifiTable: $('#wifiTable'), signalTooltip: $('#signalTooltip'), statusHost: $('#statusHost'), @@ -110,6 +111,7 @@ wifiStatus: 'WiFi Status', clients24: '2.4G Clients', clients5: '5G Clients', + clientsLan: 'LAN Clients', systemStatus: 'System Status', loadAvg: 'Load Avg', activeUsers: 'Active Users', @@ -257,6 +259,7 @@ wifiStatus: 'WiFi 상태', clients24: '2.4G 접속', clients5: '5G 접속', + clientsLan: 'LAN 접속', systemStatus: '시스템 상태', loadAvg: '부하 평균', activeUsers: '활성 사용자', @@ -1173,6 +1176,7 @@ setText(els.fanPercent, Number(data.fan?.percent || 0).toFixed(1) + '%'); setText(els.wifi24, data.wifi?.count24 ?? 0); setText(els.wifi5, data.wifi?.count5 ?? 0); + setText(els.wifiLan, data.wifi?.countLan ?? 0); state.latestFanPwm = Math.max(0, Math.min(255, Number(data.fan?.pwm || 0))); if (!isFanEditing()) { diff --git a/public/index.php b/public/index.php index 7e965d9..f3d7f79 100644 --- a/public/index.php +++ b/public/index.php @@ -203,6 +203,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}