Control 설정 모달 확장
This commit is contained in:
+213
-30
@@ -72,6 +72,144 @@ function human_remaining_seconds(int $seconds): string
|
||||
return sprintf('%dm', max(1, $minutes));
|
||||
}
|
||||
|
||||
function setting_definitions(): array
|
||||
{
|
||||
return [
|
||||
'fan.auto_min_temp' => ['group' => 'fan', 'label' => '팬 시작 온도', 'type' => 'number', 'default' => 50, 'min' => 30, 'max' => 90, 'step' => 0.5, 'unit' => 'C'],
|
||||
'fan.auto_max_temp' => ['group' => 'fan', 'label' => '팬 최대 온도', 'type' => 'number', 'default' => 80, 'min' => 45, 'max' => 100, 'step' => 0.5, 'unit' => 'C'],
|
||||
'fan.full_temp' => ['group' => 'fan', 'label' => '즉시 최대 PWM 온도', 'type' => 'number', 'default' => 80, 'min' => 45, 'max' => 100, 'step' => 0.5, 'unit' => 'C'],
|
||||
'fan.ramp_up_step' => ['group' => 'fan', 'label' => '자동 상승 PWM step', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 80, 'step' => 1, 'unit' => 'PWM'],
|
||||
'fan.ramp_down_step' => ['group' => 'fan', 'label' => '자동 하강 PWM step', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 80, 'step' => 1, 'unit' => 'PWM'],
|
||||
'event.recent_window_seconds' => ['group' => 'event', 'label' => '저전압/스로틀링 최근 판정창', 'type' => 'number', 'default' => 600, 'min' => 60, 'max' => 7200, 'step' => 30, 'unit' => '초'],
|
||||
'event.recovery_seconds' => ['group' => 'event', 'label' => '복구 중 유지 시간', 'type' => 'number', 'default' => 300, 'min' => 30, 'max' => 3600, 'step' => 30, 'unit' => '초'],
|
||||
'event.repeated_count' => ['group' => 'event', 'label' => '반복 발생 기준 횟수', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 20, 'step' => 1, 'unit' => '회'],
|
||||
'notice.baseline_samples' => ['group' => 'notice', 'label' => '팬 이상 기준 표본 수', 'type' => 'number', 'default' => 180, 'min' => 30, 'max' => 600, 'step' => 10, 'unit' => '개'],
|
||||
'notice.baseline_skip_recent' => ['group' => 'notice', 'label' => '기준 계산 제외 최신 표본', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 30, 'step' => 1, 'unit' => '개'],
|
||||
'notice.rpm_delta_threshold' => ['group' => 'notice', 'label' => '팬 RPM 이상 감지 차이', 'type' => 'number', 'default' => 1000, 'min' => 100, 'max' => 5000, 'step' => 50, 'unit' => 'RPM'],
|
||||
'notice.temp_delta_threshold' => ['group' => 'notice', 'label' => '온도 이상 감지 차이', 'type' => 'number', 'default' => 3.0, 'min' => 0.5, 'max' => 15, 'step' => 0.1, 'unit' => 'C'],
|
||||
'notice.recover_rpm_delta' => ['group' => 'notice', 'label' => '팬 RPM 복구 차이', 'type' => 'number', 'default' => 500, 'min' => 50, 'max' => 3000, 'step' => 50, 'unit' => 'RPM'],
|
||||
'notice.recover_temp_delta' => ['group' => 'notice', 'label' => '온도 복구 차이', 'type' => 'number', 'default' => 1.5, 'min' => 0.2, 'max' => 10, 'step' => 0.1, 'unit' => 'C'],
|
||||
'battery.trend_hours' => ['group' => 'battery', 'label' => '단기 배터리 학습 범위', 'type' => 'number', 'default' => 24, 'min' => 1, 'max' => 48, 'step' => 1, 'unit' => '시간'],
|
||||
'battery.profile_days' => ['group' => 'battery', 'label' => '장기 배터리 학습 범위', 'type' => 'number', 'default' => 45, 'min' => 3, 'max' => 90, 'step' => 1, 'unit' => '일'],
|
||||
'battery.load_adjust_recent' => ['group' => 'battery', 'label' => '최근 추세 부하 보정 강도', 'type' => 'number', 'default' => 0.45, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
|
||||
'battery.load_adjust_regression' => ['group' => 'battery', 'label' => '강건 회귀 부하 보정 강도', 'type' => 'number', 'default' => 0.38, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
|
||||
'battery.load_adjust_voltage' => ['group' => 'battery', 'label' => '전압 후보 부하 보정 강도', 'type' => 'number', 'default' => 0.28, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
|
||||
'battery.voltage_floor_high' => ['group' => 'battery', 'label' => '전압 하한 높음', 'type' => 'number', 'default' => 3.28, 'min' => 2.8, 'max' => 3.7, 'step' => 0.01, 'unit' => 'V'],
|
||||
'battery.voltage_floor_mid' => ['group' => 'battery', 'label' => '전압 하한 중간', 'type' => 'number', 'default' => 3.18, 'min' => 2.8, 'max' => 3.7, 'step' => 0.01, 'unit' => 'V'],
|
||||
'battery.voltage_floor_low' => ['group' => 'battery', 'label' => '전압 하한 낮음', 'type' => 'number', 'default' => 3.05, 'min' => 2.8, 'max' => 3.7, 'step' => 0.01, 'unit' => 'V'],
|
||||
'battery.candidate_min_ratio' => ['group' => 'battery', 'label' => '후보 최소 속도 비율', 'type' => 'number', 'default' => 0.38, 'min' => 0.05, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
|
||||
'battery.candidate_max_ratio' => ['group' => 'battery', 'label' => '후보 최대 속도 비율', 'type' => 'number', 'default' => 2.65, 'min' => 1, 'max' => 8, 'step' => 0.05, 'unit' => 'x'],
|
||||
'battery.capacity_model_weight' => ['group' => 'battery', 'label' => '용량 모델 가중치', 'type' => 'number', 'default' => 0.18, 'min' => 0, 'max' => 1, 'step' => 0.01, 'unit' => 'x'],
|
||||
];
|
||||
}
|
||||
|
||||
function setting_rows(bool $reload = false): array
|
||||
{
|
||||
static $cache = null;
|
||||
if (!$reload && $cache !== null) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
try {
|
||||
$rows = db()->query("SELECT setting_key, setting_value FROM app_settings")->fetchAll();
|
||||
} catch (Throwable) {
|
||||
$cache = [];
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$cache = [];
|
||||
foreach ($rows as $row) {
|
||||
$cache[(string)$row['setting_key']] = (string)$row['setting_value'];
|
||||
}
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
function setting_value(string $key): int|float|string|bool|null
|
||||
{
|
||||
$defs = setting_definitions();
|
||||
if (!isset($defs[$key])) {
|
||||
return null;
|
||||
}
|
||||
$def = $defs[$key];
|
||||
$raw = setting_rows()[$key] ?? $def['default'];
|
||||
|
||||
if (($def['type'] ?? '') === 'number') {
|
||||
$value = is_numeric($raw) ? (float)$raw : (float)$def['default'];
|
||||
$value = clamp_float($value, (float)$def['min'], (float)$def['max']);
|
||||
return fmod((float)$def['step'], 1.0) === 0.0 ? (int)round($value) : $value;
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
|
||||
function settings_payload(): array
|
||||
{
|
||||
$values = setting_rows();
|
||||
$groups = [
|
||||
'fan' => '팬 자동 제어',
|
||||
'battery' => '배터리 예측',
|
||||
'event' => '저전압/스로틀링',
|
||||
'notice' => '팬 이상 감지',
|
||||
];
|
||||
|
||||
$items = [];
|
||||
foreach (setting_definitions() as $key => $def) {
|
||||
$items[] = array_merge($def, [
|
||||
'key' => $key,
|
||||
'value' => setting_value($key),
|
||||
'default' => $def['default'],
|
||||
'custom' => array_key_exists($key, $values),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'groups' => $groups,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
function save_settings(array $input): void
|
||||
{
|
||||
$defs = setting_definitions();
|
||||
$stmt = db()->prepare("
|
||||
INSERT INTO app_settings (setting_key, setting_value)
|
||||
VALUES (:setting_key, :setting_value)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
setting_value = VALUES(setting_value),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
");
|
||||
|
||||
foreach ($input as $key => $value) {
|
||||
if (!isset($defs[$key])) {
|
||||
continue;
|
||||
}
|
||||
$def = $defs[$key];
|
||||
if (($def['type'] ?? '') === 'number') {
|
||||
if (!is_numeric($value)) {
|
||||
continue;
|
||||
}
|
||||
$value = clamp_float((float)$value, (float)$def['min'], (float)$def['max']);
|
||||
}
|
||||
$stmt->execute([
|
||||
':setting_key' => $key,
|
||||
':setting_value' => (string)$value,
|
||||
]);
|
||||
}
|
||||
setting_rows(true);
|
||||
}
|
||||
|
||||
function reset_settings(): void
|
||||
{
|
||||
$keys = array_keys(setting_definitions());
|
||||
if ($keys === []) {
|
||||
return;
|
||||
}
|
||||
$placeholders = implode(',', array_fill(0, count($keys), '?'));
|
||||
db()->prepare("DELETE FROM app_settings WHERE setting_key IN ($placeholders)")->execute($keys);
|
||||
setting_rows(true);
|
||||
}
|
||||
|
||||
function dmesg_log(): array
|
||||
{
|
||||
$path = '/tmp/dmesg.log';
|
||||
@@ -120,8 +258,8 @@ function dmesg_log(): array
|
||||
function throttled_event_status(?int $flags, string $raw, int $activeBit, int $seenBit, string $statePath): array
|
||||
{
|
||||
$now = time();
|
||||
$windowSeconds = 600;
|
||||
$recoverySeconds = 300;
|
||||
$windowSeconds = (int)setting_value('event.recent_window_seconds');
|
||||
$recoverySeconds = (int)setting_value('event.recovery_seconds');
|
||||
$active = $flags !== null && (bool)($flags & $activeBit);
|
||||
$seen = $flags !== null && (bool)($flags & $seenBit);
|
||||
|
||||
@@ -245,7 +383,7 @@ function throttled_event_status(?int $flags, string $raw, int $activeBit, int $s
|
||||
|
||||
$lastClearedAt = is_numeric($state['last_cleared_at'] ?? null) ? (int)$state['last_cleared_at'] : null;
|
||||
$recovering = !$active && $lastClearedAt !== null && ($now - $lastClearedAt) <= $recoverySeconds;
|
||||
$repeated = $recentCount >= 2;
|
||||
$repeated = $recentCount >= (int)setting_value('event.repeated_count');
|
||||
$stateLabel = 'normal';
|
||||
if ($active) {
|
||||
$stateLabel = 'active';
|
||||
@@ -400,31 +538,37 @@ function cpu_temp(): float
|
||||
|
||||
function fan_target_pwm(float $temp): int
|
||||
{
|
||||
if ($temp >= 80) {
|
||||
$minTemp = (float)setting_value('fan.auto_min_temp');
|
||||
$maxTemp = (float)setting_value('fan.auto_max_temp');
|
||||
if ($maxTemp <= $minTemp) {
|
||||
$maxTemp = $minTemp + 1;
|
||||
}
|
||||
|
||||
if ($temp >= $maxTemp) {
|
||||
return 255;
|
||||
}
|
||||
|
||||
if ($temp <= 50) {
|
||||
if ($temp <= $minTemp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ratio = ($temp - 50) / 30;
|
||||
$ratio = ($temp - $minTemp) / ($maxTemp - $minTemp);
|
||||
|
||||
return max(0, min(255, (int)round($ratio * 255)));
|
||||
}
|
||||
|
||||
function fan_ramped_pwm(int $current, int $desired, float $temp): int
|
||||
{
|
||||
if ($temp >= 80) {
|
||||
if ($temp >= (float)setting_value('fan.full_temp')) {
|
||||
return $desired;
|
||||
}
|
||||
|
||||
if ($desired > $current) {
|
||||
return min($desired, $current + 2);
|
||||
return min($desired, $current + (int)setting_value('fan.ramp_up_step'));
|
||||
}
|
||||
|
||||
if ($desired < $current) {
|
||||
return max($desired, $current - 2);
|
||||
return max($desired, $current - (int)setting_value('fan.ramp_down_step'));
|
||||
}
|
||||
|
||||
return $desired;
|
||||
@@ -984,7 +1128,7 @@ function battery_trend_candidate(array $trendRows, float $currentPercent, int $w
|
||||
$windowWatts = numeric_trimmed_average(array_column($rows, 'cpu_watts'), 0.1);
|
||||
$loadFactor = 1.0;
|
||||
if ($recentWatts !== null && $recentWatts > 0 && $windowWatts !== null && $windowWatts > 0) {
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $windowWatts, 0.65, 1.75) - 1) * 0.45);
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $windowWatts, 0.65, 1.75) - 1) * (float)setting_value('battery.load_adjust_recent'));
|
||||
$rate *= $loadFactor;
|
||||
}
|
||||
|
||||
@@ -1087,7 +1231,7 @@ function battery_regression_trend_candidate(array $trendRows, float $currentPerc
|
||||
$windowWatts = numeric_trimmed_average(array_column($rows, 'cpu_watts'), 0.1);
|
||||
$loadFactor = 1.0;
|
||||
if ($recentWatts !== null && $recentWatts > 0 && $windowWatts !== null && $windowWatts > 0) {
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $windowWatts, 0.7, 1.65) - 1) * 0.38);
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $windowWatts, 0.7, 1.65) - 1) * (float)setting_value('battery.load_adjust_regression'));
|
||||
$rate *= $loadFactor;
|
||||
}
|
||||
|
||||
@@ -1121,13 +1265,13 @@ function battery_regression_trend_candidate(array $trendRows, float $currentPerc
|
||||
function battery_voltage_floor(float $currentVoltage): float
|
||||
{
|
||||
if ($currentVoltage >= 3.55) {
|
||||
return 3.28;
|
||||
return (float)setting_value('battery.voltage_floor_high');
|
||||
}
|
||||
if ($currentVoltage >= 3.35) {
|
||||
return 3.18;
|
||||
return (float)setting_value('battery.voltage_floor_mid');
|
||||
}
|
||||
|
||||
return 3.05;
|
||||
return (float)setting_value('battery.voltage_floor_low');
|
||||
}
|
||||
|
||||
function battery_voltage_trend_candidate(array $trendRows, float $currentPercent, ?float $currentVoltage, int $windowSeconds, ?float $recentWatts): ?array
|
||||
@@ -1216,7 +1360,7 @@ function battery_voltage_trend_candidate(array $trendRows, float $currentPercent
|
||||
$windowWatts = numeric_trimmed_average(array_column($rows, 'cpu_watts'), 0.1);
|
||||
$loadFactor = 1.0;
|
||||
if ($recentWatts !== null && $recentWatts > 0 && $windowWatts !== null && $windowWatts > 0) {
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $windowWatts, 0.75, 1.55) - 1) * 0.28);
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $windowWatts, 0.75, 1.55) - 1) * (float)setting_value('battery.load_adjust_voltage'));
|
||||
$seconds = (int)round($seconds / $loadFactor);
|
||||
}
|
||||
if ($seconds <= 0) {
|
||||
@@ -1371,7 +1515,7 @@ function battery_capacity_power_candidate(float $currentPercent, ?float $recentW
|
||||
'window_seconds' => null,
|
||||
'sample_count' => null,
|
||||
'confidence' => 0.34,
|
||||
'weight' => 0.18,
|
||||
'weight' => (float)setting_value('battery.capacity_model_weight'),
|
||||
'avg_watts' => round($recentWatts, 3),
|
||||
'load_factor' => 1.0,
|
||||
'source' => 'capacity_power_model',
|
||||
@@ -1491,7 +1635,7 @@ function refine_battery_candidates(array $candidates): array
|
||||
}
|
||||
|
||||
$ratio = $rate / $medianRate;
|
||||
if ($ratio < 0.38 || $ratio > 2.65) {
|
||||
if ($ratio < (float)setting_value('battery.candidate_min_ratio') || $ratio > (float)setting_value('battery.candidate_max_ratio')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1872,9 +2016,11 @@ function trimmed_average(array $values, float $fallback): float
|
||||
|
||||
function notice_rolling_baseline(array $history, float $temp, float $rpm, float $pwm, array $state = []): array
|
||||
{
|
||||
$rows = array_slice($history, -180);
|
||||
if (count($rows) > 3) {
|
||||
$rows = array_slice($rows, 0, -3);
|
||||
$sampleCount = (int)setting_value('notice.baseline_samples');
|
||||
$skipRecent = (int)setting_value('notice.baseline_skip_recent');
|
||||
$rows = array_slice($history, -$sampleCount);
|
||||
if ($skipRecent > 0 && count($rows) > $skipRecent) {
|
||||
$rows = array_slice($rows, 0, -$skipRecent);
|
||||
}
|
||||
|
||||
$temps = [];
|
||||
@@ -1973,8 +2119,13 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
|
||||
$rpmExpected = $mode !== 'off' && ($pwm >= 20 || $pwmAvg >= 20);
|
||||
|
||||
$reasons = [];
|
||||
if ($rpmExpected && abs($rpmDelta) >= 1000) $reasons[] = 'RPM ' . signed_delta_text($rpmDelta);
|
||||
if (abs($tempDelta) >= 3.0) $reasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C');
|
||||
$rpmThreshold = (float)setting_value('notice.rpm_delta_threshold');
|
||||
$tempThreshold = (float)setting_value('notice.temp_delta_threshold');
|
||||
$recoverRpmThreshold = (float)setting_value('notice.recover_rpm_delta');
|
||||
$recoverTempThreshold = (float)setting_value('notice.recover_temp_delta');
|
||||
|
||||
if ($rpmExpected && abs($rpmDelta) >= $rpmThreshold) $reasons[] = 'RPM ' . signed_delta_text($rpmDelta);
|
||||
if (abs($tempDelta) >= $tempThreshold) $reasons[] = 'TEMP ' . signed_delta_text($tempDelta, 'C');
|
||||
|
||||
$spiking = false;
|
||||
|
||||
@@ -1996,8 +2147,8 @@ function fan_spike_analysis(array $history, array $fan, array $system, array $pr
|
||||
save_system_notice_state('normal', $rollingBaseline['temp'], $rollingBaseline['rpm'], $rollingBaseline['pwm']);
|
||||
}
|
||||
} else {
|
||||
$recovered = (abs($rpmDelta) <= 500 && abs($tempDelta) <= 1.5)
|
||||
|| (abs($rollingRpmDelta) <= 500 && abs($rollingTempDelta) <= 1.5);
|
||||
$recovered = (abs($rpmDelta) <= $recoverRpmThreshold && abs($tempDelta) <= $recoverTempThreshold)
|
||||
|| (abs($rollingRpmDelta) <= $recoverRpmThreshold && abs($rollingTempDelta) <= $recoverTempThreshold);
|
||||
if ($recovered || $reasons === []) {
|
||||
$currentState = 'normal';
|
||||
save_system_notice_state('normal', $rollingBaseline['temp'], $rollingBaseline['rpm'], $rollingBaseline['pwm']);
|
||||
@@ -2351,8 +2502,8 @@ function delta_state_text(float $value): string
|
||||
|
||||
function notice_downward_only(float $tempDelta, float $rpmDelta): bool
|
||||
{
|
||||
$tempTriggered = abs($tempDelta) >= 3.0;
|
||||
$rpmTriggered = abs($rpmDelta) >= 1000;
|
||||
$tempTriggered = abs($tempDelta) >= (float)setting_value('notice.temp_delta_threshold');
|
||||
$rpmTriggered = abs($rpmDelta) >= (float)setting_value('notice.rpm_delta_threshold');
|
||||
|
||||
if (!$tempTriggered && !$rpmTriggered) {
|
||||
return false;
|
||||
@@ -2378,11 +2529,11 @@ function send_fan_spike_notify(array $spike, array $fan, array $system, array $p
|
||||
$pwmAvg = (float)($spike['pwm_avg'] ?? 0);
|
||||
$reasons = [];
|
||||
|
||||
if (abs($tempDelta) >= 3.0) {
|
||||
if (abs($tempDelta) >= (float)setting_value('notice.temp_delta_threshold')) {
|
||||
$reasons[] = '온도 평균보다 ' . ($tempDelta >= 0 ? '상승' : '하강');
|
||||
}
|
||||
|
||||
if (abs($rpmDelta) >= 1000) {
|
||||
if (abs($rpmDelta) >= (float)setting_value('notice.rpm_delta_threshold')) {
|
||||
$reasons[] = '팬RPM 평균보다 ' . ($rpmDelta >= 0 ? '상승' : '하강');
|
||||
}
|
||||
|
||||
@@ -3217,8 +3368,8 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
]);
|
||||
|
||||
$history = add_battery_remaining_history(sensor_history(240));
|
||||
$batteryTrend = battery_trend_history(24);
|
||||
$batteryProfile = battery_profile_history(45);
|
||||
$batteryTrend = battery_trend_history((int)setting_value('battery.trend_hours'));
|
||||
$batteryProfile = battery_profile_history((int)setting_value('battery.profile_days'));
|
||||
$battery['remaining'] = battery_remaining_estimate($battery, $history, $batteryTrend, $batteryProfile);
|
||||
$history = sync_current_battery_remaining_history($history, [
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
@@ -3281,6 +3432,7 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'fan_spike' => $fanSpike,
|
||||
'fan_spike_history' => fan_spike_history(100),
|
||||
'ha_notify_history' => ha_notify_log_rows(20),
|
||||
'settings' => settings_payload(),
|
||||
];
|
||||
|
||||
return $snapshot;
|
||||
@@ -3309,6 +3461,37 @@ function control_api_dispatch(): void
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'settings') {
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'data' => settings_payload(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'settings_save') {
|
||||
$raw = (string)($_POST['settings'] ?? '{}');
|
||||
$settings = json_decode($raw, true);
|
||||
if (!is_array($settings)) {
|
||||
json_out([
|
||||
'ok' => false,
|
||||
'error' => 'bad_settings',
|
||||
], 400);
|
||||
}
|
||||
save_settings($settings);
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'data' => collect_snapshot(false),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'settings_reset') {
|
||||
reset_settings();
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'data' => collect_snapshot(false),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'fan') {
|
||||
$mode = (string)($_POST['mode'] ?? 'auto');
|
||||
$pwm = max(0, min(255, (int)($_POST['pwm'] ?? 120)));
|
||||
|
||||
Reference in New Issue
Block a user