Add throttling status tracking

This commit is contained in:
seo
2026-06-18 04:13:13 +09:00
parent 5bf4236784
commit 421c44127f
4 changed files with 85 additions and 32 deletions
+5 -3
View File
@@ -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에 서버 관측 경과 시간 적용
+38 -19
View File
@@ -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));
+38 -9
View File
@@ -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 || '-');
+4 -1
View File
@@ -203,6 +203,9 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
<div class="status-row"><div class="status-key" data-i18n="lowVoltage">Low Voltage</div><div class="status-value" id="statusLowVoltage">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="lowVoltageLast">Last Low Voltage</div><div class="status-value" id="statusLowVoltageLast">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="lowVoltageDuration">LV Duration</div><div class="status-value" id="statusLowVoltageDuration">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="throttling">Throttling</div><div class="status-value" id="statusThrottling">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="throttlingLast">Last Throttling</div><div class="status-value" id="statusThrottlingLast">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="throttlingDuration">Throttle Duration</div><div class="status-value" id="statusThrottlingDuration">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="batteryV">Battery V</div><div class="status-value" id="statusBatteryVoltage">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="batterySoc">Battery SOC</div><div class="status-value" id="statusBatterySoc">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="remaining">Remaining</div><div class="status-value" id="statusBatteryRemaining">-</div></div>
@@ -319,7 +322,7 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
</div>
</div>
</div>
<script src="/assets/app.js?v=20260617_reboot_enter1"></script>
<script src="/assets/app.js?v=20260618_throttling1"></script>
<script src="/assets/wakelock.js?v=20260607_prefs1"></script>
<script src="/push_subscribe.js?v=20260607_prefs1"></script>
<?php endif; ?>