diff --git a/README.md b/README.md index b04381d..9343169 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - PWM slider 기반 수동 팬 제어 - CPU/RP1 온도, 팬 RPM, 팬 효율, CPU 전력, 배터리 상태 차트 - 배터리 잔여 시간은 1S8P 병렬팩 `2200mAh x 8 = 17600mAh`, nominal `3.7V`, 총 `65.12Wh` 기준으로 계산 -- 라즈베리파이 저전압 현재 상태, 최근 감지 시각, 지속시간 표시 +- 라즈베리파이 저전압/스로틀링 현재 상태, 최근 감지 시각, 지속시간 표시 - WiFi client 목록과 2.4G/5G client 수 표시 - WiFi client 상태 표시 - 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정 @@ -63,6 +63,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - `control_state`: 팬 모드와 PWM 상태 - `sensor_logs`: 온도, RPM, PWM, 배터리, load, memory, disk, uptime - `/tmp/control-low-voltage-state.json`: `vcgencmd get_throttled` 기반 저전압 감지 상태와 최근 지속시간 +- `/tmp/control-throttling-state.json`: `vcgencmd get_throttled` 기반 스로틀링 감지 상태와 최근 지속시간 - `system_notice_state`: notice 활성 상태와 기준값 - `system_notice_logs`: notice 발생 이력 - `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간 @@ -154,13 +155,14 @@ control-wifi-observe.service 9. Reboot 버튼은 `재부팅` 단어 입력과 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다. 확인 단어 입력창에서 Enter를 눌러도 같은 Reboot 흐름이 다시 열리지 않도록 키 이벤트 기본 동작과 중복 실행을 차단합니다. 10. Translate 버튼은 최초 접속 시 디바이스 언어를 기본값으로 사용하고, 변경값은 `localStorage`의 `controlLang`에 저장합니다. 라벨은 `Translate` 또는 `번역`으로만 표시합니다. 11. Theme 버튼은 기본값 `dark`로 시작하고, 변경값은 `localStorage`의 `controlTheme`에 저장합니다. 라벨은 `Theme` 또는 `테마`로만 표시합니다. -12. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다. +12. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압/스로틀링 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다. 13. Push 발송, 수신, 표시, 클릭 이벤트를 기록하고 구독 DB의 건강 상태를 갱신합니다. ## 주요 함수/모듈 - `collect_snapshot()`: 센서와 fan 상태 snapshot 생성 -- `low_voltage_status()`: 라즈베리파이 throttled flag를 읽고 저전압 감지 이력을 상태 파일로 추적 +- `throttled_statuses()`: 라즈베리파이 throttled flag를 읽고 저전압/스로틀링 감지 이력을 상태 파일로 추적 +- `low_voltage_status()`: 기존 호출 호환을 위한 저전압 상태 wrapper - `apply_fan_policy()`: 팬 목표값 계산과 적용 - `json_out()`: API JSON 응답 표준화 - `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용 diff --git a/public/api.php b/public/api.php index 61096b5..95ee532 100644 --- a/public/api.php +++ b/public/api.php @@ -117,23 +117,12 @@ function dmesg_log(): array ]; } -function low_voltage_status(): array +function throttled_event_status(?int $flags, string $raw, int $activeBit, int $seenBit, string $statePath): array { - $result = sh(['/usr/bin/vcgencmd', 'get_throttled'], false, 3); - $raw = trim((string)$result['out']); - $flags = null; - - if ((int)$result['code'] === 0 && preg_match('/throttled=0x([0-9a-f]+)/i', $raw, $m)) { - $flags = hexdec($m[1]); - } - $now = time(); - $active = $flags !== null && (bool)($flags & 0x1); - $seen = $flags !== null && (bool)($flags & 0x10000); - $freqCapped = $flags !== null && (bool)($flags & 0x2); - $throttled = $flags !== null && (bool)($flags & 0x4); - $softTempLimit = $flags !== null && (bool)($flags & 0x8); - $statePath = '/tmp/control-low-voltage-state.json'; + $active = $flags !== null && (bool)($flags & $activeBit); + $seen = $flags !== null && (bool)($flags & $seenBit); + if (is_file($statePath) && !is_writable($statePath)) { @chmod($statePath, 0666); } @@ -206,9 +195,6 @@ function low_voltage_status(): array 'flags' => $flags, 'active' => $active, 'seen_since_boot' => $seen, - 'freq_capped' => $freqCapped, - 'throttled' => $throttled, - 'soft_temp_limit' => $softTempLimit, 'active_since' => $dateValue($state['active_since'] ?? null), 'last_detected_at' => $dateValue($state['last_detected_at'] ?? null), 'last_cleared_at' => $dateValue($state['last_cleared_at'] ?? null), @@ -218,6 +204,37 @@ function low_voltage_status(): array ]; } +function throttled_statuses(): array +{ + $result = sh(['/usr/bin/vcgencmd', 'get_throttled'], false, 3); + $raw = trim((string)$result['out']); + $flags = null; + + if ((int)$result['code'] === 0 && preg_match('/throttled=0x([0-9a-f]+)/i', $raw, $m)) { + $flags = hexdec($m[1]); + } + + $lowVoltage = throttled_event_status($flags, $raw, 0x1, 0x10000, '/tmp/control-low-voltage-state.json'); + $throttling = throttled_event_status($flags, $raw, 0x4, 0x40000, '/tmp/control-throttling-state.json'); + + $lowVoltage['freq_capped'] = $flags !== null && (bool)($flags & 0x2); + $lowVoltage['throttled'] = $flags !== null && (bool)($flags & 0x4); + $lowVoltage['soft_temp_limit'] = $flags !== null && (bool)($flags & 0x8); + $throttling['low_voltage'] = $flags !== null && (bool)($flags & 0x1); + $throttling['freq_capped'] = $flags !== null && (bool)($flags & 0x2); + $throttling['soft_temp_limit'] = $flags !== null && (bool)($flags & 0x8); + + return [ + 'low_voltage' => $lowVoltage, + 'throttling' => $throttling, + ]; +} + +function low_voltage_status(): array +{ + return throttled_statuses()['low_voltage']; +} + function fan_paths(): array { $candidates = glob('/sys/devices/platform/cooling_fan/hwmon/hwmon*/pwm1') ?: []; @@ -1792,6 +1809,7 @@ function collect_snapshot(bool $applyFan = true): array $mem = mem_info(); $disk = disk_info('/'); $os = os_info(); + $throttledStatuses = throttled_statuses(); $cpuPower = $applyFan ? cpu_power_status() : [ 'voltage' => isset($latest['cpu_voltage']) ? (float)$latest['cpu_voltage'] : null, 'watts' => isset($latest['cpu_watts']) ? (float)$latest['cpu_watts'] : null, @@ -1849,7 +1867,8 @@ function collect_snapshot(bool $applyFan = true): array 'active_users' => active_user_info(), 'memory' => $mem, 'disk' => $disk, - 'low_voltage' => low_voltage_status(), + 'low_voltage' => $throttledStatuses['low_voltage'], + 'throttling' => $throttledStatuses['throttling'], ]); $history = add_battery_remaining_history(sensor_history(240)); diff --git a/public/assets/app.js b/public/assets/app.js index 9d21e75..b2c5691 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -24,6 +24,9 @@ statusLowVoltage: $('#statusLowVoltage'), statusLowVoltageLast: $('#statusLowVoltageLast'), statusLowVoltageDuration: $('#statusLowVoltageDuration'), + statusThrottling: $('#statusThrottling'), + statusThrottlingLast: $('#statusThrottlingLast'), + statusThrottlingDuration: $('#statusThrottlingDuration'), statusBatteryVoltage: $('#statusBatteryVoltage'), statusBatterySoc: $('#statusBatterySoc'), statusBatteryRemaining: $('#statusBatteryRemaining'), @@ -99,6 +102,13 @@ lowVoltageNormal: 'Normal', lowVoltageSeen: 'seen since boot', lowVoltageNotSeen: 'not seen since boot', + throttling: 'Throttling', + throttlingLast: 'Last Throttling', + throttlingDuration: 'Throttle Duration', + throttlingActive: 'Throttling', + throttlingNormal: 'Normal', + throttlingSeen: 'seen since boot', + throttlingNotSeen: 'not seen since boot', currentEpisode: 'current', lastEpisode: 'last', batteryV: 'Battery V', @@ -241,6 +251,13 @@ lowVoltageNormal: '정상', lowVoltageSeen: '부팅 후 이력 있음', lowVoltageNotSeen: '부팅 후 이력 없음', + throttling: '스로틀링', + throttlingLast: '최근 스로틀링', + throttlingDuration: '스로틀링 지속', + throttlingActive: '감지 중', + throttlingNormal: '정상', + throttlingSeen: '부팅 후 이력 있음', + throttlingNotSeen: '부팅 후 이력 없음', currentEpisode: '현재', lastEpisode: '최근', batteryV: '배터리 전압', @@ -787,27 +804,35 @@ return formatDhms(parseWifiDurationSeconds(value)) || value; } - function lowVoltageStateText(lowVoltage) { - if (!lowVoltage || lowVoltage.available === false) { + function eventStateText(eventState, activeKey, normalKey, seenKey, notSeenKey) { + if (!eventState || eventState.available === false) { return t('na'); } - const stateText = lowVoltage.active ? t('lowVoltageActive') : t('lowVoltageNormal'); - const seenText = lowVoltage.seen_since_boot ? t('lowVoltageSeen') : t('lowVoltageNotSeen'); + const stateText = eventState.active ? t(activeKey) : t(normalKey); + const seenText = eventState.seen_since_boot ? t(seenKey) : t(notSeenKey); return `${stateText} (${seenText})`; } - function lowVoltageDurationText(lowVoltage) { - if (!lowVoltage || lowVoltage.available === false) { + function eventDurationText(eventState) { + if (!eventState || eventState.available === false) { return t('na'); } - const seconds = Number(lowVoltage.duration_seconds ?? 0); - const label = lowVoltage.active ? t('currentEpisode') : t('lastEpisode'); + const seconds = Number(eventState.duration_seconds ?? 0); + const label = eventState.active ? t('currentEpisode') : t('lastEpisode'); const formatted = formatDhms(seconds) || '-'; return `${label}: ${formatted}`; } + function lowVoltageStateText(lowVoltage) { + return eventStateText(lowVoltage, 'lowVoltageActive', 'lowVoltageNormal', 'lowVoltageSeen', 'lowVoltageNotSeen'); + } + + function throttlingStateText(throttling) { + return eventStateText(throttling, 'throttlingActive', 'throttlingNormal', 'throttlingSeen', 'throttlingNotSeen'); + } + function timeAgo(seconds) { const formatted = formatDhms(seconds); return formatted ? `${formatted} ${t('ago')}` : '-'; @@ -839,6 +864,7 @@ const memory = system.memory || {}; const battery = data.battery || {}; const lowVoltage = system.low_voltage || {}; + const throttling = system.throttling || {}; const load = Array.isArray(system.load) ? system.load : []; const activeUsers = system.active_users || {}; @@ -850,7 +876,10 @@ setText(els.statusUptime, system.uptime || '-'); setText(els.statusLowVoltage, lowVoltageStateText(lowVoltage)); setText(els.statusLowVoltageLast, lowVoltage.last_detected_at || '-'); - setText(els.statusLowVoltageDuration, lowVoltageDurationText(lowVoltage)); + setText(els.statusLowVoltageDuration, eventDurationText(lowVoltage)); + setText(els.statusThrottling, throttlingStateText(throttling)); + setText(els.statusThrottlingLast, throttling.last_detected_at || '-'); + setText(els.statusThrottlingDuration, eventDurationText(throttling)); setText(els.statusBatteryVoltage, battery.voltage === null || battery.voltage === undefined ? '-' : `${Number(battery.voltage).toFixed(3)} V`); setText(els.statusBatterySoc, battery.percent === null || battery.percent === undefined ? '-' : `${Number(battery.percent).toFixed(2)}%`); setText(els.statusBatteryRemaining, battery.remaining?.display || '-'); diff --git a/public/index.php b/public/index.php index 816fb17..d5aa34b 100644 --- a/public/index.php +++ b/public/index.php @@ -203,6 +203,9 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da