UPS 제거 상태에 맞춘 배터리 기능 정리

This commit is contained in:
seo
2026-07-03 14:18:05 +09:00
parent 6fceae0c01
commit 1bbbe5b694
5 changed files with 106 additions and 91 deletions
+24 -46
View File
@@ -4,7 +4,7 @@
## 프로젝트 성격 ## 프로젝트 성격
Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 웹에서 관리하기 위한 내부 운영 도구입니다. 로그인 후 대시보드에서 온도, 팬 RPM, PWM, WiFi client, 배터리, notice를 확인하고 필요한 제어 명령을 실행합니다. 첫 화면은 운영 판단에 필요한 핵심 정보 위주로 구성하고, 상세 차트와 진단성 데이터는 접힘 영역에 둡니다. Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 웹에서 관리하기 위한 내부 운영 도구입니다. 로그인 후 대시보드에서 온도, 팬 RPM, PWM, WiFi client, 저전압/스로틀링, notice를 확인하고 필요한 제어 명령을 실행합니다. 첫 화면은 운영 판단에 필요한 핵심 정보 위주로 구성하고, 상세 차트와 진단성 데이터는 접힘 영역에 둡니다.
상태 갱신은 WebSocket을 우선 사용하고, 연결 실패 시 HTTP fallback으로 전환합니다. 팬 정책 적용과 센서 수집은 백그라운드 작업과 API 호출을 통해 DB에 기록됩니다. 상태 갱신은 WebSocket을 우선 사용하고, 연결 실패 시 HTTP fallback으로 전환합니다. 팬 정책 적용과 센서 수집은 백그라운드 작업과 API 호출을 통해 DB에 기록됩니다.
@@ -12,10 +12,10 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
- 팬 모드 `auto`, `manual`, `off` 제어 - 팬 모드 `auto`, `manual`, `off` 제어
- PWM slider 기반 수동 팬 제어 - PWM slider 기반 수동 팬 제어
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지, 배터리 예측 파라미터 조정 - 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정
- CPU 온도, 팬 RPM, 배터리 SOC, 배터리 전압 핵심 차트 - CPU 온도, 팬 RPM 핵심 차트
- RP1 온도, 팬 효율, CPU 전력, 잔여 시간 상세 차트는 접힘 영역에서 확인 - RP1 온도, 팬 효율, CPU 전력 상세 차트는 접힘 영역에서 확인
- 배터리 잔여 시간은 최근 24시간 SOC 방전 추세와 최대 45일 장기 학습 프로파일을 함께 사용하고, CPU 전력 변화로 현재 부하를 보정 - UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음
- 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시 - 라즈베리파이 저전압/스로틀링 현재 상태, 복구 중/반복 발생 판정, 부팅 후 이력, 최근 감지 시각, 지속시간, 최근 10분 통계 표시
- WiFi client 목록과 2.4G/5G client 수 표시 - WiFi client 목록과 2.4G/5G client 수 표시
- 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정 - 5G 외부 WiFi 모듈이 연결 시간을 제공하지 않는 경우 서버 관측 기반 연결 시간 보정
@@ -61,7 +61,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
## 데이터/저장소 ## 데이터/저장소
- `control_state`: 팬 모드와 PWM 상태 - `control_state`: 팬 모드와 PWM 상태
- `sensor_logs`: 온도, RPM, PWM, 배터리, load, memory, disk, uptime - `sensor_logs`: 온도, RPM, PWM, load, memory, disk, uptime. 과거 배터리 컬럼은 호환을 위해 유지하지만 UPS 제거 상태에서는 새 배터리값을 기록하지 않습니다.
- `/tmp/control-low-voltage-state.json`: `vcgencmd get_throttled` 기반 저전압 감지 상태, 최근 지속시간, 최근 episode 이력 - `/tmp/control-low-voltage-state.json`: `vcgencmd get_throttled` 기반 저전압 감지 상태, 최근 지속시간, 최근 episode 이력
- `/tmp/control-throttling-state.json`: `vcgencmd get_throttled` 기반 스로틀링 감지 상태, 최근 지속시간, 최근 episode 이력 - `/tmp/control-throttling-state.json`: `vcgencmd get_throttled` 기반 스로틀링 감지 상태, 최근 지속시간, 최근 episode 이력
- `system_notice_state`: notice 활성 상태와 기준값 - `system_notice_state`: notice 활성 상태와 기준값
@@ -69,38 +69,16 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
- `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간 - `wifi_observed_sessions`: 5G WiFi client의 최초/마지막 감지 시간
- `wifi_client_aliases`: MAC별 수동 WiFi 호스트명 - `wifi_client_aliases`: MAC별 수동 WiFi 호스트명
- `app_settings`: 설정 모달에서 저장한 Control 동작값 오버라이드 - `app_settings`: 설정 모달에서 저장한 Control 동작값 오버라이드
- `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 배터리 설정 - `/home/seo/secret/control.php`: 앱 비밀번호, DB 설정, 선택적 배터리 설정
- 배터리 용량 설정`/home/seo/secret/control.php``battery.cell_capacity_mah`, `battery.parallel_cells`, `battery.nominal_voltage`, `battery.capacity_wh`를 사용합니다. - UPS/배터리 기능`/home/seo/secret/control.php``battery.enabled`가 명시적으로 true일 때만 동작합니다. 현재 운영 기준은 UPS 제거 상태이므로 기본값은 false입니다.
## 배터리 잔여 시간 계산 ## UPS/배터리 상태
잔여 시간은 단순히 현재 CPU 전력 평균만으로 계산하지 않습니다. 실제 배터리 SOC가 DB에서 어떤 속도로 떨어졌는지를 먼저 사용합니다. UPS가 과부하 상황에서 전압 하강과 강제 종료를 반복해 현재 운영 구성에서는 제거되어 있습니다. 이에 따라 Control은 기본적으로 MAX17048 배터리 센서를 읽지 않고, 배터리 SOC/전압 차트와 잔여 시간 UI도 표시하지 않습니다.
1. `sensor_logs`의 최근 24시간 배터리 기록을 1분 단위로 SQL 집계합니다. API 응답의 `battery.enabled`는 false, `battery.removed`는 true로 내려가며 `battery.remaining.display``UPS 제거됨`으로 표시됩니다. 과거에 저장된 `sensor_logs`의 배터리 컬럼은 DB 호환을 위해 남겨 두지만 새 snapshot에서는 배터리값을 추가하지 않습니다.
2. 30분, 1시간, 3시간, 6시간, 12시간, 24시간 창을 각각 검사합니다.
3. 각 창의 시작/끝 SOC는 평균 한 점이 아니라 가장자리 구간의 중앙값으로 잡아 순간 튐을 줄입니다.
4. 45분, 2시간, 4시간, 8시간, 16시간, 24시간 창은 가중 선형회귀로 SOC 기울기를 다시 계산합니다.
5. 회귀 후보는 중앙절대편차 기준으로 잔차가 큰 SOC 튐을 제거한 뒤 한 번 더 회귀해 기울기를 보정합니다.
6. 전압도 1분 집계 기록에서 가중 회귀로 기울기를 계산하고, 현재 전압에서 동적 하한 전압까지 남은 시간을 별도 후보로 만듭니다.
7. SOC가 충분히 줄어든 방전 구간만 후보로 사용하고, 충전 또는 회복처럼 보이는 구간은 제외합니다.
8. 최대 45일 기록은 약 5분 간격으로 샘플링해 SOC 구간별 장기 방전 프로파일과 SOC 1%당 실제 사용 Wh를 학습합니다.
9. 장기 프로파일은 현재 SOC와 가까운 구간, 최근에 관측된 구간, 샘플 수가 많은 구간에 더 높은 가중치를 줍니다.
10. 최근 CPU 전력이 각 후보 구간의 평균 전력보다 높거나 낮으면 방전 속도에 완만하게 반영해 현재 부하 변화를 보정합니다.
11. 단기 중앙값 후보, 강건 회귀 후보, 전압 기울기 후보, 에너지 학습 후보, 용량 전력 후보, 장기 학습 후보를 신뢰도와 안정도 가중치로 합산합니다.
12. 후보끼리 방전 속도가 크게 어긋나면 중앙값 기준으로 튄 후보를 제외하거나 가중치를 낮춥니다.
13. 최종 신뢰도는 후보별 신뢰도 가중 평균에 후보 간 방전 속도 분산을 반영해 계산합니다.
14. 용량 기반 후보와 fallback은 설정된 최소 시스템 전력보다 낮은 순간 전력값을 그대로 믿지 않고 최소 전력으로 끌어올려 계산합니다.
15. 1s8p 18650 배터리팩에서 비현실적으로 긴 값이 나오지 않도록 `잔여시간 현실 상한`을 100% 기준 최대 시간으로 두고, 현재 SOC에 비례해 최종 후보를 제한합니다.
16. 상한을 넘은 후보는 버리지 않고 상한값으로 눌러 반영하며, 신뢰도와 가중치를 낮춰 다른 실제 방전 후보를 더 우선하게 합니다.
17. SOC 추세가 부족하면 마지막 대체값으로 배터리 용량 `BATTERY_CAPACITY_WH`와 최근 CPU 전력의 절사 평균을 사용합니다.
WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과를 55초 동안, 약 5분 간격 장기 학습 샘플을 10분 동안 재사용해 DB 부담을 줄입니다. 장기 학습 샘플은 파일 캐시로도 보관해 다른 PHP 프로세스에서도 빠르게 재사용니다. 나중에 UPS 구성을 다시 붙일 경우 `/home/seo/secret/control.php``battery.enabled`를 true로 두면 기존 배터리 측정/학습 코드가 다시 사용니다.
대시보드의 잔여 시간 값에는 Control UI 스타일의 커스텀 계산 근거 팝오버가 붙습니다. 기본 브라우저 `title` tooltip은 사용하지 않으며, 마우스 hover 또는 키보드 focus 중에도 상태 갱신이 들어오면 팝오버 내용과 위치가 즉시 갱신됩니다. `SOC 다중 시간창 추세`로 표시되면 실제 DB 방전 속도 기반이며, 계산 출처에 `장기 학습`이 포함되면 누적 기록에서 학습한 SOC/부하 구간별 방전 경험도 반영된 상태입니다. `배터리 용량과 최근 전력 평균`이면 SOC 추세가 부족해 용량 기반 fallback이 사용된 상태입니다.
팝오버는 계산 잔여 시간, 현실 상한, 상한 전 계산값, 시간당 SOC 감소율, 추세 신뢰도, 후보 수, 후보 분산, 최근 CPU 전력, 계산 출처, 반영 구간을 표시합니다. 각 후보별로 최근 30분/1시간/3시간/6시간/12시간/24시간 감소율, 신뢰도, 부하 보정 배율을 표시하고, 강건 회귀 후보는 튐 제거 후 남은 유효점 비율과 후보 안정도를 함께 표시합니다. 전압 후보는 하한 전압과 시간당 전압 하강량을, 에너지 학습 후보는 SOC 1%당 실제 Wh를, 장기 학습 후보는 반영된 학습 구간 수와 SOC 구간 수를 함께 표시합니다.
잔여 시간 차트의 마지막 지점은 현재 snapshot의 `battery.remaining.seconds`와 동기화합니다. DB에 저장된 마지막 센서 기록이 몇 초 늦어도 좌측 상태 영역과 차트의 최신 잔여 시간이 서로 다른 값을 보여주지 않도록, API 응답 history 끝에 현재 snapshot row를 반영합니다.
## 설정 모달 ## 설정 모달
@@ -113,7 +91,7 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과
- 화면/진단 표시: 프로세스 후보 수, 사용자 서비스 로그 줄 수, 사용자 서비스 캐시 시간, 팬 이상 이력 수 - 화면/진단 표시: 프로세스 후보 수, 사용자 서비스 로그 줄 수, 사용자 서비스 캐시 시간, 팬 이상 이력 수
- 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수 - 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수
- 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이, RPM 감시 시작 PWM, alert 유지 중 반복 기록, 반복 기록 간격, 하강 변화 원인 후보 표시 여부 - 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이, RPM 감시 시작 PWM, alert 유지 중 반복 기록, 반복 기록 간격, 하강 변화 원인 후보 표시 여부
- 배터리 예측: 단기/장기 학습 범위, 후보 필터 비율, 부하 보정 강도, 전압 하한, 용량 모델 가중치, 잔여시간 현실 상한, 최소 시스템 전력, 상한 초과 후보 감쇠, 에너지/전압 모델 사용 여부, 상태/잔여시간 차트 표본 수 - UPS/배터리 설정: 현재 UPS 제거 상태에서는 설정 모달에서 숨겨지고, `battery.enabled=true`일 때만 배터리 예측 항목을 노출합니다.
설정 모달이 열려 있는 동안 1초 상태 갱신은 폼 DOM을 다시 만들지 않습니다. 입력 중인 값은 저장, 기본값 초기화, 닫기 전까지 유지되며, 백그라운드 snapshot은 내부 설정 payload만 갱신합니다. 단순 boolean 설정은 `켜기`/`끄기` 세그먼트로 표시합니다. 설정 모달이 열려 있는 동안 1초 상태 갱신은 폼 DOM을 다시 만들지 않습니다. 입력 중인 값은 저장, 기본값 초기화, 닫기 전까지 유지되며, 백그라운드 snapshot은 내부 설정 payload만 갱신합니다. 단순 boolean 설정은 `켜기`/`끄기` 세그먼트로 표시합니다.
@@ -123,7 +101,7 @@ WebSocket 장기 실행 프로세스에서는 1분 단위 단기 집계 결과
- WebSocket 최초 연결: 즉시 1회 상태 전송 - WebSocket 최초 연결: 즉시 1회 상태 전송
- WebSocket 끊김 시 HTTP fallback 상태 갱신: 2초마다 - WebSocket 끊김 시 HTTP fallback 상태 갱신: 2초마다
- 탭 복귀: 즉시 1회 상태 갱신 - 탭 복귀: 즉시 1회 상태 갱신
- 잔여 시간 커스텀 팝오버: 상태 갱신과 동일하며 hover/focus 중에도 내용과 위치를 계속 갱신 - UPS/배터리 잔여 시간: 현재 운영 구성에서는 표시하지 않음
- dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지 - dmesg: 진단 패널에서 열었을 때만 1초마다 갱신, 닫으면 중지
- WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행 - WiFi 관측 보정 timer: 10초마다 `bin/wifi_observe.php` 실행
- WebSocket 소스 변경 감지: 15초마다 확인 후 재시작 - WebSocket 소스 변경 감지: 15초마다 확인 후 재시작
@@ -181,10 +159,9 @@ control-wifi-observe.service
11. Reboot 버튼은 설정된 확인 문구와 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다. 11. Reboot 버튼은 설정된 확인 문구와 관리자 암호 재입력을 모두 통과한 뒤 서버 API로 재부팅을 요청합니다.
12. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압/스로틀링 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다. 12. 시스템 상태는 `vcgencmd get_throttled`를 읽어 현재 저전압/스로틀링 여부와 부팅 후 이력, 최근 감지 시각, 현재/최근 지속시간을 표시합니다.
13. 저전압/스로틀링 상태 파일에는 최근 episode 시작/종료를 보관하고, 최근 10분 발생 횟수, 감지 누적시간, 감지 비율을 계산합니다. 13. 저전압/스로틀링 상태 파일에는 최근 episode 시작/종료를 보관하고, 최근 10분 발생 횟수, 감지 누적시간, 감지 비율을 계산합니다.
14. 배터리 잔여 시간은 최근 24시간 SOC 방전 추세와 최대 45일 장기 학습 프로파일을 함께 평가하고, 현재 CPU 전력 변화와 현실 상한을 반영합니다. 14. UPS/배터리 장치가 제거되어 배터리 센서 조회와 잔여 시간 학습 계산은 기본적으로 돌리지 않습니다.
15. 잔여 시간 차트의 마지막 지점은 현재 snapshot의 잔여 시간과 같은 값을 사용하도록 history 응답을 동기화합니다. 15. System Notice 조건이 맞으면 Control이 `system_notice_logs`에 이력을 저장합니다.
16. System Notice 조건이 맞으면 Control이 `system_notice_logs`에 이력을 저장합니다. 16. 대시보드 첫 화면에는 팬/전원/스로틀링/WiFi/핵심 차트를 우선 배치하고, 상세 차트, 프로세스 후보, 사용자 서비스, dmesg는 접힘 영역으로 분리합니다.
17. 대시보드 첫 화면에는 팬/전원/스로틀링/WiFi/핵심 차트를 우선 배치하고, 상세 차트, 프로세스 후보, 사용자 서비스, dmesg는 접힘 영역으로 분리합니다.
## 팬 판단 온도 ## 팬 판단 온도
@@ -208,8 +185,9 @@ control-wifi-observe.service
- `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용 - `apply_observed_wifi_connected_time()`: 5G WiFi 연결 시간이 없는 client에 서버 관측 경과 시간 적용
- `wifi_client_aliases()`, `save_wifi_client_alias()`: MAC별 WiFi 호스트명 조회와 저장 - `wifi_client_aliases()`, `save_wifi_client_alias()`: MAC별 WiFi 호스트명 조회와 저장
- `apply_wifi_estimates()`: WiFi 신호/송신 속도/수신 속도 결측값 보정 - `apply_wifi_estimates()`: WiFi 신호/송신 속도/수신 속도 결측값 보정
- `battery_trend_history()`: 최근 24시간 배터리 기록을 1분 단위로 집계 - `battery_status()`: `CONTROL_BATTERY_ENABLED`가 true일 때만 배터리 센서를 읽고, 현재 기본 구성에서는 제거 상태를 반환
- `battery_profile_history()`: 최대 45일 배터리 기록을 약 5분 간격으로 샘플링하고 파일 캐시로 보관 - `battery_trend_history()`: 배터리 기능을 다시 켤 때 최근 24시간 배터리 기록을 1분 단위로 집계
- `battery_profile_history()`: 배터리 기능을 다시 켤 때 최대 45일 배터리 기록을 약 5분 간격으로 샘플링하고 파일 캐시로 보관
- `weighted_linear_regression()`, `numeric_mad()`: SOC 기울기 계산과 회귀 잔차 튐 제거 - `weighted_linear_regression()`, `numeric_mad()`: SOC 기울기 계산과 회귀 잔차 튐 제거
- `battery_regression_trend_candidate()`: 1분 SOC 집계를 강건 회귀로 분석해 정밀 방전 속도 후보 계산 - `battery_regression_trend_candidate()`: 1분 SOC 집계를 강건 회귀로 분석해 정밀 방전 속도 후보 계산
- `battery_voltage_trend_candidate()`: 전압 기울기와 동적 하한 전압으로 잔여 시간 후보 계산 - `battery_voltage_trend_candidate()`: 전압 기울기와 동적 하한 전압으로 잔여 시간 후보 계산
@@ -227,7 +205,8 @@ control-wifi-observe.service
- `assets/wakelock.js`: WakeLock 버튼 상태와 Screen Wake Lock API 제어 - `assets/wakelock.js`: WakeLock 버튼 상태와 Screen Wake Lock API 제어
- `customAlert()`, `customConfirm()`, `customPrompt()`: 대시보드 공통 확인/입력 dialog - `customAlert()`, `customConfirm()`, `customPrompt()`: 대시보드 공통 확인/입력 dialog
- `controlLang`, `controlTheme`: 재접속 후에도 유지되는 언어/테마 localStorage key - `controlLang`, `controlTheme`: 재접속 후에도 유지되는 언어/테마 localStorage key
- `BATTERY_CELL_CAPACITY_MAH`, `BATTERY_PARALLEL_CELLS`, `BATTERY_CAPACITY_MAH`, `BATTERY_NOMINAL_VOLTAGE`, `BATTERY_CAPACITY_WH`: 병렬 배터리팩 용량과 잔여시간 계산 기준 - `CONTROL_BATTERY_ENABLED`: UPS/배터리 센서와 잔여시간 학습 계산 사용 여부
- `BATTERY_CELL_CAPACITY_MAH`, `BATTERY_PARALLEL_CELLS`, `BATTERY_CAPACITY_MAH`, `BATTERY_NOMINAL_VOLTAGE`, `BATTERY_CAPACITY_WH`: 배터리 기능을 다시 켤 때 쓰는 병렬 배터리팩 용량과 잔여시간 계산 기준
## 보안 ## 보안
@@ -240,13 +219,12 @@ control-wifi-observe.service
- 로그인 화면은 CSRF 검사를 수행하고, 설정한 실패 횟수/집계 시간/잠금 시간에 따라 세션 단위 잠금을 적용합니다. - 로그인 화면은 CSRF 검사를 수행하고, 설정한 실패 횟수/집계 시간/잠금 시간에 따라 세션 단위 잠금을 적용합니다.
- Remember login은 설정에 따라 Secure/SameSite, 토큰 길이, 만료 기간, User-Agent 고정, IPv4 접두사 고정을 조정할 수 있습니다. - Remember login은 설정에 따라 Secure/SameSite, 토큰 길이, 만료 기간, User-Agent 고정, IPv4 접두사 고정을 조정할 수 있습니다.
- WiFi restart/reload 허용 여부와 root 명령 timeout은 설정 모달에서 조정합니다. - WiFi restart/reload 허용 여부와 root 명령 timeout은 설정 모달에서 조정합니다.
- 앱 비밀번호와 DB/배터리 설정은 저장소 밖 secret 파일로 관리합니다. - 앱 비밀번호와 DB/선택적 배터리 설정은 저장소 밖 secret 파일로 관리합니다.
## 운영 체크포인트 ## 운영 체크포인트
- 센서 수집 주기와 DB 증가량을 확인합니다. - 센서 수집 주기와 DB 증가량을 확인합니다.
- 배터리팩 변경 시 `/home/seo/secret/control.php``battery` 값을 먼저 갱신합니다. 현재 기준은 `2200mAh x 8 parallel = 17600mAh`, nominal `65.12Wh`이며, SOC 추세가 부족한 경우의 fallback 계산에 사용됩니다. - UPS/배터리를 다시 붙일 때는 `/home/seo/secret/control.php``battery.enabled`와 용량값을 먼저 확인합니다.
- 잔여 시간이 갑자기 흔들리면 커스텀 팝오버의 `계산 출처`, `반영 구간`, `시간당 SOC 감소`, `추세 신뢰도`, `최근 CPU 전력`을 먼저 확인합니다.
- WiFi client 표는 갱신 중에도 가로 스크롤 위치를 보존합니다. - WiFi client 표는 갱신 중에도 가로 스크롤 위치를 보존합니다.
- 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다. - 하드웨어 또는 OS 변경 후 fan sysfs 경로를 확인합니다.
- 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다. - 2.4G 내장 WiFi는 `hostapd-24g.service` 개별 restart 직후 일부 IoT 단말이 WPA/EAPOL 재협상 루프에 들어갈 수 있으므로, 안정화된 상태에서는 개별 restart를 피하고 필요 시 전체 reboot 또는 채널 변경으로 재초기화합니다.
+23 -1
View File
@@ -14,6 +14,7 @@ if (!is_file($controlSecretFile)) {
$controlSecretConfig = require $controlSecretFile; $controlSecretConfig = require $controlSecretFile;
$controlDbConfig = is_array($controlSecretConfig['db'] ?? null) ? $controlSecretConfig['db'] : []; $controlDbConfig = is_array($controlSecretConfig['db'] ?? null) ? $controlSecretConfig['db'] : [];
$controlBatteryConfig = is_array($controlSecretConfig['battery'] ?? null) ? $controlSecretConfig['battery'] : []; $controlBatteryConfig = is_array($controlSecretConfig['battery'] ?? null) ? $controlSecretConfig['battery'] : [];
define('CONTROL_BATTERY_ENABLED', (bool)($controlBatteryConfig['enabled'] ?? false));
define('APP_PASSWORD', (string)($controlSecretConfig['app_password'] ?? '')); define('APP_PASSWORD', (string)($controlSecretConfig['app_password'] ?? ''));
define('DB_HOST', (string)($controlDbConfig['host'] ?? '127.0.0.1')); define('DB_HOST', (string)($controlDbConfig['host'] ?? '127.0.0.1'));
@@ -126,7 +127,7 @@ function clamp_float(float $value, float $min, float $max): float
function setting_definitions(): array function setting_definitions(): array
{ {
return [ $definitions = [
'security.remember_days' => ['group' => 'security', 'label' => '자동 로그인 유지 기간', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365, 'step' => 1, 'unit' => '일'], 'security.remember_days' => ['group' => 'security', 'label' => '자동 로그인 유지 기간', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365, 'step' => 1, 'unit' => '일'],
'security.cookie_secure_mode' => ['group' => 'security', 'label' => 'Remember 쿠키 Secure', 'type' => 'select', 'default' => 'auto', 'options' => ['auto' => 'HTTPS일 때 자동', 'always' => '항상 사용', 'never' => '사용 안함']], 'security.cookie_secure_mode' => ['group' => 'security', 'label' => 'Remember 쿠키 Secure', 'type' => 'select', 'default' => 'auto', 'options' => ['auto' => 'HTTPS일 때 자동', 'always' => '항상 사용', 'never' => '사용 안함']],
'security.cookie_samesite' => ['group' => 'security', 'label' => 'Remember 쿠키 SameSite', 'type' => 'select', 'default' => 'Lax', 'options' => ['Lax' => 'Lax', 'Strict' => 'Strict', 'None' => 'None']], 'security.cookie_samesite' => ['group' => 'security', 'label' => 'Remember 쿠키 SameSite', 'type' => 'select', 'default' => 'Lax', 'options' => ['Lax' => 'Lax', 'Strict' => 'Strict', 'None' => 'None']],
@@ -191,6 +192,16 @@ function setting_definitions(): array
'battery.history_chart_limit' => ['group' => 'battery', 'label' => '상태 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'], 'battery.history_chart_limit' => ['group' => 'battery', 'label' => '상태 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'],
'battery.history_sync_limit' => ['group' => 'battery', 'label' => '잔여시간 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'], 'battery.history_sync_limit' => ['group' => 'battery', 'label' => '잔여시간 차트 표본 수', 'type' => 'number', 'default' => 240, 'min' => 60, 'max' => 1500, 'step' => 10, 'unit' => '개'],
]; ];
if (!CONTROL_BATTERY_ENABLED) {
foreach (array_keys($definitions) as $key) {
if (str_starts_with($key, 'battery.')) {
unset($definitions[$key]);
}
}
}
return $definitions;
} }
function setting_rows(bool $reload = false): array function setting_rows(bool $reload = false): array
@@ -1123,6 +1134,15 @@ function i2cget_word_bytes(int $bus, int $address, int $register): ?array
function battery_status(): array function battery_status(): array
{ {
if (!CONTROL_BATTERY_ENABLED) {
return [
'enabled' => false,
'removed' => true,
'voltage' => null,
'percent' => null,
];
}
$voltage = null; $voltage = null;
$percent = null; $percent = null;
@@ -1140,6 +1160,8 @@ function battery_status(): array
} }
return [ return [
'enabled' => true,
'removed' => false,
'voltage' => $voltage, 'voltage' => $voltage,
'percent' => $percent, 'percent' => $percent,
]; ];
+15 -4
View File
@@ -3219,8 +3219,10 @@ function collect_snapshot(bool $applyFan = true): array
'watts' => isset($latest['cpu_watts']) ? (float)$latest['cpu_watts'] : null, 'watts' => isset($latest['cpu_watts']) ? (float)$latest['cpu_watts'] : null,
]; ];
$battery = $applyFan ? battery_status() : [ $battery = $applyFan ? battery_status() : [
'voltage' => isset($latest['battery_voltage']) ? (float)$latest['battery_voltage'] : null, 'enabled' => CONTROL_BATTERY_ENABLED,
'percent' => isset($latest['battery_percent']) ? (float)$latest['battery_percent'] : null, 'removed' => !CONTROL_BATTERY_ENABLED,
'voltage' => CONTROL_BATTERY_ENABLED && isset($latest['battery_voltage']) ? (float)$latest['battery_voltage'] : null,
'percent' => CONTROL_BATTERY_ENABLED && isset($latest['battery_percent']) ? (float)$latest['battery_percent'] : null,
]; ];
if ($applyFan) { if ($applyFan) {
@@ -3231,8 +3233,8 @@ function collect_snapshot(bool $applyFan = true): array
'rp1_temp_c' => $rp1Temp, 'rp1_temp_c' => $rp1Temp,
'cpu_voltage' => $cpuPower['voltage'], 'cpu_voltage' => $cpuPower['voltage'],
'cpu_watts' => $cpuPower['watts'], 'cpu_watts' => $cpuPower['watts'],
'battery_voltage' => $battery['voltage'], 'battery_voltage' => CONTROL_BATTERY_ENABLED ? $battery['voltage'] : null,
'battery_percent' => $battery['percent'], 'battery_percent' => CONTROL_BATTERY_ENABLED ? $battery['percent'] : null,
'pwm_value' => $pwm, 'pwm_value' => $pwm,
'pwm_percent' => round($pwm / 255 * 100, 2), 'pwm_percent' => round($pwm / 255 * 100, 2),
'pwm_mode' => $policy['mode'], 'pwm_mode' => $policy['mode'],
@@ -3278,6 +3280,7 @@ function collect_snapshot(bool $applyFan = true): array
'throttling' => $throttledStatuses['throttling'], 'throttling' => $throttledStatuses['throttling'],
]); ]);
if (CONTROL_BATTERY_ENABLED) {
$history = add_battery_remaining_history(sensor_history((int)setting_value('battery.history_chart_limit'))); $history = add_battery_remaining_history(sensor_history((int)setting_value('battery.history_chart_limit')));
$batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours')); $batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours'));
$batteryProfile = battery_profile_history((int)setting_value('battery.profile_days')); $batteryProfile = battery_profile_history((int)setting_value('battery.profile_days'));
@@ -3306,6 +3309,14 @@ function collect_snapshot(bool $applyFan = true): array
'mem_percent' => $mem['total_mb'] > 0 ? round($mem['used_mb'] / $mem['total_mb'] * 100, 1) : 0, 'mem_percent' => $mem['total_mb'] > 0 ? round($mem['used_mb'] / $mem['total_mb'] * 100, 1) : 0,
], $battery['remaining']['seconds'] ?? null); ], $battery['remaining']['seconds'] ?? null);
$history = array_slice($history, -(int)setting_value('battery.history_sync_limit')); $history = array_slice($history, -(int)setting_value('battery.history_sync_limit'));
} else {
$history = sensor_history(240);
$battery['remaining'] = [
'display' => 'UPS 제거됨',
'seconds' => null,
'source' => 'battery_removed',
];
}
$processes = process_resource_data((int)setting_value('display.process_limit')); $processes = process_resource_data((int)setting_value('display.process_limit'));
$fanSpike = fan_spike_analysis($history, $fan, $system, $processes); $fanSpike = fan_spike_analysis($history, $fan, $system, $processes);
+11 -1
View File
@@ -1444,10 +1444,14 @@
setText(els.statusThrottlingLast, throttling.last_detected_at || '-'); setText(els.statusThrottlingLast, throttling.last_detected_at || '-');
setText(els.statusThrottlingDuration, eventDurationText(throttling)); setText(els.statusThrottlingDuration, eventDurationText(throttling));
setText(els.statusThrottlingRecent, eventRecentText(throttling)); setText(els.statusThrottlingRecent, eventRecentText(throttling));
if (els.statusBatteryVoltage) {
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`);
}
if (els.statusBatterySoc) {
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 || '-'); }
if (els.statusBatteryRemaining) { if (els.statusBatteryRemaining) {
setText(els.statusBatteryRemaining, battery.remaining?.display || '-');
els.statusBatteryRemaining.removeAttribute('title'); els.statusBatteryRemaining.removeAttribute('title');
updateBatteryTooltip(batteryRemainingTitle(battery.remaining)); updateBatteryTooltip(batteryRemainingTitle(battery.remaining));
} }
@@ -1952,10 +1956,16 @@
chart('fanRpmChart', 'RPM', rows, 'fan_rpm', '#3b82f6', ' RPM', dynamicScale(rows, 'fan_rpm', { minSpan: 1000, padding: 400 })); chart('fanRpmChart', 'RPM', rows, 'fan_rpm', '#3b82f6', ' RPM', dynamicScale(rows, 'fan_rpm', { minSpan: 1000, padding: 400 }));
chart('fanEfficiencyChart', 'FANEFF', rows, 'fan_efficiency', '#84cc16', '', dynamicScale(rows, 'fan_efficiency', { minSpan: 20, ceiling: 100 })); chart('fanEfficiencyChart', 'FANEFF', rows, 'fan_efficiency', '#84cc16', '', dynamicScale(rows, 'fan_efficiency', { minSpan: 20, ceiling: 100 }));
chart('cpuWattChart', 'CPUW', rows, 'cpu_watts', '#a855f7', 'W', dynamicScale(rows, 'cpu_watts', { minSpan: 1, padding: 0.4 }, cpuWattSmoothing), { transform: cpuWattSmoothing }); chart('cpuWattChart', 'CPUW', rows, 'cpu_watts', '#a855f7', 'W', dynamicScale(rows, 'cpu_watts', { minSpan: 1, padding: 0.4 }, cpuWattSmoothing), { transform: cpuWattSmoothing });
if (document.getElementById('batterySocChart')) {
chart('batterySocChart', 'BATTERYSOC', rows, 'battery_percent', '#f59e0b', '%', dynamicScale(rows, 'battery_percent', { minSpan: 10, ceiling: 110 })); chart('batterySocChart', 'BATTERYSOC', rows, 'battery_percent', '#f59e0b', '%', dynamicScale(rows, 'battery_percent', { minSpan: 10, ceiling: 110 }));
}
if (document.getElementById('remainingChart')) {
chart('remainingChart', 'REMAINING', rows, 'battery_remaining_seconds', '#06b6d4', 's', dynamicScale(rows, 'battery_remaining_seconds', { minSpan: 1800, padding: 600 }), { tooltipFormat: formatDurationSeconds }); chart('remainingChart', 'REMAINING', rows, 'battery_remaining_seconds', '#06b6d4', 's', dynamicScale(rows, 'battery_remaining_seconds', { minSpan: 1800, padding: 600 }), { tooltipFormat: formatDurationSeconds });
}
if (document.getElementById('batteryVoltageChart')) {
chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 })); chart('batteryVoltageChart', 'BATTERYV', rows, 'battery_voltage', '#14b8a6', 'V', dynamicScale(rows, 'battery_voltage', { minSpan: 0.2, padding: 0.06 }));
} }
}
function resizeChartsSoon() { function resizeChartsSoon() {
window.setTimeout(() => { window.setTimeout(() => {
-6
View File
@@ -222,9 +222,6 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
<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="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="throttlingDuration">Throttle Duration</div><div class="status-value" id="statusThrottlingDuration">-</div></div>
<div class="status-row"><div class="status-key" data-i18n="throttlingRecent">Throttle 10m</div><div class="status-value" id="statusThrottlingRecent">-</div></div> <div class="status-row"><div class="status-key" data-i18n="throttlingRecent">Throttle 10m</div><div class="status-value" id="statusThrottlingRecent">-</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 interactive" id="statusBatteryRemaining" tabindex="0" aria-describedby="batteryRuntimeTooltip">-</div></div>
</div> </div>
</div> </div>
@@ -236,8 +233,6 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
<div class="chart-grid"> <div class="chart-grid">
<div class="chart-box"><h3 data-i18n="temperature">Temperature</h3><div class="chart-canvas"><canvas id="tempChart"></canvas></div></div> <div class="chart-box"><h3 data-i18n="temperature">Temperature</h3><div class="chart-canvas"><canvas id="tempChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="fanRpm">Fan RPM</h3><div class="chart-canvas"><canvas id="fanRpmChart"></canvas></div></div> <div class="chart-box"><h3 data-i18n="fanRpm">Fan RPM</h3><div class="chart-canvas"><canvas id="fanRpmChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="batterySoc">Battery SOC</h3><div class="chart-canvas"><canvas id="batterySocChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="batteryVoltage">Battery Voltage</h3><div class="chart-canvas"><canvas id="batteryVoltageChart"></canvas></div></div>
</div> </div>
<details class="details-panel" id="secondaryChartDetails"> <details class="details-panel" id="secondaryChartDetails">
<summary data-i18n="detailCharts">Detail Charts</summary> <summary data-i18n="detailCharts">Detail Charts</summary>
@@ -246,7 +241,6 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
<div class="chart-box"><h3 data-i18n="rp1Temp">RP1 Temp</h3><div class="chart-canvas"><canvas id="rp1TempChart"></canvas></div></div> <div class="chart-box"><h3 data-i18n="rp1Temp">RP1 Temp</h3><div class="chart-canvas"><canvas id="rp1TempChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="fanEfficiency">Fan Efficiency</h3><div class="chart-canvas"><canvas id="fanEfficiencyChart"></canvas></div></div> <div class="chart-box"><h3 data-i18n="fanEfficiency">Fan Efficiency</h3><div class="chart-canvas"><canvas id="fanEfficiencyChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="cpuWatt">CPU Watt</h3><div class="chart-canvas"><canvas id="cpuWattChart"></canvas></div></div> <div class="chart-box"><h3 data-i18n="cpuWatt">CPU Watt</h3><div class="chart-canvas"><canvas id="cpuWattChart"></canvas></div></div>
<div class="chart-box"><h3 data-i18n="remaining">Remaining</h3><div class="chart-canvas"><canvas id="remainingChart"></canvas></div></div>
</div> </div>
</div> </div>
</details> </details>