Add low voltage status to control dashboard
This commit is contained in:
@@ -13,6 +13,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
- 팬 모드 `auto`, `manual`, `off` 제어
|
- 팬 모드 `auto`, `manual`, `off` 제어
|
||||||
- PWM slider 기반 수동 팬 제어
|
- PWM slider 기반 수동 팬 제어
|
||||||
- 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 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
|
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
|
||||||
@@ -59,6 +60,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
|||||||
|
|
||||||
- `control_state`: 팬 모드와 PWM 상태
|
- `control_state`: 팬 모드와 PWM 상태
|
||||||
- `sensor_logs`: 온도, RPM, PWM, 배터리, load, memory, disk, uptime
|
- `sensor_logs`: 온도, RPM, PWM, 배터리, load, memory, disk, uptime
|
||||||
|
- `/tmp/control-low-voltage-state.json`: `vcgencmd get_throttled` 기반 저전압 감지 상태와 최근 지속시간
|
||||||
- `system_notice_state`: notice 활성 상태와 기준값
|
- `system_notice_state`: notice 활성 상태와 기준값
|
||||||
- `system_notice_logs`: notice 발생 이력
|
- `system_notice_logs`: notice 발생 이력
|
||||||
- `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간
|
- `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간
|
||||||
@@ -149,11 +151,13 @@ control-wifi-observe.service
|
|||||||
9. Reboot 버튼은 `재부팅` 단어 입력과 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
|
9. Reboot 버튼은 `재부팅` 단어 입력과 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
|
||||||
10. Translate 버튼은 최초 접속 시 디바이스 언어를 기본값으로 사용하고, 변경값은 `localStorage`의 `controlLang`에 저장합니다. 라벨은 `Translate` 또는 `번역`으로만 표시합니다.
|
10. Translate 버튼은 최초 접속 시 디바이스 언어를 기본값으로 사용하고, 변경값은 `localStorage`의 `controlLang`에 저장합니다. 라벨은 `Translate` 또는 `번역`으로만 표시합니다.
|
||||||
11. Theme 버튼은 기본값 `dark`로 시작하고, 변경값은 `localStorage`의 `controlTheme`에 저장합니다. 라벨은 `Theme` 또는 `테마`로만 표시합니다.
|
11. Theme 버튼은 기본값 `dark`로 시작하고, 변경값은 `localStorage`의 `controlTheme`에 저장합니다. 라벨은 `Theme` 또는 `테마`로만 표시합니다.
|
||||||
12. Push 발송, 수신, 표시, 클릭 이벤트를 기록하고 구독 DB의 건강 상태를 갱신합니다.
|
12. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다.
|
||||||
|
13. Push 발송, 수신, 표시, 클릭 이벤트를 기록하고 구독 DB의 건강 상태를 갱신합니다.
|
||||||
|
|
||||||
## 주요 함수/모듈
|
## 주요 함수/모듈
|
||||||
|
|
||||||
- `collect_snapshot()`: 센서와 fan 상태 snapshot 생성
|
- `collect_snapshot()`: 센서와 fan 상태 snapshot 생성
|
||||||
|
- `low_voltage_status()`: 라즈베리파이 throttled flag를 읽고 저전압 감지 이력을 상태 파일로 추적
|
||||||
- `apply_fan_policy()`: 팬 목표값 계산과 적용
|
- `apply_fan_policy()`: 팬 목표값 계산과 적용
|
||||||
- `json_out()`: API JSON 응답 표준화
|
- `json_out()`: API JSON 응답 표준화
|
||||||
- `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용
|
- `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용
|
||||||
|
|||||||
+102
@@ -117,6 +117,107 @@ function dmesg_log(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function low_voltage_status(): 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';
|
||||||
|
if (is_file($statePath) && !is_writable($statePath)) {
|
||||||
|
@chmod($statePath, 0666);
|
||||||
|
}
|
||||||
|
|
||||||
|
$state = [
|
||||||
|
'active' => false,
|
||||||
|
'active_since' => null,
|
||||||
|
'last_detected_at' => null,
|
||||||
|
'last_cleared_at' => null,
|
||||||
|
'last_duration_seconds' => 0,
|
||||||
|
'updated_at' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
$fp = @fopen($statePath, 'c+');
|
||||||
|
if ($fp !== false) {
|
||||||
|
@chmod($statePath, 0666);
|
||||||
|
@flock($fp, LOCK_EX);
|
||||||
|
$saved = stream_get_contents($fp);
|
||||||
|
$decoded = json_decode((string)$saved, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$state = array_merge($state, array_intersect_key($decoded, $state));
|
||||||
|
}
|
||||||
|
|
||||||
|
$wasActive = !empty($state['active']);
|
||||||
|
$activeSince = is_numeric($state['active_since'] ?? null) ? (int)$state['active_since'] : null;
|
||||||
|
|
||||||
|
if ($flags !== null) {
|
||||||
|
if ($active) {
|
||||||
|
if (!$wasActive || $activeSince === null) {
|
||||||
|
$activeSince = $now;
|
||||||
|
$state['last_detected_at'] = $now;
|
||||||
|
}
|
||||||
|
$state['active'] = true;
|
||||||
|
$state['active_since'] = $activeSince;
|
||||||
|
} else {
|
||||||
|
if ($wasActive && $activeSince !== null) {
|
||||||
|
$state['last_cleared_at'] = $now;
|
||||||
|
$state['last_duration_seconds'] = max(0, $now - $activeSince);
|
||||||
|
}
|
||||||
|
$state['active'] = false;
|
||||||
|
$state['active_since'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$state['updated_at'] = $now;
|
||||||
|
ftruncate($fp, 0);
|
||||||
|
rewind($fp);
|
||||||
|
fwrite($fp, json_encode($state, JSON_UNESCAPED_SLASHES));
|
||||||
|
fflush($fp);
|
||||||
|
@chmod($statePath, 0666);
|
||||||
|
}
|
||||||
|
|
||||||
|
@flock($fp, LOCK_UN);
|
||||||
|
fclose($fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
$duration = 0;
|
||||||
|
if ($active && is_numeric($state['active_since'] ?? null)) {
|
||||||
|
$duration = max(0, $now - (int)$state['active_since']);
|
||||||
|
} elseif (is_numeric($state['last_duration_seconds'] ?? null)) {
|
||||||
|
$duration = max(0, (int)$state['last_duration_seconds']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dateValue = static fn($ts): ?string => is_numeric($ts) && (int)$ts > 0
|
||||||
|
? date('Y-m-d H:i:s', (int)$ts)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'available' => $flags !== null,
|
||||||
|
'raw' => $raw,
|
||||||
|
'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),
|
||||||
|
'duration_seconds' => $duration,
|
||||||
|
'last_duration_seconds' => (int)($state['last_duration_seconds'] ?? 0),
|
||||||
|
'updated_at' => $dateValue($state['updated_at'] ?? null),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function fan_paths(): array
|
function fan_paths(): array
|
||||||
{
|
{
|
||||||
$candidates = glob('/sys/devices/platform/cooling_fan/hwmon/hwmon*/pwm1') ?: [];
|
$candidates = glob('/sys/devices/platform/cooling_fan/hwmon/hwmon*/pwm1') ?: [];
|
||||||
@@ -1748,6 +1849,7 @@ function collect_snapshot(bool $applyFan = true): array
|
|||||||
'active_users' => active_user_info(),
|
'active_users' => active_user_info(),
|
||||||
'memory' => $mem,
|
'memory' => $mem,
|
||||||
'disk' => $disk,
|
'disk' => $disk,
|
||||||
|
'low_voltage' => low_voltage_status(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$history = add_battery_remaining_history(sensor_history(240));
|
$history = add_battery_remaining_history(sensor_history(240));
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
statusDisk: $('#statusDisk'),
|
statusDisk: $('#statusDisk'),
|
||||||
statusMemory: $('#statusMemory'),
|
statusMemory: $('#statusMemory'),
|
||||||
statusUptime: $('#statusUptime'),
|
statusUptime: $('#statusUptime'),
|
||||||
|
statusLowVoltage: $('#statusLowVoltage'),
|
||||||
|
statusLowVoltageLast: $('#statusLowVoltageLast'),
|
||||||
|
statusLowVoltageDuration: $('#statusLowVoltageDuration'),
|
||||||
statusBatteryVoltage: $('#statusBatteryVoltage'),
|
statusBatteryVoltage: $('#statusBatteryVoltage'),
|
||||||
statusBatterySoc: $('#statusBatterySoc'),
|
statusBatterySoc: $('#statusBatterySoc'),
|
||||||
statusBatteryRemaining: $('#statusBatteryRemaining'),
|
statusBatteryRemaining: $('#statusBatteryRemaining'),
|
||||||
@@ -88,6 +91,15 @@
|
|||||||
diskRoot: 'Disk',
|
diskRoot: 'Disk',
|
||||||
memory: 'Memory',
|
memory: 'Memory',
|
||||||
uptime: 'Uptime',
|
uptime: 'Uptime',
|
||||||
|
lowVoltage: 'Low Voltage',
|
||||||
|
lowVoltageLast: 'Last Low Voltage',
|
||||||
|
lowVoltageDuration: 'LV Duration',
|
||||||
|
lowVoltageActive: 'Active',
|
||||||
|
lowVoltageNormal: 'Normal',
|
||||||
|
lowVoltageSeen: 'seen since boot',
|
||||||
|
lowVoltageNotSeen: 'not seen since boot',
|
||||||
|
currentEpisode: 'current',
|
||||||
|
lastEpisode: 'last',
|
||||||
batteryV: 'Battery V',
|
batteryV: 'Battery V',
|
||||||
batterySoc: 'Battery SOC',
|
batterySoc: 'Battery SOC',
|
||||||
sensorHistory: 'Sensor History',
|
sensorHistory: 'Sensor History',
|
||||||
@@ -221,6 +233,15 @@
|
|||||||
diskRoot: '디스크',
|
diskRoot: '디스크',
|
||||||
memory: '메모리',
|
memory: '메모리',
|
||||||
uptime: '가동 시간',
|
uptime: '가동 시간',
|
||||||
|
lowVoltage: '저전압',
|
||||||
|
lowVoltageLast: '최근 저전압',
|
||||||
|
lowVoltageDuration: '저전압 지속',
|
||||||
|
lowVoltageActive: '감지 중',
|
||||||
|
lowVoltageNormal: '정상',
|
||||||
|
lowVoltageSeen: '부팅 후 이력 있음',
|
||||||
|
lowVoltageNotSeen: '부팅 후 이력 없음',
|
||||||
|
currentEpisode: '현재',
|
||||||
|
lastEpisode: '최근',
|
||||||
batteryV: '배터리 전압',
|
batteryV: '배터리 전압',
|
||||||
batterySoc: '배터리 SOC',
|
batterySoc: '배터리 SOC',
|
||||||
sensorHistory: '센서 이력',
|
sensorHistory: '센서 이력',
|
||||||
@@ -756,6 +777,27 @@
|
|||||||
return formatDhms(parseWifiDurationSeconds(value)) || value;
|
return formatDhms(parseWifiDurationSeconds(value)) || value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lowVoltageStateText(lowVoltage) {
|
||||||
|
if (!lowVoltage || lowVoltage.available === false) {
|
||||||
|
return t('na');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateText = lowVoltage.active ? t('lowVoltageActive') : t('lowVoltageNormal');
|
||||||
|
const seenText = lowVoltage.seen_since_boot ? t('lowVoltageSeen') : t('lowVoltageNotSeen');
|
||||||
|
return `${stateText} (${seenText})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lowVoltageDurationText(lowVoltage) {
|
||||||
|
if (!lowVoltage || lowVoltage.available === false) {
|
||||||
|
return t('na');
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = Number(lowVoltage.duration_seconds ?? 0);
|
||||||
|
const label = lowVoltage.active ? t('currentEpisode') : t('lastEpisode');
|
||||||
|
const formatted = formatDhms(seconds) || '-';
|
||||||
|
return `${label}: ${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
function timeAgo(seconds) {
|
function timeAgo(seconds) {
|
||||||
const formatted = formatDhms(seconds);
|
const formatted = formatDhms(seconds);
|
||||||
return formatted ? `${formatted} ${t('ago')}` : '-';
|
return formatted ? `${formatted} ${t('ago')}` : '-';
|
||||||
@@ -786,6 +828,7 @@
|
|||||||
const disk = system.disk || {};
|
const disk = system.disk || {};
|
||||||
const memory = system.memory || {};
|
const memory = system.memory || {};
|
||||||
const battery = data.battery || {};
|
const battery = data.battery || {};
|
||||||
|
const lowVoltage = system.low_voltage || {};
|
||||||
const load = Array.isArray(system.load) ? system.load : [];
|
const load = Array.isArray(system.load) ? system.load : [];
|
||||||
const activeUsers = system.active_users || {};
|
const activeUsers = system.active_users || {};
|
||||||
|
|
||||||
@@ -795,6 +838,9 @@
|
|||||||
setText(els.statusDisk, `${Number(disk.used_kb || 0).toLocaleString()} / ${Number(disk.total_kb || 0).toLocaleString()} KB (${disk.percent ?? '-'}%)`);
|
setText(els.statusDisk, `${Number(disk.used_kb || 0).toLocaleString()} / ${Number(disk.total_kb || 0).toLocaleString()} KB (${disk.percent ?? '-'}%)`);
|
||||||
setText(els.statusMemory, `${memory.used_mb ?? '-'} / ${memory.total_mb ?? '-'} MB (${memory.percent ?? '-'}%)`);
|
setText(els.statusMemory, `${memory.used_mb ?? '-'} / ${memory.total_mb ?? '-'} MB (${memory.percent ?? '-'}%)`);
|
||||||
setText(els.statusUptime, system.uptime || '-');
|
setText(els.statusUptime, system.uptime || '-');
|
||||||
|
setText(els.statusLowVoltage, lowVoltageStateText(lowVoltage));
|
||||||
|
setText(els.statusLowVoltageLast, lowVoltage.last_detected_at || '-');
|
||||||
|
setText(els.statusLowVoltageDuration, lowVoltageDurationText(lowVoltage));
|
||||||
setText(els.statusBatteryVoltage, battery.voltage === null || battery.voltage === undefined ? '-' : `${Number(battery.voltage).toFixed(3)} V`);
|
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.statusBatterySoc, battery.percent === null || battery.percent === undefined ? '-' : `${Number(battery.percent).toFixed(2)}%`);
|
||||||
setText(els.statusBatteryRemaining, battery.remaining?.display || '-');
|
setText(els.statusBatteryRemaining, battery.remaining?.display || '-');
|
||||||
|
|||||||
@@ -200,6 +200,9 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
|
|||||||
<div class="status-row"><div class="status-key" data-i18n="diskRoot">Disk /</div><div class="status-value" id="statusDisk">-</div></div>
|
<div class="status-row"><div class="status-key" data-i18n="diskRoot">Disk /</div><div class="status-value" id="statusDisk">-</div></div>
|
||||||
<div class="status-row"><div class="status-key" data-i18n="memory">Memory</div><div class="status-value" id="statusMemory">-</div></div>
|
<div class="status-row"><div class="status-key" data-i18n="memory">Memory</div><div class="status-value" id="statusMemory">-</div></div>
|
||||||
<div class="status-row"><div class="status-key" data-i18n="uptime">Uptime</div><div class="status-value" id="statusUptime">-</div></div>
|
<div class="status-row"><div class="status-key" data-i18n="uptime">Uptime</div><div class="status-value" id="statusUptime">-</div></div>
|
||||||
|
<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="batteryV">Battery V</div><div class="status-value" id="statusBatteryVoltage">-</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="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>
|
<div class="status-row"><div class="status-key" data-i18n="remaining">Remaining</div><div class="status-value" id="statusBatteryRemaining">-</div></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user