Files
matrix-screen-controller/核桃派软件源代码/app/static/views/settings.js
T

1114 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("");
}
}
}
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) 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) 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();
},
});