873 lines
31 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<title>IoT Pump controller</title>
<style>
:root {
/* Min slider colors */
--min-slider-start: #e94f32;
--min-slider-end: #cb9085;
/* Max slider colors */
--max-slider-start: #fbbf24;
--max-slider-end: #c0bbad;
/* Common slider styles */
--slider-height: 6px;
--slider-radius: 12px;
--thumb-size: 24px;
--shadow-inset: inset 0 1px 2px #00000040;
}
* {
box-sizing: border-box;
user-select: none;
}
body {
margin: 0;
min-height: 100dvh;
background: radial-gradient(circle at 20% 30%, #121826, #0b0f17);
font-family: 'Segoe UI', 'Inter', system-ui, -apple-system, 'Roboto', sans-serif;
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
}
.dashboard {
max-width: 920px;
width: 100%;
background: rgba(18, 22, 30, 0.75);
backdrop-filter: blur(8px);
border-radius: 60px 60px 48px 48px;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08);
padding: 18px 20px 24px;
transition: all 0.2s ease;
border: 1px solid rgba(71, 85, 105, 0.5);
}
.gauge-container {
display: flex;
justify-content: center;
margin: 5px 0 8px;
filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.5));
}
canvas {
width: 100%;
height: auto;
max-width: 460px;
aspect-ratio: 1 / 1;
background: #131a24;
border-radius: 50%;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.7), inset 0 1px 2px rgba(255, 255, 255, 0.1);
display: block;
transition: box-shadow 0.2s;
}
.pressure-row {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin: 0 auto 12px;
}
.pressure-value {
text-align: center;
font-size: 1.9rem;
font-weight: 700;
margin: 0;
color: #eef4ff;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(12px);
padding: 6px 24px;
border-radius: 80px;
display: inline-block;
width: auto;
letter-spacing: 1px;
border: 1px solid rgba(80, 140, 200, 0.4);
font-family: 'JetBrains Mono', monospace, 'Segoe UI';
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.value-wrapper {
text-align: center;
}
.sliders-panel {
background: #0e121cb3;
backdrop-filter: blur(12px);
border-radius: 44px;
padding: 18px 20px 16px;
margin-top: 16px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 12px 28px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(72, 92, 118, 0.5);
}
.slider-group {
margin-bottom: 24px;
-webkit-tap-highlight-color: transparent;
}
.slider-group label {
display: flex;
justify-content: space-between;
font-weight: 600;
color: #e0eafc;
font-size: 1.1rem;
margin-bottom: 8px;
letter-spacing: 0.3px;
text-shadow: 0 1px 1px black;
}
.threshold-value {
background: #1e2f3f;
color: #c0e0ff;
padding: 3px 14px;
border-radius: 40px;
font-size: 0.85rem;
font-weight: 500;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3), 0 1px 0 rgba(255,255,255,0.1);
display: inline-flex;
gap: 4px;
align-items: baseline;
}
.threshold-value span:first-child {
min-width: 45px;
text-align: right;
font-weight: 600;
}
.threshold-value span:last-child {
opacity: 0.8;
}
.slider-group span {
background: #1e2f3f;
color: #c0e0ff;
padding: 3px 14px;
border-radius: 40px;
font-size: 0.85rem;
font-weight: 500;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3), 0 1px 0 rgba(255,255,255,0.1);
}
.slider-base {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
border-radius: 12px;
cursor: pointer;
box-shadow: inset 0 1px 2px #00000040;
}
.min-slider {
width: 100%;
height: var(--slider-height);
-webkit-appearance: none;
background: linear-gradient(90deg, var(--min-slider-end), var(--min-slider-start));
border-radius: var(--slider-radius);
cursor: pointer;
box-shadow: var(--shadow-inset);
}
.max-slider {
width: 100%;
height: var(--slider-height);
-webkit-appearance: none;
background: linear-gradient(90deg, var(--max-slider-end), var(--max-slider-start));
border-radius: var(--slider-radius);
cursor: pointer;
box-shadow: var(--shadow-inset);
}
.min-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: var(--thumb-size);
height: var(--thumb-size);
background: radial-gradient(circle, var(--min-slider-start), var(--min-slider-end));
border-radius: 50%;
cursor: pointer;
transition: transform 0.08s;
}
.max-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: var(--thumb-size);
height: var(--thumb-size);
background: radial-gradient(circle, var(--max-slider-start), var(--max-slider-end));
border-radius: 50%;
cursor: pointer;
transition: transform 0.08s;
}
/* Optional: Add hover effect for thumbs */
.min-slider::-webkit-slider-thumb:hover,
.max-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
input[type="range"]::-webkit-slider-thumb:active {
transform: scale(1.15);
}
.threshold-hint {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 0.7rem;
color: #94a9c9;
font-weight: 500;
}
.save-btn {
width: 100%;
background: linear-gradient(95deg, #1f5a3a, #2b7a55);
border: none;
padding: 12px;
font-size: 1.25rem;
font-weight: bold;
color: #f0faf5;
border-radius: 60px;
margin-top: 18px;
cursor: pointer;
box-shadow: 0 5px 0 #0d3522;
transition: transform 0.08s, box-shadow 0.08s, background 0.1s;
letter-spacing: 1px;
backdrop-filter: blur(2px);
font-family: inherit;
-webkit-tap-highlight-color: transparent;
}
.save-btn:active {
transform: translateY(3px);
box-shadow: 0 2px 0 #0d3522;
background: linear-gradient(95deg, #154d33, #216a48);
}
.toast-msg {
visibility: hidden;
background: #1f2f3cee;
backdrop-filter: blur(20px);
color: #c2f0d6;
text-align: center;
border-radius: 60px;
padding: 10px 24px;
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
z-index: 1000;
pointer-events: none;
white-space: nowrap;
font-weight: 600;
border: 1px solid #3c936e;
letter-spacing: 0.4px;
box-shadow: 0 6px 16px black;
}
.toast-msg.show {
visibility: visible;
opacity: 1;
transition: opacity 0.2s ease;
}
@media (max-width: 550px) {
.dashboard { padding: 14px 14px 20px; }
.slider-group label { font-size: 0.95rem; }
.save-btn { font-size: 1.1rem; padding: 10px; }
.pressure-value { font-size: 1.5rem; padding: 4px 18px; }
.toast-msg { white-space: nowrap; font-size: 0.8rem; padding: 6px 18px;}
.sliders-panel { padding: 14px 16px 12px; margin-top: 12px; }
.slider-group { margin-bottom: 18px; }
.gauge-container { margin: 2px 0 5px; }
.pressure-row { gap: 12px; margin-bottom: 8px; }
canvas { max-width: 380px; }
}
.pump-state {
text-align: center;
margin: 0;
padding: 12px 24px;
width: fit-content;
border-radius: 60px;
background: #33373b;
font-weight: 700;
font-size: 1.1rem;
letter-spacing: 1px;
color: #959ea6;
font-family: monospace;
text-transform: uppercase;
border: none;
transition: 0.15s linear;
}
.pump-state.enabled {
background: #00a86b;
color: white;
text-shadow: 0 0 2px rgba(0,0,0,0.3);
animation: pump-glow 1.2s infinite ease;
}
@keyframes pump-glow {
0% {
box-shadow: 0 0 0 0 rgba(0, 168, 107, 0.4);
}
70% {
box-shadow: 0 0 0 8px rgba(0, 168, 107, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(0, 168, 107, 0);
}
}
footer {
text-align: center;
font-size: 0.65rem;
margin-top: 14px;
color: #6f8aae;
opacity: 0.7;
font-weight: 500;
}
</style>
</head>
<body>
<div class="dashboard">
<div class="gauge-container">
<canvas id="manometerCanvas" width="500" height="500"></canvas>
</div>
<div class="value-wrapper">
<div class="pressure-row">
<div class="pressure-value" id="currentPressureDisplay">0.00 атм</div>
<div class="pump-state" id="currentPumpState"></div>
</div>
</div>
<div class="sliders-panel">
<div class="slider-group">
<label>
Нижний порог (включение)
<span class="threshold-value">
<span id="minThresholdVal">0.00</span> атм
</span>
</label>
<input type="range" class="min-slider" id="minSlider" min="0" max="8" step="0.05" value="0.0">
</div>
<div class="slider-group">
<label>
Верхний порог (выключение)
<span class="threshold-value">
<span id="maxThresholdVal">0.10</span> атм
</span>
</label>
<input type="range" class="max-slider" id="maxSlider" min="0" max="8" step="0.05" value="3.0">
</div>
<button class="save-btn" id="saveButton">💾 СОХРАНИТЬ УСТАНОВКИ</button>
</div>
<footer>⚡IoT Pump controller | © Vladislav Kan &lt;thek4n@yandex.ru&gt;</footer>
</div>
<div id="toastMsg" class="toast-msg">✓ Пороги успешно сохранены</div>
<script>
(function() {
// ----- ДИАПАЗОН И ГЕОМЕТРИЯ -----
const MIN_PRESSURE = 0;
const MAX_PRESSURE = 8;
const START_ANGLE = -225; // градусы
const END_ANGLE = 45;
// DOM элементы
const canvas = document.getElementById('manometerCanvas');
const ctx = canvas.getContext('2d');
const minSlider = document.getElementById('minSlider');
const maxSlider = document.getElementById('maxSlider');
const minSpan = document.getElementById('minThresholdVal');
const maxSpan = document.getElementById('maxThresholdVal');
const pressureDisplay = document.getElementById('currentPressureDisplay');
const currentState = document.getElementById('currentPumpState');
const saveBtn = document.getElementById('saveButton');
const toast = document.getElementById('toastMsg');
let minThreshold = 1.4;
let maxThreshold = 6.2;
let currentPressure = 0;
let pollingInterval = null;
let cachedScale = null;
let canvasWidth = 500, canvasHeight = 500;
// --- Утилиты для углов ---
const degToRad = (deg) => (deg * Math.PI) / 180;
// ---- Построение кеша разметки шкалы (адаптирован цвет под тёмную тему) ----
function buildScaleCache() {
const cache = [];
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;
const radius = canvasWidth * 0.42;
const startRad = degToRad(START_ANGLE);
const endRad = degToRad(END_ANGLE);
const angleRange = endRad - startRad;
for (let p = MIN_PRESSURE; p <= MAX_PRESSURE + 0.01; p += 0.1) {
const pressureVal = Math.round(p * 10) / 10;
if (pressureVal > MAX_PRESSURE) continue;
const t = (pressureVal - MIN_PRESSURE) / (MAX_PRESSURE - MIN_PRESSURE);
const angle = startRad + t * angleRange;
const isFullAtm = Math.abs(pressureVal - Math.round(pressureVal)) < 0.01;
const isHalfAtm = !isFullAtm && (Math.abs(pressureVal * 2 - Math.round(pressureVal * 2)) < 0.05);
let innerR, outerR, lineWidth;
if (isFullAtm) {
innerR = radius - 18;
outerR = radius + 9;
lineWidth = 3.2;
} else if (isHalfAtm) {
innerR = radius - 13;
outerR = radius + 5;
lineWidth = 2.2;
} else {
innerR = radius - 9;
outerR = radius + 2;
lineWidth = 1.3;
}
const x1 = centerX + innerR * Math.cos(angle);
const y1 = centerY + innerR * Math.sin(angle);
const x2 = centerX + outerR * Math.cos(angle);
const y2 = centerY + outerR * Math.sin(angle);
cache.push({
pressure: pressureVal,
isFullAtm,
x1, y1, x2, y2,
lineWidth,
textX: isFullAtm ? centerX + (radius - 27) * Math.cos(angle) - 9 : null,
textY: isFullAtm ? centerY + (radius - 27) * Math.sin(angle) + 8 : null
});
}
return cache;
}
// Отрисовка шкалы (тёмные, читаемые метки)
function drawScale() {
if (!cachedScale) return;
for (const mark of cachedScale) {
ctx.beginPath();
ctx.moveTo(mark.x1, mark.y1);
ctx.lineTo(mark.x2, mark.y2);
ctx.lineWidth = mark.lineWidth;
ctx.strokeStyle = '#a0bbdf';
ctx.shadowBlur = 0;
ctx.stroke();
if (mark.isFullAtm && mark.textX !== null) {
ctx.font = `600 ${canvasWidth * 0.048}px "Segoe UI", "Inter", system-ui`;
ctx.fillStyle = '#eef2fc';
ctx.shadowBlur = 2;
ctx.shadowColor = "rgba(0,0,0,0.6)";
ctx.fillText(Math.round(mark.pressure).toString(), mark.textX, mark.textY);
ctx.shadowBlur = 0;
}
}
}
// Отрисовка цветных порогов (яркие акценты)
function drawThresholdMark(pressure, color) {
const w = canvas.width, h = canvas.height;
const centerX = w / 2, centerY = h / 2;
const radius = w * 0.42;
const startRad = degToRad(START_ANGLE);
const endRad = degToRad(END_ANGLE);
const t = Math.min(Math.max((pressure - MIN_PRESSURE) / (MAX_PRESSURE - MIN_PRESSURE), 0), 1);
const angle = startRad + t * (endRad - startRad);
const innerRad = radius - 18;
const outerRad = radius + 24;
const x1 = centerX + innerRad * Math.cos(angle);
const y1 = centerY + innerRad * Math.sin(angle);
const x2 = centerX + outerRad * Math.cos(angle);
const y2 = centerY + outerRad * Math.sin(angle);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineWidth = 9;
ctx.strokeStyle = color;
ctx.shadowBlur = 8;
ctx.shadowColor = "rgba(0,0,0,0.6)";
ctx.stroke();
ctx.shadowBlur = 0;
}
// Эффектная стрелка с металлическим отливом и глянцем
function drawPointer(angle, centerX, centerY, radius) {
const mainLength = radius - 22;
const tailLength = radius * 0.28;
const dirX = Math.cos(angle);
const dirY = Math.sin(angle);
const perpX = -Math.sin(angle);
const perpY = Math.cos(angle);
const baseDist = 16;
const baseWidth = 19;
const tipWidth = 5;
const baseLeftX = centerX + baseDist * dirX + (baseWidth/2) * perpX;
const baseLeftY = centerY + baseDist * dirY + (baseWidth/2) * perpY;
const baseRightX = centerX + baseDist * dirX - (baseWidth/2) * perpX;
const baseRightY = centerY + baseDist * dirY - (baseWidth/2) * perpY;
const tipX = centerX + mainLength * dirX;
const tipY = centerY + mainLength * dirY;
const tipLeftX = tipX + (tipWidth/2) * perpX;
const tipLeftY = tipY + (tipWidth/2) * perpY;
const tipRightX = tipX - (tipWidth/2) * perpX;
const tipRightY = tipY - (tipWidth/2) * perpY;
ctx.beginPath();
ctx.moveTo(baseLeftX, baseLeftY);
ctx.lineTo(tipLeftX, tipLeftY);
ctx.lineTo(tipRightX, tipRightY);
ctx.lineTo(baseRightX, baseRightY);
ctx.closePath();
const metalGrad = ctx.createLinearGradient(
centerX + dirX * 12, centerY + dirY * 12,
centerX + dirX * (mainLength - 15), centerY + dirY * (mainLength - 15)
);
metalGrad.addColorStop(0, '#cfddee');
metalGrad.addColorStop(0.6, '#919db0');
metalGrad.addColorStop(1, '#5f6c80');
ctx.fillStyle = metalGrad;
ctx.fill();
ctx.strokeStyle = '#2c3d55';
ctx.lineWidth = 1.2;
ctx.stroke();
// Хвост (противовес)
const tailAngle = angle + Math.PI;
const tailDirX = Math.cos(tailAngle);
const tailDirY = Math.sin(tailAngle);
const tailPerpX = -Math.sin(tailAngle);
const tailPerpY = Math.cos(tailAngle);
const tailBaseDist = 13;
const tailWidth = 13;
const tailEndX = centerX + tailLength * tailDirX;
const tailEndY = centerY + tailLength * tailDirY;
const tailBaseX = centerX + tailBaseDist * tailDirX;
const tailBaseY = centerY + tailBaseDist * tailDirY;
const tailLeftX = tailBaseX + (tailWidth/2) * tailPerpX;
const tailLeftY = tailBaseY + (tailWidth/2) * tailPerpY;
const tailRightX = tailBaseX - (tailWidth/2) * tailPerpX;
const tailRightY = tailBaseY - (tailWidth/2) * tailPerpY;
const tailEndLeftX = tailEndX + (tailWidth/2.5) * tailPerpX;
const tailEndLeftY = tailEndY + (tailWidth/2.5) * tailPerpY;
const tailEndRightX = tailEndX - (tailWidth/2.5) * tailPerpX;
const tailEndRightY = tailEndY - (tailWidth/2.5) * tailPerpY;
ctx.beginPath();
ctx.moveTo(tailLeftX, tailLeftY);
ctx.lineTo(tailEndLeftX, tailEndLeftY);
ctx.lineTo(tailEndRightX, tailEndRightY);
ctx.lineTo(tailRightX, tailRightY);
ctx.fillStyle = '#6b788e';
ctx.fill();
ctx.stroke();
}
async function fetchState() {
try {
const response = await fetch('/state');
if (!response.ok) {
throw new Error('Failed to fetch state');
}
const data = await response.json();
const state = typeof data === 'number' ? data : data.state;
if (typeof state === 'number' && !isNaN(state)) {
state === 1 ? currentState.classList.add("enabled") : currentState.classList.remove("enabled");
}
} catch (error) {
console.error('Error fetching state:', error);
}
}
async function fetchPressure() {
try {
const response = await fetch('/pressure');
if (!response.ok) {
throw new Error('Failed to fetch pressure');
}
const data = await response.json();
const pressure = typeof data === 'number' ? data : data.value / 100;
if (typeof pressure === 'number' && !isNaN(pressure)) {
currentPressure = Math.min(Math.max(pressure, MIN_PRESSURE), MAX_PRESSURE);
pressureDisplay.innerText = currentPressure.toFixed(2) + " атм";
drawGauge();
}
} catch (error) {
console.error('Error fetching pressure:', error);
}
}
async function fetchThresholds() {
try {
const response = await fetch('/thresholds');
if (!response.ok) {
throw new Error('Failed to fetch thresholds');
}
const data = await response.json();
if (typeof data.low === 'number' && typeof data.up === 'number') {
minThreshold = Math.min(Math.max(data.low / 100, MIN_PRESSURE), MAX_PRESSURE);
maxThreshold = Math.min(Math.max(data.up / 100, MIN_PRESSURE), MAX_PRESSURE);
updateUI();
}
} catch (error) {
console.error('Error fetching thresholds:', error);
}
}
// Функция для отправки порогов на сервер с debounce
let saveTimeout = null;
function debounceSetThresholds() {
// Очищаем предыдущий таймаут
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(async () => {
try {
const response = await fetch('/thresholds', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
low: parseFloat(minThreshold * 100),
up: parseFloat(maxThreshold * 100)
})
});
if (!response.ok) {
throw new Error('Failed to save thresholds');
}
console.log('Thresholds saved:', await response.json());
// Показываем временный тост
toast.innerText = "✓ Пороги обновлены";
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1000);
} catch (error) {
console.error('Error saving thresholds:', error);
toast.innerText = "✗ Ошибка сохранения";
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1600);
}
}, 700);
}
async function saveThresholds() {
try {
const response = await fetch('/persist_thresholds', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
low: parseFloat(minThreshold * 100),
up: parseFloat(maxThreshold * 100)
})
});
if (!response.ok) {
throw new Error('Failed to save thresholds');
}
const data = await response.json();
console.log('Thresholds saved:', data);
toast.innerText = "✓ Пороги сохранены";
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1600);
// После сохранения обновляем пороги с сервера для синхронизации
await fetchThresholds();
} catch (error) {
console.error('Error saving thresholds:', error);
toast.innerText = "✗ Ошибка сохранения порогов";
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1600);
}
}
function drawGauge() {
if (!ctx) return;
const w = canvas.width, h = canvas.height;
const centerX = w / 2, centerY = h / 2;
const radius = w * 0.42;
ctx.clearRect(0, 0, w, h);
ctx.shadowBlur = 0;
// Внешний металлический обод (тёмный хром)
ctx.beginPath();
ctx.arc(centerX, centerY, radius + 10, 0, Math.PI * 2);
ctx.strokeStyle = '#3e4b60';
ctx.lineWidth = 4;
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX, centerY, radius + 6, 0, Math.PI * 2);
ctx.strokeStyle = '#232b37';
ctx.lineWidth = 2.5;
ctx.stroke();
// Внутренний фон (ночной глянец)
ctx.beginPath();
ctx.arc(centerX, centerY, radius + 2, 0, Math.PI * 2);
ctx.fillStyle = '#0d1420';
ctx.fill();
// Основная шкала
drawScale();
// Текст "атм" стилизованный
ctx.font = `500 ${w * 0.042}px "Segoe UI"`;
ctx.fillStyle = '#b8cfec';
ctx.shadowBlur = 2;
ctx.fillText("atm", centerX - 18, centerY + radius * 0.69);
ctx.fillStyle = '#85a9d0';
ctx.font = `300 ${w * 0.028}px monospace`;
ctx.shadowBlur = 0;
// пороговые метки
drawThresholdMark(minThreshold, '#f0745c'); // кораллово-красный
drawThresholdMark(maxThreshold, '#fbbf24'); // янтарный
// стрелка
const startRad = degToRad(START_ANGLE);
const endRad = degToRad(END_ANGLE);
const tValue = (currentPressure - MIN_PRESSURE) / (MAX_PRESSURE - MIN_PRESSURE);
const angle = startRad + Math.min(Math.max(tValue, 0), 1) * (endRad - startRad);
ctx.shadowBlur = 8;
ctx.shadowColor = "rgba(0,0,0,0.55)";
drawPointer(angle, centerX, centerY, radius);
ctx.shadowBlur = 0;
// центральная ступица (тёмная)
ctx.fillStyle = "#202632";
ctx.beginPath();
ctx.arc(centerX, centerY, 14, 0, 2 * Math.PI);
ctx.fill();
const hubGrad = ctx.createLinearGradient(centerX - 5, centerY - 5, centerX + 5, centerY + 5);
hubGrad.addColorStop(0, "#5d6f8a");
hubGrad.addColorStop(1, "#2e3b4a");
ctx.fillStyle = hubGrad;
ctx.beginPath();
ctx.arc(centerX, centerY, 9, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = "#bac8dc";
ctx.beginPath();
ctx.arc(centerX, centerY, 4.2, 0, 2 * Math.PI);
ctx.fill();
}
function updateUI() {
minSlider.value = minThreshold;
maxSlider.value = maxThreshold;
minSpan.innerText = minThreshold.toFixed(2);
maxSpan.innerText = maxThreshold.toFixed(2);
drawGauge();
}
function initEvents() {
minSlider.addEventListener('input', () => {
let val = parseFloat(minSlider.value);
if (val >= maxThreshold) {
val = Math.max(MIN_PRESSURE, maxThreshold - 0.08);
minSlider.value = val;
}
minThreshold = val;
minSpan.innerText = minThreshold.toFixed(2);
drawGauge();
debounceSetThresholds();
});
maxSlider.addEventListener('input', () => {
let val = parseFloat(maxSlider.value);
if (val <= minThreshold) {
val = Math.min(MAX_PRESSURE, minThreshold + 0.08);
maxSlider.value = val;
}
maxThreshold = val;
maxSpan.innerText = maxThreshold.toFixed(2);
drawGauge();
debounceSetThresholds();
});
saveBtn.addEventListener('click', () => {
saveThresholds();
});
}
function startPolling() {
// Запускаем опрос давления каждые 500 мс
pollingInterval = setInterval(fetchPressure, 500);
pollingInterval = setInterval(fetchState, 500);
}
function stopPolling() {
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
}
async function init() {
canvasWidth = canvas.width;
canvasHeight = canvas.height;
cachedScale = buildScaleCache();
// Загружаем начальные данные
await fetchThresholds();
await fetchPressure();
initEvents();
updateUI();
startPolling();
window.addEventListener('resize', () => {
drawGauge();
});
}
// Очистка интервала при выгрузке страницы
window.addEventListener('beforeunload', () => {
stopPolling();
});
init();
})();
</script>
</body>
</html>