1128 lines
45 KiB
JavaScript
1128 lines
45 KiB
JavaScript
import {
|
||
announce,
|
||
apiJson,
|
||
registerWorkspace,
|
||
setActiveButton,
|
||
} from "../core.js";
|
||
import { setPreviewRefreshInterval } from "../scene-board.js";
|
||
import {
|
||
ANIMATION_PREVIEW_DEFAULT_MAX_CONCURRENT,
|
||
ANIMATION_PREVIEW_MAX_MAX_CONCURRENT,
|
||
ANIMATION_PREVIEW_MIN_MAX_CONCURRENT,
|
||
normalizeAnimationPreviewMaxConcurrent,
|
||
setAnimationPreviewMaxConcurrent,
|
||
} from "../animation-thumbnail-preview.js";
|
||
import { refreshStatus } from "./device.js";
|
||
import { setControlBusy, setControlReady, setControlUnavailable } from "../interaction-feedback.js";
|
||
import { discardWifiSettingsDrafts, loadWifiSettings } from "./wifi-settings.js";
|
||
import { discardFrpDrafts, loadFrpSettings } from "./frp-settings.js";
|
||
|
||
const orientationButtons = Array.from(document.querySelectorAll("[data-orientation]"));
|
||
const matrixRefreshRateButtons = Array.from(
|
||
document.querySelectorAll("[data-matrix-refresh-rate]"),
|
||
);
|
||
const brightness = document.getElementById("config-brightness");
|
||
const brightnessValue = document.getElementById("brightness-value");
|
||
const brightnessProtectionStatus = document.getElementById("brightness-protection-status");
|
||
const feedback = document.getElementById("settings-feedback");
|
||
const voltageCurrent = document.getElementById("voltage-current");
|
||
const voltageSensorStatus = document.getElementById("voltage-sensor-status");
|
||
const voltageCalibrationStatus = document.getElementById("voltage-calibration-status");
|
||
const voltageCalibratedAt = document.getElementById("voltage-calibrated-at");
|
||
const startCalibration = document.getElementById("start-voltage-calibration");
|
||
const resetCalibration = document.getElementById("reset-voltage-calibration");
|
||
const calibrationDialog = document.getElementById("voltage-calibration-dialog");
|
||
const calibrationReference = document.getElementById("voltage-reference-input");
|
||
const calibrationDialogFeedback = document.getElementById("voltage-calibration-dialog-feedback");
|
||
const calibrationPreview = document.getElementById("voltage-calibration-preview");
|
||
const previewCalibration = document.getElementById("preview-voltage-calibration");
|
||
const confirmCalibration = document.getElementById("confirm-voltage-calibration");
|
||
const previewRefreshInput = document.getElementById("preview-refresh-interval");
|
||
const previewRefreshWarning = document.getElementById("preview-refresh-warning");
|
||
const previewRefreshError = document.getElementById("preview-refresh-error");
|
||
const lowVoltageProtectionToggle = document.getElementById("low-voltage-protection-enabled");
|
||
const lowVoltageProtectionSwitchState = document.getElementById("low-voltage-protection-switch-state");
|
||
const lowVoltageProtectionFeedback = document.getElementById("low-voltage-protection-feedback");
|
||
const performanceModeToggle = document.getElementById("performance-mode-enabled");
|
||
const performanceModeSwitchState = document.getElementById("performance-mode-switch-state");
|
||
const performanceModeFeedback = document.getElementById("performance-mode-feedback");
|
||
const animationPreviewMaxConcurrentInput = document.getElementById("animation-preview-max-concurrent");
|
||
const animationPreviewConcurrencyFeedback = document.getElementById("animation-preview-concurrency-feedback");
|
||
const otaCurrentVersion = document.getElementById("ota-current-version");
|
||
const otaFeatureUpdatedAt = document.getElementById("ota-feature-updated-at");
|
||
const otaFileInput = document.getElementById("ota-file-input");
|
||
const otaSelectFile = document.getElementById("ota-select-file");
|
||
const otaProgressGroup = document.getElementById("ota-progress-group");
|
||
const otaProgressStage = document.getElementById("ota-progress-stage");
|
||
const otaProgressValue = document.getElementById("ota-progress-value");
|
||
const otaProgress = document.getElementById("ota-progress");
|
||
const otaFeedback = document.getElementById("ota-feedback");
|
||
const otaFailureLogActions = document.getElementById("ota-failure-log-actions");
|
||
const otaViewFailureLog = document.getElementById("ota-view-failure-log");
|
||
const otaCopyFailureLog = document.getElementById("ota-copy-failure-log");
|
||
const otaFailureLogDialog = document.getElementById("ota-failure-log-dialog");
|
||
const otaFailureLogContent = document.getElementById("ota-failure-log-content");
|
||
const otaFailureLogFeedback = document.getElementById("ota-failure-log-feedback");
|
||
const otaFailureLogClose = document.getElementById("ota-failure-log-close");
|
||
const otaFailureLogCopy = document.getElementById("ota-failure-log-copy");
|
||
let confirmedOrientation = 0;
|
||
let confirmedBrightness = 40;
|
||
let confirmedMatrixRefreshRate = 100;
|
||
let confirmedPreviewRefreshInterval = 1000;
|
||
let confirmedLowVoltageProtectionEnabled = false;
|
||
let confirmedPerformanceModeEnabled = false;
|
||
let confirmedAnimationPreviewMaxConcurrent = ANIMATION_PREVIEW_DEFAULT_MAX_CONCURRENT;
|
||
let voltageConfig = null;
|
||
let voltageStatus = null;
|
||
let lowVoltageProtectionStatus = null;
|
||
let latestEffectiveBrightness = null;
|
||
let displayTestActive = false;
|
||
let displayTestBrightness = null;
|
||
let calibrationProposal = null;
|
||
let desiredBrightness = null;
|
||
let applyingBrightness = false;
|
||
let orientationPending = false;
|
||
let matrixRefreshRatePending = false;
|
||
let previewRefreshPending = false;
|
||
let desiredPreviewRefreshInterval = null;
|
||
let savingPreviewRefreshInterval = null;
|
||
let lowVoltageProtectionPending = false;
|
||
let performanceModePending = false;
|
||
let animationPreviewConcurrencyPending = false;
|
||
let otaCurrentSoftwareVersion = null;
|
||
let otaPolling = false;
|
||
let otaFailureLogText = "";
|
||
let otaFailureDialogShownFor = null;
|
||
|
||
const LOW_VOLTAGE_PROTECTION_MODES = new Set([
|
||
"disabled",
|
||
"unavailable",
|
||
"inactive",
|
||
"limiting",
|
||
"critical",
|
||
]);
|
||
|
||
function setFeedback(text, state = "idle") {
|
||
feedback.textContent = text;
|
||
feedback.dataset.state = state;
|
||
feedback.hidden = !text;
|
||
}
|
||
|
||
function setOtaFeedback(text, state = "idle") {
|
||
otaFeedback.textContent = text;
|
||
otaFeedback.dataset.state = state;
|
||
otaFeedback.hidden = !text;
|
||
}
|
||
|
||
function showOtaProgress(percent, stage) {
|
||
const value = Math.max(0, Math.min(100, Number(percent) || 0));
|
||
otaProgressGroup.hidden = false;
|
||
otaProgress.value = value;
|
||
otaProgress.textContent = `${value}%`;
|
||
otaProgressValue.textContent = `${value}%`;
|
||
otaProgressStage.textContent = stage || "正在更新";
|
||
}
|
||
|
||
function xhrUpload(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const request = new XMLHttpRequest();
|
||
request.open("POST", `/api/ota/update?filename=${encodeURIComponent(file.name)}`);
|
||
request.setRequestHeader("Content-Type", "application/octet-stream");
|
||
request.upload.addEventListener("progress", (event) => {
|
||
if (!event.lengthComputable) return;
|
||
showOtaProgress(Math.floor(event.loaded * 100 / event.total), "正在上传更新包");
|
||
});
|
||
request.addEventListener("load", () => {
|
||
let body = {};
|
||
try { body = request.responseText ? JSON.parse(request.responseText) : {}; } catch {}
|
||
if (request.status >= 200 && request.status < 300) resolve(body);
|
||
else reject(new Error(body.detail || `上传失败(HTTP ${request.status})`));
|
||
});
|
||
request.addEventListener("error", () => reject(new Error("上传连接中断,设备尚未接受更新包")));
|
||
request.addEventListener("abort", () => reject(new Error("上传已中止")));
|
||
request.send(file);
|
||
});
|
||
}
|
||
|
||
async function fetchOtaStatus() {
|
||
const response = await fetch("/api/ota/status", { cache: "no-store" });
|
||
if (!response.ok) throw new Error(`读取 OTA 状态失败(HTTP ${response.status})`);
|
||
return response.json();
|
||
}
|
||
|
||
async function fetchOtaFailureLog() {
|
||
const response = await fetch("/api/ota/failure-log", { cache: "no-store" });
|
||
if (!response.ok) throw new Error(`读取 OTA 失败日志失败(HTTP ${response.status})`);
|
||
return response.text();
|
||
}
|
||
|
||
function setOtaFailureLogFeedback(text, state = "idle") {
|
||
otaFailureLogFeedback.textContent = text;
|
||
otaFailureLogFeedback.dataset.state = state;
|
||
otaFailureLogFeedback.hidden = !text;
|
||
}
|
||
|
||
async function ensureOtaFailureLog() {
|
||
if (!otaFailureLogText) otaFailureLogText = await fetchOtaFailureLog();
|
||
return otaFailureLogText;
|
||
}
|
||
|
||
async function showOtaFailureLog() {
|
||
try {
|
||
const text = await ensureOtaFailureLog();
|
||
otaFailureLogContent.textContent = text;
|
||
setOtaFailureLogFeedback("");
|
||
if (!otaFailureLogDialog.open) otaFailureLogDialog.showModal();
|
||
otaFailureLogContent.focus();
|
||
} catch (error) {
|
||
announce(error.message, "error");
|
||
}
|
||
}
|
||
|
||
function fallbackCopyText(text) {
|
||
const textarea = document.createElement("textarea");
|
||
textarea.value = text;
|
||
textarea.setAttribute("readonly", "");
|
||
textarea.style.position = "fixed";
|
||
textarea.style.opacity = "0";
|
||
document.body.append(textarea);
|
||
textarea.select();
|
||
const copied = document.execCommand("copy");
|
||
textarea.remove();
|
||
if (!copied) throw new Error("浏览器拒绝复制操作");
|
||
}
|
||
|
||
async function copyOtaFailureLog() {
|
||
try {
|
||
const text = await ensureOtaFailureLog();
|
||
let copied = false;
|
||
if (navigator.clipboard?.writeText) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
copied = true;
|
||
} catch {}
|
||
}
|
||
if (!copied) fallbackCopyText(text);
|
||
setOtaFailureLogFeedback("日志已复制到剪贴板。", "success");
|
||
announce("OTA 失败日志已复制", "success");
|
||
} catch (error) {
|
||
setOtaFailureLogFeedback(`复制失败:${error.message}`, "error");
|
||
announce(`复制 OTA 失败日志失败:${error.message}`, "error");
|
||
}
|
||
}
|
||
|
||
function formatBeijingFeatureUpdatedAt(value) {
|
||
const date = new Date(value);
|
||
if (!value || Number.isNaN(date.getTime())) return "读取失败";
|
||
const parts = Object.fromEntries(
|
||
new Intl.DateTimeFormat("zh-CN", {
|
||
timeZone: "Asia/Shanghai",
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
hourCycle: "h23",
|
||
}).formatToParts(date).map(({ type, value: part }) => [type, part]),
|
||
);
|
||
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}(北京时间)`;
|
||
}
|
||
|
||
function applyOtaStatus(status) {
|
||
const current = status?.current_version;
|
||
if (current) {
|
||
otaCurrentVersion.textContent = current;
|
||
if (otaCurrentSoftwareVersion && current !== otaCurrentSoftwareVersion) {
|
||
window.location.reload();
|
||
return;
|
||
}
|
||
otaCurrentSoftwareVersion = current;
|
||
}
|
||
otaFeatureUpdatedAt.textContent = formatBeijingFeatureUpdatedAt(status?.feature_updated_at);
|
||
const job = status?.job;
|
||
if (status?.active && job) {
|
||
otaFailureLogActions.hidden = true;
|
||
otaFailureLogText = "";
|
||
showOtaProgress(job.percent, job.message || job.phase);
|
||
setControlBusy(otaSelectFile, { label: "正在更新…" });
|
||
setOtaFeedback("更新已交给设备后台处理;网页短暂断开属于正常现象。", "pending");
|
||
} else {
|
||
setControlReady(otaSelectFile);
|
||
if (job?.phase === "failed") {
|
||
showOtaProgress(0, "更新失败");
|
||
setOtaFeedback(job.error || "更新失败,设备已恢复原版本。", "error");
|
||
otaFailureLogActions.hidden = !status.failure_log_available;
|
||
const failureKey = `${job.id || "unknown"}:${job.finished_at || ""}`;
|
||
if (status.failure_log_available && otaFailureDialogShownFor !== failureKey) {
|
||
otaFailureDialogShownFor = failureKey;
|
||
void showOtaFailureLog();
|
||
}
|
||
} else if (job?.phase === "complete") {
|
||
otaFailureLogActions.hidden = true;
|
||
otaFailureLogText = "";
|
||
otaProgressGroup.hidden = true;
|
||
setOtaFeedback("");
|
||
}
|
||
if (job?.phase !== "failed") {
|
||
const recovery = status?.kernel_recovery;
|
||
if (recovery?.state === "pending") {
|
||
setOtaFeedback("应用更新完成;正在进行一次候选内核恢复重启…", "pending");
|
||
} else if (recovery?.state === "succeeded") {
|
||
setOtaFeedback("候选内核与 CPU 调频策略已恢复。", "ready");
|
||
} else if (recovery?.state === "failed") {
|
||
setOtaFeedback(`内核恢复失败:${recovery.reason || "请查看设备诊断"}`, "error");
|
||
} else if (recovery?.state === "requires_package") {
|
||
setOtaFeedback("应用已更新;内核材料缺失或损坏,需要专用修复包。", "error");
|
||
} else if (recovery?.state === "ready") {
|
||
setOtaFeedback("当前设备运行原内核,性能模式不可用;下次 OTA 将尝试轻量恢复。", "error");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
otaViewFailureLog.addEventListener("click", () => void showOtaFailureLog());
|
||
otaCopyFailureLog.addEventListener("click", () => void copyOtaFailureLog());
|
||
otaFailureLogCopy.addEventListener("click", () => void copyOtaFailureLog());
|
||
otaFailureLogClose.addEventListener("click", () => otaFailureLogDialog.close());
|
||
|
||
async function pollOtaUntilFinished() {
|
||
if (otaPolling) return;
|
||
otaPolling = true;
|
||
let disconnected = false;
|
||
try {
|
||
while (true) {
|
||
try {
|
||
const status = await fetchOtaStatus();
|
||
if (disconnected) setOtaFeedback("设备服务已恢复,正在确认更新结果…", "pending");
|
||
applyOtaStatus(status);
|
||
if (!status.active && status.kernel_recovery?.state !== "pending") return;
|
||
} catch {
|
||
disconnected = true;
|
||
setOtaFeedback("设备正在切换软件版本,等待服务恢复…", "pending");
|
||
}
|
||
await new Promise((resolve) => window.setTimeout(resolve, 1000));
|
||
}
|
||
} finally {
|
||
otaPolling = false;
|
||
}
|
||
}
|
||
|
||
async function loadOtaStatus() {
|
||
try {
|
||
const status = await fetchOtaStatus();
|
||
applyOtaStatus(status);
|
||
if (status.active || status.kernel_recovery?.state === "pending") pollOtaUntilFinished();
|
||
} catch (error) {
|
||
otaCurrentVersion.textContent = "读取失败";
|
||
otaFeatureUpdatedAt.textContent = "读取失败";
|
||
setOtaFeedback(error.message, "error");
|
||
}
|
||
}
|
||
|
||
otaSelectFile.addEventListener("click", () => otaFileInput.click());
|
||
otaFileInput.addEventListener("change", async () => {
|
||
const file = otaFileInput.files?.[0];
|
||
if (!file) return;
|
||
setControlBusy(otaSelectFile, { label: "正在上传…", message: "开始上传完整 OTA 更新包" });
|
||
setOtaFeedback("正在上传;上传完成并通过校验后将自动开始安装。", "pending");
|
||
showOtaProgress(0, "正在上传更新包");
|
||
try {
|
||
await xhrUpload(file);
|
||
setOtaFeedback("更新包已接受,安装将在设备后台继续。", "success");
|
||
await pollOtaUntilFinished();
|
||
} catch (error) {
|
||
setControlReady(otaSelectFile);
|
||
setOtaFeedback(error.message, "error");
|
||
} finally {
|
||
otaFileInput.value = "";
|
||
}
|
||
});
|
||
|
||
function showOrientation(value) {
|
||
setActiveButton(
|
||
orientationButtons,
|
||
orientationButtons.find((button) => Number(button.dataset.orientation) === Number(value)),
|
||
);
|
||
}
|
||
|
||
function showBrightness(value) {
|
||
brightness.value = String(value);
|
||
brightnessValue.textContent = `${value}%`;
|
||
}
|
||
|
||
function showMatrixRefreshRate(value) {
|
||
setActiveButton(
|
||
matrixRefreshRateButtons,
|
||
matrixRefreshRateButtons.find(
|
||
(button) => Number(button.dataset.matrixRefreshRate) === Number(value),
|
||
),
|
||
);
|
||
}
|
||
|
||
function finitePercent(value) {
|
||
if (value === null || value === undefined || value === "") return null;
|
||
const number = Number(value);
|
||
if (!Number.isFinite(number)) return null;
|
||
return Math.max(1, Math.min(100, Math.floor(number)));
|
||
}
|
||
|
||
function lowVoltageProtectionMode() {
|
||
const mode = lowVoltageProtectionStatus?.mode;
|
||
return LOW_VOLTAGE_PROTECTION_MODES.has(mode) ? mode : "unavailable";
|
||
}
|
||
|
||
function estimatedEffectiveBrightness(userBrightness) {
|
||
const user = finitePercent(userBrightness);
|
||
if (user === null) return null;
|
||
const mode = lowVoltageProtectionMode();
|
||
if (mode === "critical") return 35;
|
||
if (mode === "limiting") {
|
||
const limit = finitePercent(lowVoltageProtectionStatus?.brightness_limit_percent);
|
||
return limit === null ? user : Math.min(user, limit);
|
||
}
|
||
return user;
|
||
}
|
||
|
||
function showBrightnessProtectionStatus(userBrightness, effectiveBrightness = undefined) {
|
||
const mode = lowVoltageProtectionMode();
|
||
brightnessProtectionStatus.dataset.mode = mode;
|
||
if (mode !== "limiting" && mode !== "critical") {
|
||
brightnessProtectionStatus.hidden = true;
|
||
if (brightnessProtectionStatus.textContent) brightnessProtectionStatus.textContent = "";
|
||
return;
|
||
}
|
||
|
||
const user = finitePercent(userBrightness) ?? confirmedBrightness;
|
||
const reportedEffective = finitePercent(effectiveBrightness);
|
||
const effective = reportedEffective ?? estimatedEffectiveBrightness(user);
|
||
const staleSuffix = lowVoltageProtectionStatus?.reading_stale === true
|
||
? " 电压读数异常,正在保持上次保护。"
|
||
: "";
|
||
let text;
|
||
if (mode === "critical") {
|
||
text = `用户画面已禁用,低电图标固定以 35% 亮度显示。用户设置 ${user}% 会保留并在电压恢复后生效。${staleSuffix}`;
|
||
} else {
|
||
const limit = finitePercent(lowVoltageProtectionStatus?.brightness_limit_percent);
|
||
const limitText = limit === null ? "-" : String(limit);
|
||
const effectiveText = effective === null ? "-" : String(effective);
|
||
text = `当前允许的最高亮度:${limitText}%。用户设置:${user}%;当前实际亮度:${effectiveText}%。${staleSuffix}`;
|
||
}
|
||
if (brightnessProtectionStatus.textContent !== text) {
|
||
brightnessProtectionStatus.textContent = text;
|
||
}
|
||
brightnessProtectionStatus.hidden = false;
|
||
}
|
||
|
||
function showLowVoltageProtectionSwitch(enabled) {
|
||
const checked = enabled === true;
|
||
lowVoltageProtectionToggle.checked = checked;
|
||
lowVoltageProtectionSwitchState.textContent = checked ? "开启" : "关闭";
|
||
}
|
||
|
||
function setLowVoltageProtectionFeedback(text, state = "idle") {
|
||
lowVoltageProtectionFeedback.textContent = text;
|
||
lowVoltageProtectionFeedback.dataset.state = state;
|
||
lowVoltageProtectionFeedback.hidden = !text;
|
||
}
|
||
|
||
function syncLowVoltageProtectionEnabled(value) {
|
||
if (typeof value !== "boolean") return false;
|
||
confirmedLowVoltageProtectionEnabled = value;
|
||
showLowVoltageProtectionSwitch(value);
|
||
if (!lowVoltageProtectionPending) setControlReady(lowVoltageProtectionToggle);
|
||
return true;
|
||
}
|
||
|
||
function showPerformanceModeSwitch(enabled) {
|
||
const checked = enabled === true;
|
||
performanceModeToggle.checked = checked;
|
||
performanceModeSwitchState.textContent = checked ? "开启" : "关闭";
|
||
}
|
||
|
||
function setPerformanceModeFeedback(text, state = "idle") {
|
||
performanceModeFeedback.textContent = text;
|
||
performanceModeFeedback.dataset.state = state;
|
||
performanceModeFeedback.hidden = !text;
|
||
}
|
||
|
||
function syncPerformanceModeStatus(status) {
|
||
if (!status || typeof status !== "object") {
|
||
setControlUnavailable(performanceModeToggle, "CPU 调频状态不可用,请稍后重试。");
|
||
setPerformanceModeFeedback("CPU 调频状态不可用", "error");
|
||
return;
|
||
}
|
||
if (!performanceModePending && typeof status.requested === "boolean") {
|
||
confirmedPerformanceModeEnabled = status.requested === true && status.effective === true;
|
||
showPerformanceModeSwitch(confirmedPerformanceModeEnabled);
|
||
}
|
||
if (performanceModePending) return;
|
||
if (status.available === true) {
|
||
setControlReady(performanceModeToggle);
|
||
if (status.requested === true && status.effective !== true) {
|
||
setPerformanceModeFeedback(
|
||
`性能模式未实际生效${status.last_error ? `:${status.last_error}` : ""}`,
|
||
"error",
|
||
);
|
||
} else {
|
||
setPerformanceModeFeedback(
|
||
status.effective === true ? "性能模式已开启并实际生效" : "性能模式已关闭",
|
||
"ready",
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
const reason = status.last_error
|
||
? `CPU 调频策略不可用:${status.last_error}`
|
||
: "CPU 调频策略不可用;系统修复完成前不能开启性能模式。";
|
||
setControlUnavailable(performanceModeToggle, reason);
|
||
setPerformanceModeFeedback(reason, "error");
|
||
}
|
||
|
||
function brightnessFeedbackText(stage, userBrightness) {
|
||
const user = finitePercent(userBrightness) ?? confirmedBrightness;
|
||
const mode = lowVoltageProtectionMode();
|
||
if (displayTestActive && mode !== "critical") {
|
||
const test = finitePercent(displayTestBrightness);
|
||
const suffix = test === null ? "测试亮度" : `测试亮度 ${test}%`;
|
||
return stage === "pending"
|
||
? `正在保存用户亮度 ${user}%;当前仍由${suffix}覆盖…`
|
||
: `用户亮度 ${user}% 已保存,但当前由${suffix}覆盖`;
|
||
}
|
||
if (mode !== "limiting" && mode !== "critical") {
|
||
return stage === "pending"
|
||
? `正在应用并保存用户亮度 ${user}%…`
|
||
: `用户亮度 ${user}% 已生效并保存`;
|
||
}
|
||
const effective = estimatedEffectiveBrightness(user);
|
||
return stage === "pending"
|
||
? `正在应用并保存用户亮度 ${user}%;当前实际亮度 ${effective}%…`
|
||
: `用户亮度 ${user}% 已保存;当前实际亮度 ${effective}%`;
|
||
}
|
||
|
||
function showPreviewRefreshWarning(value) {
|
||
previewRefreshWarning.hidden = !(Number.isInteger(value) && value < 100);
|
||
}
|
||
|
||
function showPreviewRefreshError(message = "") {
|
||
previewRefreshError.textContent = message;
|
||
previewRefreshError.hidden = !message;
|
||
}
|
||
|
||
function showPreviewRefreshInterval(value) {
|
||
previewRefreshInput.value = String(value);
|
||
showPreviewRefreshWarning(Number(value));
|
||
showPreviewRefreshError();
|
||
}
|
||
|
||
function showAnimationPreviewMaxConcurrent(value) {
|
||
animationPreviewMaxConcurrentInput.value = String(value);
|
||
}
|
||
|
||
function setAnimationPreviewConcurrencyFeedback(message = "", state = "idle") {
|
||
animationPreviewConcurrencyFeedback.textContent = message;
|
||
animationPreviewConcurrencyFeedback.dataset.state = state;
|
||
animationPreviewConcurrencyFeedback.hidden = !message;
|
||
}
|
||
|
||
function voltageStatusText(status) {
|
||
if (status === "ok") return "正常";
|
||
if (status === "disconnected") return "传感器断开";
|
||
if (status === "error") return "读取异常";
|
||
return "正在读取";
|
||
}
|
||
|
||
function showVoltageDetails() {
|
||
const status = voltageStatus?.status || "starting";
|
||
voltageCurrent.textContent = status === "ok" && Number.isFinite(Number(voltageStatus.volts))
|
||
? `${Number(voltageStatus.volts).toFixed(2)} V`
|
||
: "-";
|
||
voltageSensorStatus.textContent = voltageStatusText(status);
|
||
const calibratedAt = voltageConfig?.voltage_calibrated_at;
|
||
voltageCalibrationStatus.textContent = calibratedAt ? "已校准" : "未校准(标称值)";
|
||
voltageCalibratedAt.textContent = calibratedAt
|
||
? new Date(calibratedAt).toLocaleString("zh-CN")
|
||
: "-";
|
||
if (status === "ok") setControlReady(startCalibration);
|
||
else if (status === "disconnected") setControlUnavailable(startCalibration, "电压传感器已断开,请连接传感器并等待状态恢复后再校准。");
|
||
else if (status === "error") setControlUnavailable(startCalibration, "电压读取异常,请检查传感器和接线后再校准。");
|
||
else setControlUnavailable(startCalibration, "正在读取电压传感器状态,请稍候。");
|
||
if (resetCalibration.dataset.controlState !== "busy") {
|
||
if (calibratedAt) setControlReady(resetCalibration);
|
||
else setControlUnavailable(resetCalibration, "当前没有已保存的校准,无需恢复标称值。");
|
||
}
|
||
}
|
||
|
||
function resetCalibrationDialog() {
|
||
calibrationProposal = null;
|
||
calibrationPreview.hidden = true;
|
||
setControlUnavailable(confirmCalibration, "请先完成“采样并预览”,再保存校准。");
|
||
setControlReady(previewCalibration);
|
||
calibrationReference.disabled = false;
|
||
calibrationDialogFeedback.textContent = "点击“采样并预览”,约需 1 秒。";
|
||
}
|
||
|
||
function showCalibrationProposal(proposal) {
|
||
document.getElementById("calibration-reference-result").textContent = `${Number(proposal.reference_volts).toFixed(3)} V`;
|
||
document.getElementById("calibration-raw-result").textContent = `${Number(proposal.uncalibrated_volts).toFixed(3)} V`;
|
||
document.getElementById("calibration-current-factor").textContent = Number(proposal.current_factor).toFixed(6);
|
||
document.getElementById("calibration-proposed-factor").textContent = Number(proposal.proposed_factor).toFixed(6);
|
||
document.getElementById("calibration-projected-result").textContent = `${Number(proposal.projected_volts).toFixed(3)} V`;
|
||
calibrationPreview.hidden = false;
|
||
}
|
||
|
||
async function loadSettings() {
|
||
loadWifiSettings();
|
||
loadFrpSettings();
|
||
try {
|
||
const config = await apiJson("/api/config");
|
||
confirmedOrientation = Number(config.orientation);
|
||
confirmedBrightness = Number(config.brightness);
|
||
confirmedMatrixRefreshRate = Number(config.matrix_refresh_rate_limit_hz);
|
||
confirmedPreviewRefreshInterval = Number(config.preview_refresh_interval_ms);
|
||
confirmedPerformanceModeEnabled = config.performance_mode_enabled === true;
|
||
confirmedAnimationPreviewMaxConcurrent = normalizeAnimationPreviewMaxConcurrent(
|
||
config.animation_preview_max_concurrent,
|
||
);
|
||
setAnimationPreviewMaxConcurrent(confirmedAnimationPreviewMaxConcurrent);
|
||
voltageConfig = config;
|
||
syncLowVoltageProtectionEnabled(config.low_voltage_protection_enabled);
|
||
showPerformanceModeSwitch(confirmedPerformanceModeEnabled);
|
||
showAnimationPreviewMaxConcurrent(confirmedAnimationPreviewMaxConcurrent);
|
||
setAnimationPreviewConcurrencyFeedback();
|
||
showOrientation(confirmedOrientation);
|
||
showBrightness(confirmedBrightness);
|
||
showMatrixRefreshRate(confirmedMatrixRefreshRate);
|
||
showBrightnessProtectionStatus(confirmedBrightness, latestEffectiveBrightness);
|
||
showPreviewRefreshInterval(confirmedPreviewRefreshInterval);
|
||
setPreviewRefreshInterval(confirmedPreviewRefreshInterval);
|
||
showVoltageDetails();
|
||
setFeedback("");
|
||
} catch (error) {
|
||
setFeedback(`设置读取失败:${error.message}`, "error");
|
||
announce(`设置读取失败:${error.message}`, true);
|
||
}
|
||
}
|
||
|
||
loadOtaStatus();
|
||
|
||
startCalibration.addEventListener("click", () => {
|
||
resetCalibrationDialog();
|
||
calibrationDialog.showModal();
|
||
calibrationReference.focus();
|
||
calibrationReference.select();
|
||
});
|
||
|
||
previewCalibration.addEventListener("click", async () => {
|
||
const referenceVolts = Number(calibrationReference.value);
|
||
if (!Number.isFinite(referenceVolts) || referenceVolts < 4.5 || referenceVolts > 5.5) {
|
||
calibrationDialogFeedback.textContent = "请输入 4.500V 到 5.500V 之间的万用表读数。";
|
||
return;
|
||
}
|
||
calibrationProposal = null;
|
||
calibrationPreview.hidden = true;
|
||
setControlUnavailable(confirmCalibration, "正在生成新的校准预览,请稍候。");
|
||
setControlBusy(previewCalibration, { label: "正在采样…", message: "正在采集 15 个新样本…" });
|
||
calibrationReference.disabled = true;
|
||
calibrationDialogFeedback.textContent = "正在采集 15 个新样本,请保持供电和表笔稳定…";
|
||
try {
|
||
const proposal = await apiJson("/api/power/voltage/calibration/preview", {
|
||
method: "POST",
|
||
body: JSON.stringify({ reference_volts: referenceVolts }),
|
||
});
|
||
calibrationProposal = proposal;
|
||
showCalibrationProposal(proposal);
|
||
setControlReady(confirmCalibration);
|
||
calibrationDialogFeedback.textContent = "请核对预览结果;确认后才会写入本机校准。";
|
||
} catch (error) {
|
||
calibrationDialogFeedback.textContent = `校准预览失败:${error.message}`;
|
||
setControlUnavailable(confirmCalibration, "请先完成“采样并预览”,再保存校准。");
|
||
} finally {
|
||
setControlReady(previewCalibration);
|
||
calibrationReference.disabled = false;
|
||
}
|
||
});
|
||
|
||
confirmCalibration.addEventListener("click", async () => {
|
||
if (!calibrationProposal) return;
|
||
setControlBusy(confirmCalibration, { label: "正在保存…", message: "正在保存校准并重新采样…" });
|
||
setControlBusy(previewCalibration);
|
||
calibrationDialogFeedback.textContent = "正在保存并重新采样…";
|
||
try {
|
||
const result = await apiJson("/api/power/voltage/calibration/confirm", {
|
||
method: "POST",
|
||
body: JSON.stringify({ proposal_id: calibrationProposal.proposal_id }),
|
||
});
|
||
voltageConfig = result.config;
|
||
voltageStatus = result.screen_input_voltage;
|
||
syncLowVoltageProtectionEnabled(result.config?.low_voltage_protection_enabled);
|
||
showVoltageDetails();
|
||
calibrationDialog.close();
|
||
setFeedback("电压传感器已完成本机校准", "success");
|
||
announce("电压传感器校准已保存");
|
||
await refreshStatus();
|
||
} catch (error) {
|
||
calibrationDialogFeedback.textContent = `保存校准失败:${error.message}`;
|
||
} finally {
|
||
setControlReady(confirmCalibration);
|
||
setControlReady(previewCalibration);
|
||
}
|
||
});
|
||
|
||
resetCalibration.addEventListener("click", async () => {
|
||
const confirmed = window.confirm("确认恢复标称校准系数 1.0?更换 ADC 或测量接线后应执行此操作并重新校准。");
|
||
if (!confirmed) return;
|
||
setControlBusy(resetCalibration, { label: "正在恢复…", message: "正在恢复标称校准…" });
|
||
setFeedback("正在恢复标称校准…", "pending");
|
||
try {
|
||
const result = await apiJson("/api/power/voltage/calibration/reset", {
|
||
method: "POST",
|
||
body: "{}",
|
||
});
|
||
voltageConfig = result.config;
|
||
voltageStatus = result.screen_input_voltage;
|
||
syncLowVoltageProtectionEnabled(result.config?.low_voltage_protection_enabled);
|
||
showVoltageDetails();
|
||
setFeedback("已恢复标称校准系数 1.0", "success");
|
||
announce("电压传感器已恢复标称值");
|
||
await refreshStatus();
|
||
} catch (error) {
|
||
setFeedback(`恢复标称值失败:${error.message}`, "error");
|
||
announce(`恢复标称值失败:${error.message}`, true);
|
||
} finally {
|
||
setControlReady(resetCalibration);
|
||
showVoltageDetails();
|
||
}
|
||
});
|
||
|
||
lowVoltageProtectionToggle.addEventListener("change", async () => {
|
||
if (lowVoltageProtectionPending) {
|
||
showLowVoltageProtectionSwitch(confirmedLowVoltageProtectionEnabled);
|
||
return;
|
||
}
|
||
|
||
const requested = lowVoltageProtectionToggle.checked === true;
|
||
const previous = confirmedLowVoltageProtectionEnabled;
|
||
lowVoltageProtectionPending = true;
|
||
setControlBusy(lowVoltageProtectionToggle);
|
||
lowVoltageProtectionSwitchState.textContent = requested ? "正在开启" : "正在关闭";
|
||
setLowVoltageProtectionFeedback(
|
||
requested ? "正在开启低电压亮度保护…" : "正在关闭低电压亮度保护…",
|
||
"pending",
|
||
);
|
||
|
||
let saved = false;
|
||
try {
|
||
const config = await apiJson("/api/config", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ low_voltage_protection_enabled: requested }),
|
||
});
|
||
if (typeof config.low_voltage_protection_enabled !== "boolean") {
|
||
throw new Error("服务器返回了无效的保护开关状态");
|
||
}
|
||
voltageConfig = config;
|
||
confirmedLowVoltageProtectionEnabled = config.low_voltage_protection_enabled;
|
||
showLowVoltageProtectionSwitch(confirmedLowVoltageProtectionEnabled);
|
||
setLowVoltageProtectionFeedback(
|
||
confirmedLowVoltageProtectionEnabled
|
||
? "低电压亮度保护已开启"
|
||
: "低电压亮度保护已关闭",
|
||
"success",
|
||
);
|
||
announce(confirmedLowVoltageProtectionEnabled
|
||
? "低电压亮度保护已开启"
|
||
: "低电压亮度保护已关闭");
|
||
saved = true;
|
||
} catch (error) {
|
||
confirmedLowVoltageProtectionEnabled = previous;
|
||
showLowVoltageProtectionSwitch(previous);
|
||
setLowVoltageProtectionFeedback(`低电压亮度保护保存失败:${error.message}`, "error");
|
||
announce(`低电压亮度保护保存失败:${error.message}`, true);
|
||
} finally {
|
||
lowVoltageProtectionPending = false;
|
||
setControlReady(lowVoltageProtectionToggle);
|
||
}
|
||
|
||
if (saved) {
|
||
try {
|
||
await refreshStatus();
|
||
} catch (error) {
|
||
setLowVoltageProtectionFeedback(
|
||
`保护开关已保存,但状态刷新失败:${error.message}`,
|
||
"error",
|
||
);
|
||
}
|
||
}
|
||
});
|
||
|
||
performanceModeToggle.addEventListener("change", async () => {
|
||
if (performanceModePending) {
|
||
showPerformanceModeSwitch(confirmedPerformanceModeEnabled);
|
||
return;
|
||
}
|
||
const requested = performanceModeToggle.checked === true;
|
||
const previous = confirmedPerformanceModeEnabled;
|
||
performanceModePending = true;
|
||
setControlBusy(performanceModeToggle);
|
||
performanceModeSwitchState.textContent = requested ? "正在开启" : "正在关闭";
|
||
setPerformanceModeFeedback(
|
||
requested ? "正在应用 performance governor…" : "正在恢复原 CPU governor…",
|
||
"pending",
|
||
);
|
||
try {
|
||
const config = await apiJson("/api/config", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ performance_mode_enabled: requested }),
|
||
});
|
||
if (typeof config.performance_mode_enabled !== "boolean") {
|
||
throw new Error("服务器返回了无效的性能模式状态");
|
||
}
|
||
confirmedPerformanceModeEnabled = config.performance_mode_enabled;
|
||
showPerformanceModeSwitch(confirmedPerformanceModeEnabled);
|
||
setPerformanceModeFeedback(
|
||
confirmedPerformanceModeEnabled ? "性能模式已开启" : "性能模式已关闭并恢复原 governor",
|
||
"success",
|
||
);
|
||
announce(confirmedPerformanceModeEnabled ? "性能模式已开启" : "性能模式已关闭");
|
||
} catch (error) {
|
||
confirmedPerformanceModeEnabled = previous;
|
||
showPerformanceModeSwitch(previous);
|
||
setPerformanceModeFeedback(`性能模式切换失败:${error.message}`, "error");
|
||
announce(`性能模式切换失败:${error.message}`, true);
|
||
} finally {
|
||
performanceModePending = false;
|
||
setControlReady(performanceModeToggle);
|
||
}
|
||
try {
|
||
await refreshStatus();
|
||
} catch (error) {
|
||
setPerformanceModeFeedback(`性能模式状态刷新失败:${error.message}`, "error");
|
||
}
|
||
});
|
||
|
||
async function saveAnimationPreviewMaxConcurrent(requested) {
|
||
if (animationPreviewConcurrencyPending) return;
|
||
const previous = confirmedAnimationPreviewMaxConcurrent;
|
||
animationPreviewConcurrencyPending = true;
|
||
setControlBusy(animationPreviewMaxConcurrentInput);
|
||
setAnimationPreviewConcurrencyFeedback(`正在保存同时播放数量 ${requested}…`, "pending");
|
||
try {
|
||
const config = await apiJson("/api/config", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ animation_preview_max_concurrent: requested }),
|
||
});
|
||
const saved = normalizeAnimationPreviewMaxConcurrent(
|
||
config.animation_preview_max_concurrent,
|
||
);
|
||
confirmedAnimationPreviewMaxConcurrent = saved;
|
||
setAnimationPreviewMaxConcurrent(saved);
|
||
showAnimationPreviewMaxConcurrent(saved);
|
||
setAnimationPreviewConcurrencyFeedback(`同时播放数量 ${saved} 已保存`, "success");
|
||
announce(`模板界面同时播放的动图数量已设置为 ${saved}`);
|
||
} catch (error) {
|
||
confirmedAnimationPreviewMaxConcurrent = previous;
|
||
setAnimationPreviewMaxConcurrent(previous);
|
||
showAnimationPreviewMaxConcurrent(previous);
|
||
setAnimationPreviewConcurrencyFeedback(`同时播放数量保存失败:${error.message}`, "error");
|
||
announce(`同时播放数量保存失败:${error.message}`, true);
|
||
} finally {
|
||
animationPreviewConcurrencyPending = false;
|
||
setControlReady(animationPreviewMaxConcurrentInput);
|
||
}
|
||
}
|
||
|
||
function commitAnimationPreviewMaxConcurrent() {
|
||
if (animationPreviewConcurrencyPending) return;
|
||
const raw = animationPreviewMaxConcurrentInput.value.trim();
|
||
const requested = Number(raw);
|
||
try {
|
||
if (raw === "" || !/^\d+$/.test(raw)) throw new Error();
|
||
normalizeAnimationPreviewMaxConcurrent(requested);
|
||
} catch {
|
||
showAnimationPreviewMaxConcurrent(confirmedAnimationPreviewMaxConcurrent);
|
||
setAnimationPreviewConcurrencyFeedback(
|
||
`请输入 ${ANIMATION_PREVIEW_MIN_MAX_CONCURRENT} 到 ${ANIMATION_PREVIEW_MAX_MAX_CONCURRENT} 之间的整数。`,
|
||
"error",
|
||
);
|
||
return;
|
||
}
|
||
if (requested === confirmedAnimationPreviewMaxConcurrent) {
|
||
setAnimationPreviewConcurrencyFeedback();
|
||
return;
|
||
}
|
||
void saveAnimationPreviewMaxConcurrent(requested);
|
||
}
|
||
|
||
animationPreviewMaxConcurrentInput.addEventListener("input", () => {
|
||
setAnimationPreviewConcurrencyFeedback();
|
||
});
|
||
animationPreviewMaxConcurrentInput.addEventListener("change", commitAnimationPreviewMaxConcurrent);
|
||
animationPreviewMaxConcurrentInput.addEventListener("blur", commitAnimationPreviewMaxConcurrent);
|
||
animationPreviewMaxConcurrentInput.addEventListener("keydown", (event) => {
|
||
if (event.key !== "Enter") return;
|
||
event.preventDefault();
|
||
animationPreviewMaxConcurrentInput.blur();
|
||
});
|
||
|
||
orientationButtons.forEach((button) => {
|
||
button.addEventListener("click", async () => {
|
||
if (orientationPending) return;
|
||
const requested = Number(button.dataset.orientation);
|
||
if (requested === confirmedOrientation) return;
|
||
orientationPending = true;
|
||
orientationButtons.forEach((item) => setControlBusy(item));
|
||
showOrientation(requested);
|
||
setFeedback("正在应用屏幕方向…", "pending");
|
||
try {
|
||
const config = await apiJson("/api/config", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ orientation: requested }),
|
||
});
|
||
confirmedOrientation = Number(config.orientation);
|
||
showOrientation(confirmedOrientation);
|
||
setFeedback(`方向 ${confirmedOrientation}° 已生效并保存`, "success");
|
||
announce(`屏幕方向已切换为 ${confirmedOrientation}°`);
|
||
await refreshStatus();
|
||
} catch (error) {
|
||
showOrientation(confirmedOrientation);
|
||
setFeedback(`方向应用失败:${error.message}`, "error");
|
||
announce(`方向应用失败:${error.message}`, true);
|
||
} finally {
|
||
orientationPending = false;
|
||
orientationButtons.forEach((item) => setControlReady(item));
|
||
}
|
||
});
|
||
});
|
||
|
||
matrixRefreshRateButtons.forEach((button) => {
|
||
button.addEventListener("click", async () => {
|
||
if (matrixRefreshRatePending) return;
|
||
const requested = Number(button.dataset.matrixRefreshRate);
|
||
if (requested === confirmedMatrixRefreshRate) return;
|
||
|
||
const previous = confirmedMatrixRefreshRate;
|
||
matrixRefreshRatePending = true;
|
||
matrixRefreshRateButtons.forEach((item) => setControlBusy(item));
|
||
showMatrixRefreshRate(requested);
|
||
setFeedback(`正在应用 ${requested}Hz 扫描上限…`, "pending");
|
||
try {
|
||
const config = await apiJson("/api/config", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ matrix_refresh_rate_limit_hz: requested }),
|
||
});
|
||
const applied = Number(config.matrix_refresh_rate_limit_hz);
|
||
if (!matrixRefreshRateButtons.some(
|
||
(item) => Number(item.dataset.matrixRefreshRate) === applied
|
||
)) {
|
||
throw new Error("服务器返回了无效的扫描刷新率挡位");
|
||
}
|
||
confirmedMatrixRefreshRate = applied;
|
||
showMatrixRefreshRate(confirmedMatrixRefreshRate);
|
||
setFeedback(`${confirmedMatrixRefreshRate}Hz 上限已生效并保存`, "success");
|
||
announce(`屏幕扫描刷新率上限已切换为 ${confirmedMatrixRefreshRate}Hz`);
|
||
await refreshStatus();
|
||
} catch (error) {
|
||
confirmedMatrixRefreshRate = previous;
|
||
showMatrixRefreshRate(previous);
|
||
setFeedback(`扫描刷新率应用失败:${error.message}`, "error");
|
||
announce(`扫描刷新率应用失败:${error.message}`, true);
|
||
} finally {
|
||
matrixRefreshRatePending = false;
|
||
matrixRefreshRateButtons.forEach((item) => setControlReady(item));
|
||
}
|
||
});
|
||
});
|
||
|
||
async function processBrightnessQueue() {
|
||
if (applyingBrightness) return;
|
||
applyingBrightness = true;
|
||
while (desiredBrightness !== null) {
|
||
const requested = desiredBrightness;
|
||
desiredBrightness = null;
|
||
setFeedback(brightnessFeedbackText("pending", requested), "pending");
|
||
try {
|
||
const config = await apiJson("/api/config", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ brightness: requested }),
|
||
});
|
||
confirmedBrightness = Number(config.brightness);
|
||
latestEffectiveBrightness = null;
|
||
showBrightnessProtectionStatus(confirmedBrightness);
|
||
setFeedback(brightnessFeedbackText("success", confirmedBrightness), "success");
|
||
} catch (error) {
|
||
if (desiredBrightness === null) {
|
||
showBrightness(confirmedBrightness);
|
||
showBrightnessProtectionStatus(confirmedBrightness, latestEffectiveBrightness);
|
||
}
|
||
setFeedback(`亮度应用失败:${error.message}`, "error");
|
||
announce(`亮度应用失败:${error.message}`, true);
|
||
}
|
||
}
|
||
applyingBrightness = false;
|
||
await refreshStatus().catch(() => {});
|
||
}
|
||
|
||
function queueBrightness(value) {
|
||
desiredBrightness = Math.max(1, Math.min(100, Number(value)));
|
||
processBrightnessQueue();
|
||
}
|
||
|
||
brightness.addEventListener("input", () => {
|
||
brightnessValue.textContent = `${brightness.value}%`;
|
||
showBrightnessProtectionStatus(brightness.value);
|
||
setFeedback(brightnessFeedbackText("pending", brightness.value), "pending");
|
||
queueBrightness(brightness.value);
|
||
});
|
||
brightness.addEventListener("change", () => {
|
||
queueBrightness(brightness.value);
|
||
});
|
||
|
||
async function processPreviewRefreshQueue() {
|
||
if (previewRefreshPending) return;
|
||
previewRefreshPending = true;
|
||
while (desiredPreviewRefreshInterval !== null) {
|
||
const requested = desiredPreviewRefreshInterval;
|
||
desiredPreviewRefreshInterval = null;
|
||
savingPreviewRefreshInterval = requested;
|
||
setFeedback(`正在保存预览刷新间隔 ${requested}ms…`, "pending");
|
||
try {
|
||
const config = await apiJson("/api/config", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ preview_refresh_interval_ms: requested }),
|
||
});
|
||
confirmedPreviewRefreshInterval = Number(config.preview_refresh_interval_ms);
|
||
showPreviewRefreshInterval(confirmedPreviewRefreshInterval);
|
||
setPreviewRefreshInterval(confirmedPreviewRefreshInterval);
|
||
setFeedback(`预览刷新间隔 ${confirmedPreviewRefreshInterval}ms 已保存`, "success");
|
||
} catch (error) {
|
||
if (desiredPreviewRefreshInterval === null) {
|
||
showPreviewRefreshInterval(confirmedPreviewRefreshInterval);
|
||
}
|
||
setFeedback(`预览刷新间隔保存失败:${error.message}`, "error");
|
||
announce(`预览刷新间隔保存失败:${error.message}`, true);
|
||
} finally {
|
||
savingPreviewRefreshInterval = null;
|
||
}
|
||
}
|
||
previewRefreshPending = false;
|
||
await refreshStatus().catch(() => {});
|
||
}
|
||
|
||
function commitPreviewRefreshInterval() {
|
||
const value = previewRefreshInput.value.trim();
|
||
const requested = Number(value);
|
||
if (
|
||
value === ""
|
||
|| !/^\d+$/.test(value)
|
||
|| !Number.isInteger(requested)
|
||
|| requested < 1
|
||
|| requested > 60000
|
||
) {
|
||
previewRefreshInput.value = String(confirmedPreviewRefreshInterval);
|
||
showPreviewRefreshWarning(confirmedPreviewRefreshInterval);
|
||
showPreviewRefreshError("请输入 1 到 60000 之间的整数。");
|
||
return;
|
||
}
|
||
showPreviewRefreshError();
|
||
showPreviewRefreshWarning(requested);
|
||
if (
|
||
requested === savingPreviewRefreshInterval
|
||
|| requested === desiredPreviewRefreshInterval
|
||
) return;
|
||
if (requested === confirmedPreviewRefreshInterval && !previewRefreshPending) return;
|
||
desiredPreviewRefreshInterval = requested;
|
||
void processPreviewRefreshQueue();
|
||
}
|
||
|
||
previewRefreshInput.addEventListener("input", () => {
|
||
showPreviewRefreshWarning(null);
|
||
showPreviewRefreshError();
|
||
});
|
||
previewRefreshInput.addEventListener("change", commitPreviewRefreshInterval);
|
||
previewRefreshInput.addEventListener("blur", commitPreviewRefreshInterval);
|
||
previewRefreshInput.addEventListener("keydown", (event) => {
|
||
if (event.key !== "Enter") return;
|
||
event.preventDefault();
|
||
previewRefreshInput.blur();
|
||
});
|
||
|
||
document.addEventListener("matrix:status", (event) => {
|
||
const detail = event.detail || {};
|
||
syncPerformanceModeStatus(detail.system?.performance_mode);
|
||
const statusMatrixRefreshRate = Number(
|
||
detail.screen?.driver_options?.limit_refresh_rate_hz,
|
||
);
|
||
if (
|
||
Number.isInteger(statusMatrixRefreshRate)
|
||
&& !matrixRefreshRatePending
|
||
&& matrixRefreshRateButtons.some(
|
||
(button) => Number(button.dataset.matrixRefreshRate) === statusMatrixRefreshRate,
|
||
)
|
||
) {
|
||
confirmedMatrixRefreshRate = statusMatrixRefreshRate;
|
||
showMatrixRefreshRate(confirmedMatrixRefreshRate);
|
||
}
|
||
const statusPreviewInterval = Number(detail.ui?.preview_refresh_interval_ms);
|
||
if (
|
||
Number.isInteger(statusPreviewInterval)
|
||
&& !previewRefreshPending
|
||
&& desiredPreviewRefreshInterval === null
|
||
&& document.activeElement !== previewRefreshInput
|
||
) {
|
||
confirmedPreviewRefreshInterval = statusPreviewInterval;
|
||
showPreviewRefreshInterval(confirmedPreviewRefreshInterval);
|
||
setPreviewRefreshInterval(confirmedPreviewRefreshInterval);
|
||
}
|
||
|
||
const state = detail.state;
|
||
displayTestActive = state?.display_test?.active === true;
|
||
displayTestBrightness = displayTestActive
|
||
? finitePercent(state?.display_test?.brightness)
|
||
: null;
|
||
voltageStatus = detail.power?.screen_input_voltage || null;
|
||
lowVoltageProtectionStatus = detail.power?.low_voltage_protection || null;
|
||
latestEffectiveBrightness = finitePercent(state?.effective_brightness);
|
||
showVoltageDetails();
|
||
|
||
if (!lowVoltageProtectionPending && typeof lowVoltageProtectionStatus?.enabled === "boolean") {
|
||
const protectionChanged = lowVoltageProtectionStatus.enabled
|
||
!== confirmedLowVoltageProtectionEnabled;
|
||
syncLowVoltageProtectionEnabled(lowVoltageProtectionStatus.enabled);
|
||
if (protectionChanged && voltageConfig !== null) {
|
||
setLowVoltageProtectionFeedback(
|
||
confirmedLowVoltageProtectionEnabled
|
||
? "保护开关已从其他页面同步为开启"
|
||
: "保护开关已从其他页面同步为关闭",
|
||
"ready",
|
||
);
|
||
}
|
||
}
|
||
showBrightnessProtectionStatus(state?.brightness ?? confirmedBrightness, latestEffectiveBrightness);
|
||
|
||
if (orientationPending || applyingBrightness || desiredBrightness !== null) return;
|
||
if (!state) return;
|
||
const nextOrientation = Number(state.orientation);
|
||
const nextBrightness = Number(state.brightness);
|
||
const changed = nextOrientation !== confirmedOrientation || nextBrightness !== confirmedBrightness;
|
||
confirmedOrientation = nextOrientation;
|
||
confirmedBrightness = nextBrightness;
|
||
showOrientation(confirmedOrientation);
|
||
showBrightness(confirmedBrightness);
|
||
showBrightnessProtectionStatus(confirmedBrightness, latestEffectiveBrightness);
|
||
if (changed && detail.externalChange) {
|
||
setFeedback("方向或亮度已由其他控制页面更新", "ready");
|
||
}
|
||
});
|
||
|
||
registerWorkspace({
|
||
id: "settings",
|
||
title: "系统设置",
|
||
elementId: "workspace-settings",
|
||
order: 20,
|
||
persistence: "server",
|
||
onEnter: loadSettings,
|
||
onLeave: () => {
|
||
discardWifiSettingsDrafts();
|
||
discardFrpDrafts();
|
||
},
|
||
});
|