터치 디스플레이 설정 관리 추가
This commit is contained in:
@@ -13,6 +13,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
- 팬 모드 `auto`, `manual`, `off` 제어
|
||||
- PWM slider 기반 수동 팬 제어
|
||||
- 오버레이 설정 모달에서 보안 정책, 팬 자동곡선, 이벤트 판정, 팬 이상감지 파라미터 조정
|
||||
- 설정 모달에서 라즈베리파이 정품 7인치 DSI 터치 디스플레이 상태 확인, 콘솔 회전, 터치 좌표 보정, LCD/터치 비활성화 부팅 설정 조정
|
||||
- CPU 온도, 팬 RPM 핵심 차트
|
||||
- RP1 온도, 팬 효율, CPU 전력 상세 차트는 접힘 영역에서 확인
|
||||
- UPS/배터리 잔여 시간 기능은 장치 제거 후 비활성 상태로 두고, 배터리 센서 조회와 장기 학습 계산은 돌리지 않음
|
||||
@@ -46,6 +47,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
- `public/api.php?action=settings`: 설정 가능한 Control 동작값 조회
|
||||
- `public/api.php?action=settings_save`: 설정 모달 값 저장
|
||||
- `public/api.php?action=settings_reset`: 설정 모달 값을 기본값으로 초기화
|
||||
- `public/api.php?action=touch_display_save`: `/boot/firmware/config.txt`, `/boot/firmware/cmdline.txt`에 DSI 터치 디스플레이 부팅 설정 저장
|
||||
- `public/api.php?action=dmesg`: dmesg 로그 조회
|
||||
- `public/api.php?action=reboot`: 확인 단어와 관리자 암호 재검증 후 sudo reboot 실행
|
||||
|
||||
@@ -96,10 +98,13 @@ API 응답의 `battery.enabled`는 false, `battery.removed`는 true로 내려가
|
||||
- 팬 자동 제어: 팬 시작 온도, 최대 온도, 즉시 최대 PWM 온도, RP1 온도 반영 비율, 팬 판단 온도 여유, 자동 상승/하강 PWM step
|
||||
- 보안 정책: 자동 로그인 유지 기간, remember 쿠키 Secure/SameSite, CSRF 길이, remember 토큰 길이, User-Agent/IP 접두사 검증, 로그인 실패 잠금, 재부팅 허용 여부, 재부팅 확인 문구, 명령 timeout, WiFi 조작 허용 여부
|
||||
- 화면/진단 표시: 프로세스 후보 수, 사용자 서비스 로그 줄 수, 사용자 서비스 캐시 시간, 팬 이상 이력 수
|
||||
- 터치 디스플레이: 현재 DSI 연결 상태, `800x480` 모드, framebuffer, 자동감지/수동 overlay 상태 표시와 콘솔 회전, LCD 출력, 터치 입력, `invx`/`invy`/`swapxy`, 터치 좌표 크기 저장
|
||||
- 저전압/스로틀링: 최근 판정창, 복구 중 유지 시간, 반복 발생 기준 횟수
|
||||
- 팬 이상감지: 기준 표본 수, 최신 표본 제외 수, RPM/온도 이상 감지 차이, RPM/온도 복구 차이, RPM 감시 시작 PWM, alert 유지 중 반복 기록, 반복 기록 간격, 하강 변화 원인 후보 표시 여부
|
||||
- UPS/배터리 설정: 현재 UPS 제거 상태에서는 설정 모달에서 숨겨지고, `battery.enabled=true`일 때만 배터리 예측 항목을 노출합니다.
|
||||
|
||||
디스플레이 설정은 DB가 아니라 라즈베리파이 부팅 파일에 저장합니다. 저장 전 기존 파일을 `.control-YYYYMMDD-HHMMSS.bak` 형식으로 백업하고, 변경 사항은 재부팅 후 적용됩니다. 구형 정품 7인치 Touch Display의 콘솔 회전은 `cmdline.txt`의 `video=DSI-1:800x480@60,rotate=...`를 사용하며, 터치 좌표 보정이 필요하면 `config.txt`의 `vc4-kms-dsi-7inch` overlay로 전환합니다.
|
||||
|
||||
설정 모달이 열려 있는 동안 1초 상태 갱신은 폼 DOM을 다시 만들지 않습니다. 입력 중인 값은 저장, 기본값 초기화, 닫기 전까지 유지되며, 백그라운드 snapshot은 내부 설정 payload만 갱신합니다. 단순 boolean 설정은 `켜기`/`끄기` 세그먼트로 표시합니다.
|
||||
|
||||
## 갱신 주기
|
||||
@@ -208,6 +213,7 @@ control-wifi-observe.service
|
||||
- `sync_current_battery_remaining_history()`: 잔여 시간 차트 마지막 지점을 현재 snapshot 잔여 시간과 동기화
|
||||
- `batteryRemainingTitle()`, `showBatteryTooltip()`, `updateBatteryTooltip()`: 잔여 시간 계산 근거 커스텀 팝오버 표시와 실시간 갱신
|
||||
- `setting_definitions()`, `settings_payload()`, `save_settings()`, `reset_settings()`: 설정 모달 항목 정의, 조회, 저장, 초기화
|
||||
- `touch_display_status()`, `touch_display_set_config()`: 정품 7인치 DSI 터치 디스플레이 인식 상태와 부팅 설정 조회/저장
|
||||
- `bin/wifi_observe.php`: 화면 접속 없이 `wifi_data()`를 호출해 5G/LAN 관측 세션을 선제 갱신
|
||||
- `assets/wakelock.js`: WakeLock 버튼 상태와 Screen Wake Lock API 제어
|
||||
- `customAlert()`, `customConfirm()`, `customPrompt()`: 대시보드 공통 확인/입력 dialog
|
||||
|
||||
+328
-2
@@ -3478,6 +3478,308 @@ function custom_systemd_services(): array
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function touch_display_bool(mixed $value): bool
|
||||
{
|
||||
return in_array((string)$value, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
function touch_display_file(string $kind): string
|
||||
{
|
||||
return $kind === 'cmdline' ? '/boot/firmware/cmdline.txt' : '/boot/firmware/config.txt';
|
||||
}
|
||||
|
||||
function touch_display_read_file(string $kind): string
|
||||
{
|
||||
$path = touch_display_file($kind);
|
||||
$content = @file_get_contents($path);
|
||||
return is_string($content) ? $content : '';
|
||||
}
|
||||
|
||||
function touch_display_backup_file(string $path): void
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$backup = $path . '.control-' . date('Ymd-His') . '.bak';
|
||||
if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
|
||||
@copy($path, $backup);
|
||||
return;
|
||||
}
|
||||
|
||||
sh(['/bin/cp', '-a', $path, $backup], true, 5);
|
||||
}
|
||||
|
||||
function touch_display_write_file(string $path, string $content): void
|
||||
{
|
||||
if (function_exists('posix_geteuid') && posix_geteuid() === 0) {
|
||||
if (@file_put_contents($path, $content) === false) {
|
||||
throw new RuntimeException(basename($path) . ' 저장에 실패했습니다.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$tmp = sys_get_temp_dir() . '/control-touch-display-' . bin2hex(random_bytes(6));
|
||||
if (@file_put_contents($tmp, $content) === false) {
|
||||
throw new RuntimeException('임시 설정 파일 생성에 실패했습니다.');
|
||||
}
|
||||
@chmod($tmp, 0644);
|
||||
|
||||
$result = sh(['/bin/cp', '-f', $tmp, $path], true, 5);
|
||||
@unlink($tmp);
|
||||
if ($result['code'] !== 0) {
|
||||
throw new RuntimeException(trim($result['out']) !== '' ? trim($result['out']) : basename($path) . ' 저장에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
function touch_display_active_config_lines(string $config): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
preg_split('/\R/', $config) ?: [],
|
||||
static function (string $line): bool {
|
||||
$trimmed = trim($line);
|
||||
return $trimmed !== '' && !str_starts_with($trimmed, '#');
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
function touch_display_config_flag(array $lines, string $key): bool
|
||||
{
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^' . preg_quote($key, '/') . '\s*=\s*1\s*$/', $line)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function touch_display_overlay(array $lines): array
|
||||
{
|
||||
$overlay = [
|
||||
'enabled' => false,
|
||||
'params' => [],
|
||||
];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (!preg_match('/^dtoverlay\s*=\s*vc4-kms-dsi-7inch(?<params>(?:,.*)?)\s*$/', $line, $m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$overlay['enabled'] = true;
|
||||
$params = trim((string)($m['params'] ?? ''), ',');
|
||||
foreach (array_filter(array_map('trim', explode(',', $params))) as $param) {
|
||||
if (str_contains($param, '=')) {
|
||||
[$key, $value] = array_map('trim', explode('=', $param, 2));
|
||||
$overlay['params'][$key] = $value;
|
||||
} else {
|
||||
$overlay['params'][$param] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $overlay;
|
||||
}
|
||||
|
||||
function touch_display_rotation(string $cmdline): int
|
||||
{
|
||||
if (preg_match('/(?:^|\s)video=DSI-1:\d+x\d+@\d+(?:\.\d+)?,rotate=(0|90|180|270)(?:\s|$)/', $cmdline, $m)) {
|
||||
return (int)$m[1];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function touch_display_status(): array
|
||||
{
|
||||
$connector = '/sys/class/drm/card0-DSI-1';
|
||||
$config = touch_display_read_file('config');
|
||||
$cmdline = touch_display_read_file('cmdline');
|
||||
$lines = touch_display_active_config_lines($config);
|
||||
$overlay = touch_display_overlay($lines);
|
||||
$params = $overlay['params'];
|
||||
|
||||
$statusPath = $connector . '/status';
|
||||
$modesPath = $connector . '/modes';
|
||||
$enabledPath = $connector . '/enabled';
|
||||
$dpmsPath = $connector . '/dpms';
|
||||
$modes = is_readable($modesPath)
|
||||
? array_values(array_filter(array_map('trim', file($modesPath, FILE_IGNORE_NEW_LINES) ?: [])))
|
||||
: [];
|
||||
|
||||
return [
|
||||
'connector' => 'DSI-1',
|
||||
'detected' => is_dir($connector),
|
||||
'connected' => trim((string)@file_get_contents($statusPath)) === 'connected',
|
||||
'status' => is_readable($statusPath) ? trim((string)@file_get_contents($statusPath)) : 'unknown',
|
||||
'enabled_state' => is_readable($enabledPath) ? trim((string)@file_get_contents($enabledPath)) : 'unknown',
|
||||
'dpms' => is_readable($dpmsPath) ? trim((string)@file_get_contents($dpmsPath)) : 'unknown',
|
||||
'modes' => $modes,
|
||||
'current_mode' => $modes[0] ?? '800x480',
|
||||
'framebuffer' => is_dir('/sys/class/graphics/fb0') ? 'fb0' : '-',
|
||||
'touch_input' => trim((string)shell_exec("for e in /sys/class/input/event*/device/name; do cat \"\$e\" 2>/dev/null; done | grep -i 'ft5x06' | head -n 1")) ?: '-',
|
||||
'config' => [
|
||||
'display_auto_detect' => touch_display_config_flag($lines, 'display_auto_detect'),
|
||||
'manual_overlay' => (bool)$overlay['enabled'],
|
||||
'display_enabled' => !touch_display_config_flag($lines, 'ignore_lcd'),
|
||||
'touch_enabled' => !touch_display_config_flag($lines, 'disable_touchscreen') && empty($params['disable_touch']),
|
||||
'rotation' => touch_display_rotation($cmdline),
|
||||
'touch_invx' => !empty($params['invx']) && (string)$params['invx'] !== '0',
|
||||
'touch_invy' => !empty($params['invy']) && (string)$params['invy'] !== '0',
|
||||
'touch_swapxy' => !empty($params['swapxy']) && (string)$params['swapxy'] !== '0',
|
||||
'touch_sizex' => is_numeric($params['sizex'] ?? null) ? (int)$params['sizex'] : 800,
|
||||
'touch_sizey' => is_numeric($params['sizey'] ?? null) ? (int)$params['sizey'] : 480,
|
||||
],
|
||||
'paths' => [
|
||||
'config' => touch_display_file('config'),
|
||||
'cmdline' => touch_display_file('cmdline'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function touch_display_comment_directives(string $config, array $patterns): string
|
||||
{
|
||||
$lines = preg_split('/\R/', $config) ?: [];
|
||||
$cleaned = [];
|
||||
$skipControlBlock = false;
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
if ($trimmed === '# Control touch display settings') {
|
||||
$skipControlBlock = true;
|
||||
continue;
|
||||
}
|
||||
if ($skipControlBlock) {
|
||||
if (
|
||||
$trimmed === ''
|
||||
|| preg_match('/^(display_auto_detect|ignore_lcd|disable_touchscreen)\s*=/', $trimmed)
|
||||
|| preg_match('/^dtoverlay\s*=\s*vc4-kms-dsi-7inch(?:,.*)?$/', $trimmed)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$skipControlBlock = false;
|
||||
}
|
||||
$cleaned[] = $line;
|
||||
}
|
||||
$lines = $cleaned;
|
||||
|
||||
foreach ($lines as &$line) {
|
||||
$trimmed = trim($line);
|
||||
if ($trimmed === '' || str_starts_with($trimmed, '#')) {
|
||||
continue;
|
||||
}
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $trimmed)) {
|
||||
$line = '# control disabled: ' . $line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($line);
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
function touch_display_set_config(array $input): array
|
||||
{
|
||||
$rotation = (int)($input['rotation'] ?? 0);
|
||||
if (!in_array($rotation, [0, 90, 180, 270], true)) {
|
||||
throw new RuntimeException('지원하지 않는 회전 값입니다.');
|
||||
}
|
||||
|
||||
$displayEnabled = touch_display_bool($input['display_enabled'] ?? '1');
|
||||
$touchEnabled = touch_display_bool($input['touch_enabled'] ?? '1');
|
||||
$invx = touch_display_bool($input['touch_invx'] ?? '0');
|
||||
$invy = touch_display_bool($input['touch_invy'] ?? '0');
|
||||
$swapxy = touch_display_bool($input['touch_swapxy'] ?? '0');
|
||||
$sizeX = max(100, min(4096, (int)($input['touch_sizex'] ?? 800)));
|
||||
$sizeY = max(100, min(4096, (int)($input['touch_sizey'] ?? 480)));
|
||||
$manualOverlay = touch_display_bool($input['manual_overlay'] ?? '0') || $invx || $invy || $swapxy || !$touchEnabled || $sizeX !== 800 || $sizeY !== 480;
|
||||
|
||||
$configPath = touch_display_file('config');
|
||||
$cmdlinePath = touch_display_file('cmdline');
|
||||
$config = touch_display_read_file('config');
|
||||
$cmdline = trim(touch_display_read_file('cmdline'));
|
||||
|
||||
if ($config === '' || $cmdline === '') {
|
||||
throw new RuntimeException('부팅 설정 파일을 읽을 수 없습니다.');
|
||||
}
|
||||
|
||||
$newConfig = touch_display_comment_directives($config, [
|
||||
'/^display_auto_detect\s*=/',
|
||||
'/^ignore_lcd\s*=/',
|
||||
'/^disable_touchscreen\s*=/',
|
||||
'/^dtoverlay\s*=\s*vc4-kms-dsi-7inch(?:,.*)?$/',
|
||||
]);
|
||||
|
||||
$append = [];
|
||||
$append[] = '';
|
||||
$append[] = '# Control touch display settings';
|
||||
if ($manualOverlay) {
|
||||
$params = ['vc4-kms-dsi-7inch'];
|
||||
if ($sizeX !== 800) {
|
||||
$params[] = 'sizex=' . $sizeX;
|
||||
}
|
||||
if ($sizeY !== 480) {
|
||||
$params[] = 'sizey=' . $sizeY;
|
||||
}
|
||||
if ($invx) {
|
||||
$params[] = 'invx';
|
||||
}
|
||||
if ($invy) {
|
||||
$params[] = 'invy';
|
||||
}
|
||||
if ($swapxy) {
|
||||
$params[] = 'swapxy';
|
||||
}
|
||||
if (!$touchEnabled) {
|
||||
$params[] = 'disable_touch';
|
||||
}
|
||||
$append[] = 'dtoverlay=' . implode(',', $params);
|
||||
} else {
|
||||
$append[] = 'display_auto_detect=1';
|
||||
}
|
||||
if (!$displayEnabled) {
|
||||
$append[] = 'ignore_lcd=1';
|
||||
}
|
||||
if (!$touchEnabled) {
|
||||
$append[] = 'disable_touchscreen=1';
|
||||
}
|
||||
$newConfig = rtrim($newConfig) . "\n" . implode("\n", $append) . "\n";
|
||||
|
||||
$newCmdline = preg_replace('/(?:^|\s)video=DSI-1:\d+x\d+@\d+(?:\.\d+)?,rotate=(?:0|90|180|270)(?=\s|$)/', '', $cmdline) ?? $cmdline;
|
||||
$newCmdline = trim(preg_replace('/\s+/', ' ', $newCmdline) ?? $newCmdline);
|
||||
if ($rotation !== 0) {
|
||||
$newCmdline .= ' video=DSI-1:800x480@60,rotate=' . $rotation;
|
||||
}
|
||||
$newCmdline .= "\n";
|
||||
|
||||
$changed = [];
|
||||
if ($newConfig !== $config) {
|
||||
touch_display_backup_file($configPath);
|
||||
touch_display_write_file($configPath, $newConfig);
|
||||
$changed[] = $configPath;
|
||||
}
|
||||
if ($newCmdline !== touch_display_read_file('cmdline')) {
|
||||
touch_display_backup_file($cmdlinePath);
|
||||
touch_display_write_file($cmdlinePath, $newCmdline);
|
||||
$changed[] = $cmdlinePath;
|
||||
}
|
||||
|
||||
add_fan_action(
|
||||
'touch_display_config',
|
||||
null,
|
||||
null,
|
||||
'rotation=' . $rotation . ', manual_overlay=' . ($manualOverlay ? '1' : '0') . ', display_enabled=' . ($displayEnabled ? '1' : '0') . ', touch_enabled=' . ($touchEnabled ? '1' : '0'),
|
||||
true
|
||||
);
|
||||
|
||||
return [
|
||||
'changed' => $changed,
|
||||
'reboot_required' => $changed !== [],
|
||||
'display' => touch_display_status(),
|
||||
];
|
||||
}
|
||||
|
||||
function collect_snapshot(bool $applyFan = true): array
|
||||
{
|
||||
if ($applyFan) {
|
||||
@@ -3653,9 +3955,12 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'history' => $history,
|
||||
'processes' => $processes,
|
||||
'custom_services' => custom_systemd_services(),
|
||||
'touch_display' => touch_display_status(),
|
||||
'fan_spike' => $fanSpike,
|
||||
'fan_spike_history' => fan_spike_history((int)setting_value('display.fan_notice_history_limit')),
|
||||
'settings' => settings_payload(),
|
||||
'settings' => array_merge(settings_payload(), [
|
||||
'touch_display' => touch_display_status(),
|
||||
]),
|
||||
];
|
||||
|
||||
return $snapshot;
|
||||
@@ -3689,7 +3994,9 @@ function control_api_dispatch(): void
|
||||
if ($action === 'settings') {
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'data' => settings_payload(),
|
||||
'data' => array_merge(settings_payload(), [
|
||||
'touch_display' => touch_display_status(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3717,6 +4024,25 @@ function control_api_dispatch(): void
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'touch_display_save') {
|
||||
$raw = (string)($_POST['display'] ?? '{}');
|
||||
$display = json_decode($raw, true);
|
||||
if (!is_array($display)) {
|
||||
json_out([
|
||||
'ok' => false,
|
||||
'error' => 'bad_display_settings',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$result = touch_display_set_config($display);
|
||||
$data = collect_snapshot(false);
|
||||
$data['touch_display_save'] = $result;
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'fan') {
|
||||
$mode = (string)($_POST['mode'] ?? 'auto');
|
||||
$pwm = max(0, min(255, (int)($_POST['pwm'] ?? 120)));
|
||||
|
||||
+149
-2
@@ -199,6 +199,25 @@
|
||||
settingsSaveFailed: 'settings save failed',
|
||||
settingsResetConfirm: 'Reset all Control settings to defaults?',
|
||||
settingsDefault: 'default',
|
||||
touchDisplaySettings: 'Touch Display',
|
||||
touchDisplayStatus: 'Status',
|
||||
touchDisplayDetected: 'Detected',
|
||||
touchDisplayConnected: 'Connected',
|
||||
touchDisplayMode: 'Mode',
|
||||
touchDisplayFramebuffer: 'Framebuffer',
|
||||
touchDisplayOverlay: 'Manual overlay',
|
||||
touchDisplayAutoDetect: 'Auto detect',
|
||||
touchDisplayEnabled: 'LCD output',
|
||||
touchInputEnabled: 'Touch input',
|
||||
touchRotation: 'Console rotation',
|
||||
touchInvx: 'Invert X',
|
||||
touchInvy: 'Invert Y',
|
||||
touchSwapxy: 'Swap X/Y',
|
||||
touchSizeX: 'Touch X size',
|
||||
touchSizeY: 'Touch Y size',
|
||||
touchDisplaySave: 'Save display boot settings',
|
||||
touchDisplaySaved: 'Display settings saved. Reboot is required.',
|
||||
touchDisplayNoChange: 'Display settings already match.',
|
||||
rebootButton: 'Reboot',
|
||||
dialogAlert: 'Alert',
|
||||
dialogConfirm: 'Confirm',
|
||||
@@ -351,6 +370,25 @@
|
||||
settingsSaveFailed: '설정 저장 실패',
|
||||
settingsResetConfirm: 'Control 설정을 모두 기본값으로 되돌릴까요?',
|
||||
settingsDefault: '기본값',
|
||||
touchDisplaySettings: '터치 디스플레이',
|
||||
touchDisplayStatus: '상태',
|
||||
touchDisplayDetected: '인식',
|
||||
touchDisplayConnected: '연결',
|
||||
touchDisplayMode: '모드',
|
||||
touchDisplayFramebuffer: '프레임버퍼',
|
||||
touchDisplayOverlay: '수동 overlay',
|
||||
touchDisplayAutoDetect: '자동 감지',
|
||||
touchDisplayEnabled: 'LCD 출력',
|
||||
touchInputEnabled: '터치 입력',
|
||||
touchRotation: '콘솔 회전',
|
||||
touchInvx: '터치 X 반전',
|
||||
touchInvy: '터치 Y 반전',
|
||||
touchSwapxy: '터치 X/Y 교환',
|
||||
touchSizeX: '터치 X 크기',
|
||||
touchSizeY: '터치 Y 크기',
|
||||
touchDisplaySave: '디스플레이 부팅 설정 저장',
|
||||
touchDisplaySaved: '디스플레이 설정을 저장했습니다. 재부팅이 필요합니다.',
|
||||
touchDisplayNoChange: '디스플레이 설정이 이미 동일합니다.',
|
||||
rebootButton: '재부팅',
|
||||
dialogAlert: '알림',
|
||||
dialogConfirm: '확인',
|
||||
@@ -645,6 +683,85 @@
|
||||
return `setting-${String(key).replace(/[^A-Za-z0-9_-]/g, '-')}`;
|
||||
}
|
||||
|
||||
function displayDomId(key) {
|
||||
return `touch-display-${String(key).replace(/[^A-Za-z0-9_-]/g, '-')}`;
|
||||
}
|
||||
|
||||
function boolLabel(value) {
|
||||
return value ? 'ON' : 'OFF';
|
||||
}
|
||||
|
||||
function renderTouchDisplaySettings(display) {
|
||||
if (!display || !display.config) return '';
|
||||
const config = display.config || {};
|
||||
const statusRows = [
|
||||
[t('touchDisplayDetected'), boolLabel(!!display.detected)],
|
||||
[t('touchDisplayConnected'), `${display.status || 'unknown'} / ${display.enabled_state || 'unknown'} / ${display.dpms || 'unknown'}`],
|
||||
[t('touchDisplayMode'), display.current_mode || '-'],
|
||||
[t('touchDisplayFramebuffer'), display.framebuffer || '-'],
|
||||
[t('touchDisplayAutoDetect'), boolLabel(!!config.display_auto_detect)],
|
||||
[t('touchDisplayOverlay'), boolLabel(!!config.manual_overlay)],
|
||||
];
|
||||
const checkboxField = (key, label, checked) => {
|
||||
const id = escapeHtml(displayDomId(key));
|
||||
return `
|
||||
<div class="settings-field">
|
||||
<span class="settings-label"><span>${escapeHtml(label)}</span><span></span></span>
|
||||
<span class="settings-control">
|
||||
<span class="settings-toggle">
|
||||
<input id="${id}" data-display-key="${escapeHtml(key)}" type="checkbox" value="1" autocomplete="off" ${checked ? 'checked' : ''}>
|
||||
<span class="settings-toggle-on">켜기</span>
|
||||
<span class="settings-toggle-off">끄기</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
const numberField = (key, label, value, min, max, step = 1) => `
|
||||
<div class="settings-field">
|
||||
<span class="settings-label"><span>${escapeHtml(label)}</span><span></span></span>
|
||||
<span class="settings-control">
|
||||
<input id="${escapeHtml(displayDomId(key))}" data-display-key="${escapeHtml(key)}" type="number" min="${min}" max="${max}" step="${step}" autocomplete="off" value="${escapeHtml(value)}">
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<section class="settings-group touch-display-settings">
|
||||
<h4>${escapeHtml(t('touchDisplaySettings'))}</h4>
|
||||
<div class="touch-display-status">
|
||||
${statusRows.map(([key, value]) => `
|
||||
<div><span>${escapeHtml(key)}</span><strong>${escapeHtml(value)}</strong></div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="settings-grid">
|
||||
<div class="settings-field">
|
||||
<span class="settings-label"><span>${escapeHtml(t('touchRotation'))}</span><span>deg</span></span>
|
||||
<span class="settings-control">
|
||||
<select data-display-key="rotation" class="settings-select" autocomplete="off">
|
||||
${[0, 90, 180, 270].map(value => `
|
||||
<option value="${value}" ${Number(config.rotation || 0) === value ? 'selected' : ''}>${value}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
${checkboxField('manual_overlay', t('touchDisplayOverlay'), !!config.manual_overlay)}
|
||||
${checkboxField('display_enabled', t('touchDisplayEnabled'), !!config.display_enabled)}
|
||||
${checkboxField('touch_enabled', t('touchInputEnabled'), !!config.touch_enabled)}
|
||||
${checkboxField('touch_invx', t('touchInvx'), !!config.touch_invx)}
|
||||
${checkboxField('touch_invy', t('touchInvy'), !!config.touch_invy)}
|
||||
${checkboxField('touch_swapxy', t('touchSwapxy'), !!config.touch_swapxy)}
|
||||
${numberField('touch_sizex', t('touchSizeX'), config.touch_sizex || 800, 100, 4096)}
|
||||
${numberField('touch_sizey', t('touchSizeY'), config.touch_sizey || 480, 100, 4096)}
|
||||
</div>
|
||||
<div class="settings-inline-actions">
|
||||
<button id="touchDisplaySave" class="btn" type="button">${escapeHtml(t('touchDisplaySave'))}</button>
|
||||
<span class="settings-note">/boot/firmware/config.txt, /boot/firmware/cmdline.txt</span>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSettings(payload = state.settingsPayload, options = {}) {
|
||||
if (!els.settingsBody || !payload) return;
|
||||
if (state.settingsOpen && state.settingsDirty && !options.force) return;
|
||||
@@ -718,7 +835,7 @@
|
||||
return item.default ?? '';
|
||||
};
|
||||
|
||||
els.settingsBody.innerHTML = Object.entries(byGroup).map(([group, items]) => `
|
||||
els.settingsBody.innerHTML = renderTouchDisplaySettings(payload.touch_display) + Object.entries(byGroup).map(([group, items]) => `
|
||||
<section class="settings-group">
|
||||
<h4>${escapeHtml(groups[group] || group)}</h4>
|
||||
<div class="settings-grid">
|
||||
@@ -737,7 +854,7 @@
|
||||
</div>
|
||||
</section>
|
||||
`).join('');
|
||||
els.settingsBody.querySelectorAll('[data-setting-key]').forEach(input => {
|
||||
els.settingsBody.querySelectorAll('[data-setting-key],[data-display-key]').forEach(input => {
|
||||
input.addEventListener('input', () => {
|
||||
state.settingsDirty = true;
|
||||
});
|
||||
@@ -757,6 +874,7 @@
|
||||
input.dispatchEvent(new Event('change'));
|
||||
});
|
||||
});
|
||||
$('#touchDisplaySave', els.settingsBody)?.addEventListener('click', saveTouchDisplaySettings);
|
||||
}
|
||||
|
||||
function settingByKey(key, fallback = null) {
|
||||
@@ -797,6 +915,35 @@
|
||||
return values;
|
||||
}
|
||||
|
||||
function currentTouchDisplayFormValues() {
|
||||
const values = {};
|
||||
els.settingsBody?.querySelectorAll('[data-display-key]').forEach(input => {
|
||||
values[input.dataset.displayKey] = input.type === 'checkbox'
|
||||
? (input.checked ? '1' : '0')
|
||||
: input.value;
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
async function saveTouchDisplaySettings() {
|
||||
if (!await window.customConfirm('디스플레이 부팅 설정을 저장할까요? 저장 후 재부팅해야 적용됩니다.', { title: t('touchDisplaySettings') })) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await api('touch_display_save', {
|
||||
display: JSON.stringify(currentTouchDisplayFormValues()),
|
||||
});
|
||||
state.settingsDirty = false;
|
||||
render(data);
|
||||
renderSettings(data.settings, { force: true });
|
||||
const changed = data.touch_display_save?.changed || [];
|
||||
notice(changed.length ? t('touchDisplaySaved') : t('touchDisplayNoChange'), 'success');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notice(e.message || t('settingsSaveFailed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const data = await api('settings_save', {
|
||||
|
||||
+3
-2
@@ -145,9 +145,10 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
.status-value.interactive{cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:4px}.runtime-tooltip{position:fixed;left:0;top:0;z-index:110;display:none;width:min(520px,calc(100vw - 28px));max-height:calc(100vh - 28px);overflow:hidden;padding:14px 16px;border:1px solid rgba(84,101,128,.78);border-radius:16px;background:var(--card);color:var(--text);box-shadow:0 18px 48px rgba(0,0,0,.38);font-size:13px;line-height:1.55;white-space:pre-line;word-break:keep-all;overflow-wrap:anywhere;pointer-events:none}.runtime-tooltip.signal-tooltip{width:auto;min-width:118px;max-width:220px;padding:9px 12px;border-radius:12px;font-size:12px;line-height:1.45;box-shadow:0 12px 30px rgba(0,0,0,.28)}.runtime-tooltip[data-open="1"]{display:block}.runtime-tooltip::before{content:"";position:absolute;left:18px;top:-7px;width:12px;height:12px;border-left:1px solid rgba(84,101,128,.78);border-top:1px solid rgba(84,101,128,.78);background:var(--card);transform:rotate(45deg)}html[data-theme="light"] .runtime-tooltip{background:#f8fbff;border-color:#aebed2;color:#172033;box-shadow:0 18px 44px rgba(28,43,64,.18)}html[data-theme="light"] .runtime-tooltip.signal-tooltip{box-shadow:0 12px 28px rgba(28,43,64,.16)}html[data-theme="light"] .runtime-tooltip::before{background:#f8fbff;border-color:#aebed2}
|
||||
.dialog-layer{position:fixed;inset:0;display:none;align-items:center;justify-content:center;padding:22px;background:rgba(4,7,12,.68);backdrop-filter:blur(8px);z-index:120}.dialog-layer[data-open="1"]{display:flex}.dialog-box{width:min(430px,100%);border:1px solid rgba(84,101,128,.78);border-radius:20px;background:var(--card);box-shadow:0 24px 70px rgba(0,0,0,.42);padding:20px}.dialog-box h3{margin:0 0 8px;font-size:19px;font-weight:500}.dialog-message{margin:0 0 16px;color:var(--sub);line-height:1.5;white-space:pre-wrap}.dialog-input{width:100%;height:48px;margin-bottom:14px;border-radius:14px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 14px;outline:none}.dialog-input:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.dialog-actions{display:flex;justify-content:flex-end;gap:10px}.dialog-actions .btn{min-width:84px}.dialog-actions .btn.red{background:#c62828}
|
||||
.settings-layer{align-items:stretch}.settings-box{width:min(980px,100%);max-height:calc(100vh - 44px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;padding:0;overflow:hidden}.settings-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:18px 20px;border-bottom:1px solid var(--line)}.settings-sub{margin:0;color:var(--sub);font-size:13px}.settings-body{overflow:auto;padding:16px 20px;display:grid;gap:14px}.settings-group{border:1px solid var(--line);border-radius:16px;background:var(--card2);padding:14px}.settings-group h4{margin:0 0 12px;font-size:15px}.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.settings-field{display:grid;gap:7px;min-width:0}.settings-label{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--sub);font-size:13px}.settings-label span:last-child{white-space:nowrap;color:var(--sub)}.settings-control{display:flex;align-items:center;gap:9px}.settings-control input,.settings-select{width:100%;height:42px;border-radius:12px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 12px;outline:none}.settings-control input:focus,.settings-select:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.settings-toggle{display:grid;grid-template-columns:1fr 1fr;align-items:center;width:100%;height:42px;border:1px solid var(--line);border-radius:12px;background:var(--input);padding:4px;cursor:pointer;gap:4px}.settings-toggle input{position:absolute;opacity:0;pointer-events:none}.settings-toggle span{display:grid;place-items:center;min-width:0;height:32px;border-radius:10px;color:var(--sub);font-weight:700;font-size:12px;transition:background .12s ease,color .12s ease}.settings-toggle[data-checked="1"] .settings-toggle-on,.settings-toggle[data-checked="0"] .settings-toggle-off{background:var(--blue);color:#fff}.settings-toggle[data-checked="0"] .settings-toggle-off{background:#64748b}.settings-default{font-size:12px;color:var(--sub);white-space:nowrap}.settings-actions{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--line)}
|
||||
.touch-display-status{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-bottom:14px}.touch-display-status div{display:grid;gap:4px;border:1px solid rgba(128,145,170,.28);border-radius:12px;background:var(--input);padding:10px 12px;min-width:0}.touch-display-status span{color:var(--sub);font-size:12px}.touch-display-status strong{font-size:13px;font-weight:500;word-break:break-word}.settings-inline-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:14px}.settings-note{color:var(--sub);font-size:12px;word-break:break-all}
|
||||
@media(max-width:900px){.chart-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:1100px){.layout{grid-template-columns:1fr}.resource-grid,.wifi-scan-grid{grid-template-columns:1fr}.topbar{align-items:flex-start;flex-direction:column}.topbar-right{width:100%}.topbar-right .btn{flex:1}.stat-grid{grid-template-columns:1fr}.resource-head{align-items:flex-start;flex-direction:column}.baseline-pill{text-align:left;white-space:normal}}
|
||||
@media(max-width:720px){.chart-grid{grid-template-columns:1fr}.settings-grid{grid-template-columns:1fr}.settings-head{align-items:stretch;flex-direction:column}.settings-actions{display:grid;grid-template-columns:1fr 1fr}.settings-box{max-height:calc(100vh - 24px)}}
|
||||
@media(max-width:720px){.chart-grid{grid-template-columns:1fr}.settings-grid,.touch-display-status{grid-template-columns:1fr}.settings-head{align-items:stretch;flex-direction:column}.settings-actions{display:grid;grid-template-columns:1fr 1fr}.settings-box{max-height:calc(100vh - 24px)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -363,7 +364,7 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/app.js?v=20260721_service_logs1"></script>
|
||||
<script src="/assets/app.js?v=20260723_touch_display1"></script>
|
||||
<script src="/assets/wakelock.js?v=20260607_prefs1"></script>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user