В процессе работы

This commit is contained in:
Maxim
2026-06-22 14:38:07 +03:00
parent eab9e17212
commit 877b29d5b4
4 changed files with 331 additions and 382 deletions

View File

@@ -1,26 +1,28 @@
// app.js (оркестратор)
// app.ts - Оркестратор приложения
import {
fetchHealth,
fetchHistory
} from "./data.js";
import {
state,
addSignals,
resetBufferTracking,
updatePeakHolder,
updateSignalPeakHolder,
setPeakHolderListener
state,
addSignals,
resetBufferTracking,
updatePeakHolder,
updateSignalPeakHolder,
setPeakHolderListener
} from "./state.js";
import {
initDOM,
setServerTime,
setStatus,
setMeta,
setBars,
setSignal,
setAudioIndicator,
DOM,
updateAllChannels
initDOM,
setServerTime,
setStatus,
setMeta,
setBars,
setSignal,
setAudioIndicator,
DOM,
setChannelValue,
channelElements
} from "./ui.js";
import { initAccordion } from "./accordion.js";
import { drawChart } from "./chart.js";
@@ -37,29 +39,28 @@ let lastSignalLevel: number = 0;
// ================= INIT =================
window.addEventListener("DOMContentLoaded", () => {
initDOM();
initAccordion();
initResize();
initCamera();
initDOM();
initAccordion();
initResize();
initCamera();
// Инициализация звука при первом клике
document.addEventListener('click', () => {
audioEngine.updateLevel(0);
}, { once: true });
document.addEventListener('click', () => {
audioEngine.updateLevel(0);
}, { once: true });
setPeakHolderListener(() => {
if (!connectionLost) {
setBars(state.peakValue);
setSignal(state.peakSignalLevel);
setPeakHolderListener(() => {
if (!connectionLost) {
setBars(state.peakValue);
setSignal(state.peakSignalLevel);
const signalLevel = state.peakSignalLevel;
if (signalLevel !== lastSignalLevel) {
lastSignalLevel = signalLevel;
audioEngine.updateLevel(signalLevel);
setAudioIndicator(signalLevel > 0, signalLevel);
}
}
});
const signalLevel = state.peakSignalLevel;
if (signalLevel !== lastSignalLevel) {
lastSignalLevel = signalLevel;
audioEngine.updateLevel(signalLevel);
setAudioIndicator(signalLevel > 0, signalLevel);
}
}
});
});
document.addEventListener('DOMContentLoaded', () => {
@@ -76,197 +77,214 @@ document.addEventListener('DOMContentLoaded', () => {
});
document.addEventListener("keydown", (e: KeyboardEvent) => {
const leftPanel = document.getElementById("leftPanel");
const rightPanel = document.getElementById("rightPanel");
const leftPanel = document.getElementById("leftPanel");
const rightPanel = document.getElementById("rightPanel");
switch(e.key.toLowerCase()) {
case 'c':
const camBtn = document.getElementById('camToggle');
if (camBtn) camBtn.click();
break;
case 'f':
if (leftPanel) {
leftPanel.style.width = "100%";
if (rightPanel) rightPanel.style.display = "none";
}
break;
case 'escape':
if (rightPanel) rightPanel.style.display = "flex";
if (leftPanel && rightPanel) {
const total = window.innerWidth;
const leftWidth = total * 0.6;
leftPanel.style.flex = `0 0 ${leftWidth}px`;
rightPanel.style.flex = `0 0 ${total - leftWidth}px`;
}
break;
case 'm':
if (lastSignalLevel > 0) {
audioEngine.updateLevel(0);
setAudioIndicator(false);
console.log('[Audio] Звук отключён (M)');
window._mutedLevel = lastSignalLevel;
lastSignalLevel = 0;
} else {
const level = window._mutedLevel || state.peakSignalLevel;
if (level > 0) {
audioEngine.updateLevel(level);
lastSignalLevel = level;
setAudioIndicator(true, level);
console.log('[Audio] Звук включён (M)');
}
}
break;
}
switch (e.key.toLowerCase()) {
case 'c':
const camBtn = document.getElementById('camToggle');
if (camBtn) camBtn.click();
break;
case 'f':
if (leftPanel) {
leftPanel.style.width = "100%";
if (rightPanel) rightPanel.style.display = "none";
}
break;
case 'escape':
if (rightPanel) rightPanel.style.display = "flex";
if (leftPanel && rightPanel) {
const total = window.innerWidth;
const leftWidth = total * 0.6;
leftPanel.style.flex = `0 0 ${leftWidth}px`;
rightPanel.style.flex = `0 0 ${total - leftWidth}px`;
}
break;
case 'm':
if (lastSignalLevel > 0) {
audioEngine.updateLevel(0);
setAudioIndicator(false);
window._mutedLevel = lastSignalLevel;
lastSignalLevel = 0;
} else {
const level = window._mutedLevel || state.peakSignalLevel;
if (level > 0) {
audioEngine.updateLevel(level);
lastSignalLevel = level;
setAudioIndicator(true, level);
}
}
break;
}
});
// ================= UPDATE =================
async function update(): Promise<void> {
try {
const [health, history] = await Promise.all([
fetchHealth(),
fetchHistory()
]);
try {
const [health, history] = await Promise.all([
fetchHealth(),
fetchHistory()
]);
consecutiveErrors = 0;
consecutiveErrors = 0;
if (health.server_time) {
setServerTime(health.server_time);
}
if (connectionLost) {
console.log("[app] Соединение восстановлено");
}
const isOnline = health.pipe_alive && health.has_data;
if (isOnline) {
setStatus("На связи");
connectionLost = false;
} else {
setStatus("Нет связи", true);
if (!connectionLost) resetBufferTracking();
connectionLost = true;
if (lastSignalLevel > 0) {
lastSignalLevel = 0;
audioEngine.updateLevel(0);
setAudioIndicator(false);
}
}
const arr = Array.isArray(history.bytes) ? history.bytes : [];
const raw = arr[arr.length - 1] ?? 0;
// Обновляем историю сигналов в state
if (arr.length > 0 && isOnline) {
const bufferSize = health.stats?.filled ?? 0;
addSignals(arr, bufferSize);
}
// ===== ПАРСИНГ ДАННЫХ =====
// Биты 0-5: значение (0-63)
// Биты 6-7: уровень сигнала (0-3)
const value = raw & 0x3F; // 0-63
const level = (raw >> 6) & 0x3; // 0-3
const percent = (value / 63) * 100; // 0-100%
// ===== ОБНОВЛЯЕМ КАНАЛЫ =====
// Передаем сырые данные для отображения в каналах
updateAllChannels(arr);
if (isOnline) {
// ===== Обновляем Peak Holder (проценты) ====
const displayValue = updatePeakHolder(percent);
setBars(displayValue);
// Обновляем уровень сигнала с Peak Holder
const displaySignalLevel = updateSignalPeakHolder(level);
setSignal(displaySignalLevel);
// Обновляем звук
const currentSignalLevel = state.peakSignalLevel;
if (currentSignalLevel !== lastSignalLevel) {
lastSignalLevel = currentSignalLevel;
audioEngine.updateLevel(currentSignalLevel);
setAudioIndicator(currentSignalLevel > 0, currentSignalLevel);
if (health.server_time) {
setServerTime(health.server_time);
}
} else {
const isOnline = health.pipe_alive && health.has_data;
if (isOnline) {
setStatus("На связи");
connectionLost = false;
} else {
setStatus("Нет связи", true);
if (!connectionLost) resetBufferTracking();
connectionLost = true;
if (lastSignalLevel > 0) {
lastSignalLevel = 0;
audioEngine.updateLevel(0);
setAudioIndicator(false);
}
}
const arr = Array.isArray(history.bytes) ? history.bytes : [];
const raw = arr[arr.length - 1] ?? 0;
if (arr.length > 0 && isOnline) {
const bufferSize = health.stats?.filled ?? 0;
addSignals(arr, bufferSize);
}
// ===== НАХОДИМ МАКСИМУМ ИЗ БУФЕРА =====
let maxSignal = 0;
let maxLevel = 0;
let lastSignal = 0;
if (isOnline && arr.length > 0) {
for (const byte of arr) {
const signal = byte & 0x3F;
const amp = (byte >> 6) & 0x3;
if (signal > maxSignal) {
maxSignal = signal;
}
if (amp > maxLevel) {
maxLevel = amp;
}
}
lastSignal = raw & 0x3F;
}
const percent = (maxSignal / 63) * 100;
// ===== ОБНОВЛЯЕМ КАНАЛЫ =====
if (isOnline && arr.length > 0) {
setChannelValue(0, maxSignal, maxLevel);
for (let i = 1; i < 8; i++) {
setChannelValue(i, 0, 0);
if (channelElements[i]) {
channelElements[i].item.classList.remove('channel-active');
}
}
if (channelElements[0]) {
channelElements[0].item.classList.add('channel-active');
}
if (DOM.lastByte) {
DOM.lastByte.textContent = lastSignal.toString();
}
} else {
for (let i = 0; i < 8; i++) {
setChannelValue(i, 0, 0);
if (channelElements[i]) {
channelElements[i].item.classList.remove('channel-active');
}
}
if (DOM.lastByte) {
DOM.lastByte.textContent = '--';
}
}
if (isOnline) {
updatePeakHolder(percent);
updateSignalPeakHolder(maxLevel);
} else {
setBars(0, true);
setSignal(0, true);
if (lastSignalLevel > 0) {
lastSignalLevel = 0;
audioEngine.updateLevel(0);
setAudioIndicator(false);
}
}
const uptime = Math.round(health.uptime_sec || 0);
const filled = health.stats?.filled ?? 0;
const Bps = Math.round(health.stats?.bytes_per_sec ?? 0);
setMeta(`Работа: ${uptime}s | Буфер: ${filled} | Приём: ${Bps} B/s`);
if (DOM.history) {
DOM.history.innerHTML = arr
.map((b, i) => `[${i}] ${(b & 0xFF).toString(2).padStart(8, "0")} = ${b & 0xFF}`)
.join("<br>");
}
const now = performance.now();
if (now - lastChart > 100 && DOM.canvas) {
drawChart(DOM.canvas, state.signalHistory);
lastChart = now;
}
} catch (e) {
consecutiveErrors++;
setStatus("СЕРВЕР НЕДОСТУПЕН", true);
setServerTime("--:--:--");
setBars(0, true);
setSignal(0, true);
resetBufferTracking();
if (lastSignalLevel > 0) {
lastSignalLevel = 0;
audioEngine.updateLevel(0);
setAudioIndicator(false);
lastSignalLevel = 0;
audioEngine.updateLevel(0);
setAudioIndicator(false);
}
if (DOM.lastByte) DOM.lastByte.textContent = "--";
if (consecutiveErrors >= MAX_ERRORS && pollTimer) {
clearInterval(pollTimer);
}
}
if (DOM.lastByte) {
DOM.lastByte.textContent = isOnline ? value.toString() : "--";
}
const uptime = Math.round(health.uptime_sec || 0);
const filled = health.stats?.filled ?? 0;
const Bps = Math.round(health.stats?.bytes_per_sec ?? 0);
setMeta(`Работа: ${uptime}s | Буфер: ${filled} | Приём: ${Bps} B/s`);
if (DOM.history) {
DOM.history.innerHTML = arr
.map((b, i) => `[${i}] ${(b & 0xFF).toString(2).padStart(8, "0")} = ${b & 0xFF}`)
.join("<br>");
}
const now = performance.now();
if (now - lastChart > 100 && DOM.canvas) {
drawChart(DOM.canvas, state.signalHistory);
lastChart = now;
}
} catch (e) {
consecutiveErrors++;
if (consecutiveErrors === 1) console.warn("[app] Сервер недоступен, ждём...");
setStatus("СЕРВЕР НЕДОСТУПЕН", true);
setServerTime("--:--:--");
setBars(0, true);
setSignal(0, true);
resetBufferTracking();
if (lastSignalLevel > 0) {
lastSignalLevel = 0;
audioEngine.updateLevel(0);
setAudioIndicator(false);
}
if (DOM.lastByte) DOM.lastByte.textContent = "--";
if (consecutiveErrors >= MAX_ERRORS && pollTimer) {
clearInterval(pollTimer);
}
}
}
function startPolling(): void {
if (pollTimer) clearInterval(pollTimer);
if (consecutiveErrors > 0) update();
pollTimer = window.setInterval(update, 500);
if (pollTimer) clearInterval(pollTimer);
if (consecutiveErrors > 0) update();
pollTimer = window.setInterval(update, 500);
}
window.addEventListener("focus", () => {
if (consecutiveErrors >= MAX_ERRORS) {
consecutiveErrors = 0;
startPolling();
}
if (consecutiveErrors >= MAX_ERRORS) {
consecutiveErrors = 0;
startPolling();
}
});
window.addEventListener("beforeunload", () => {
audioEngine.dispose();
audioEngine.dispose();
});
declare global {
interface Window {
_mutedLevel?: number;
}
interface Window {
_mutedLevel?: number;
}
}
startPolling();
update();
update();

View File

@@ -1,23 +1,24 @@
// state.ts - Peak Holder алгоритм с таймером
// state.ts - Peak Holder с сбросом до текущего максимума
interface AppState {
maxPoints: number;
signalHistory: number[];
lastBufferSize: number;
// Peak Holder
// Peak Holder для амплитуды
peakValue: number; // Текущее удерживаемое пиковое значение амплитуды
currentValue: number; // Текущее реальное значение амплитуды
currentValue: number; // Текущий максимум из буфера
peakTimer: number | null; // Таймер для сброса пика амплитуды
peakHoldTime: number; // Время удержания пика в миллисекундах
// Peak Holder для уровня сигнала
peakSignalLevel: number;
currentSignalLevel: number;
currentSignalLevel: number; // Текущий максимум уровня из буфера
signalPeakTimer: number | null;
signalPeakHoldTime: number;
hasPeak: boolean;
lastMaxSignal: number;
}
type PeakHolderListener = () => void;
@@ -30,7 +31,7 @@ export const state: AppState = {
peakValue: 0,
currentValue: 0,
peakTimer: null,
peakHoldTime: 3000, // 3 секунды
peakHoldTime: 3000,
peakSignalLevel: 0,
currentSignalLevel: 0,
@@ -38,6 +39,7 @@ export const state: AppState = {
signalPeakHoldTime: 3000,
hasPeak: false,
lastMaxSignal: 0,
};
let peakHolderListener: PeakHolderListener | null = null;
@@ -46,20 +48,95 @@ let peakHolderListener: PeakHolderListener | null = null;
// ПУБЛИЧНЫЕ ФУНКЦИИ
// ============================================
export function setPeakHolderListener(listener: PeakHolderListener | null): void {
peakHolderListener = listener;
}
function notifyPeakHolderChange(): void {
if (peakHolderListener) {
peakHolderListener();
}
}
/**
* Установка времени удержания пика сигнала
* @param ms - время в миллисекундах
* Обновление пикового значения амплитуды
* @param currentMax - текущий максимум из буфера (в процентах 0-100)
* @returns пиковое значение
*/
// noinspection JSUnusedGlobalSymbols
export function updatePeakHolder(currentMax: number): number {
state.currentValue = currentMax;
state.lastMaxSignal = currentMax;
if (currentMax >= state.peakValue) {
state.peakValue = currentMax;
state.hasPeak = true;
scheduleAmplitudePeakRelease();
notifyPeakHolderChange();
return state.peakValue;
}
return state.peakValue;
}
/**
* Обновление пикового уровня сигнала
* @param currentMaxLevel - текущий максимум уровня из буфера (0-3)
* @returns пиковый уровень
*/
export function updateSignalPeakHolder(currentMaxLevel: number): number {
state.currentSignalLevel = currentMaxLevel;
if (currentMaxLevel >= state.peakSignalLevel) {
state.peakSignalLevel = currentMaxLevel;
state.hasPeak = true;
scheduleSignalPeakRelease();
notifyPeakHolderChange();
return state.peakSignalLevel;
}
return state.peakSignalLevel;
}
// ============================================
// ВНУТРЕННИЕ ФУНКЦИИ
// ============================================
function scheduleAmplitudePeakRelease(): void {
if (state.peakTimer !== null) {
clearTimeout(state.peakTimer);
state.peakTimer = null;
}
state.peakTimer = window.setTimeout(() => {
state.peakValue = state.currentValue;
state.peakTimer = null;
state.hasPeak = false;
notifyPeakHolderChange();
}, state.peakHoldTime);
}
function scheduleSignalPeakRelease(): void {
if (state.signalPeakTimer !== null) {
clearTimeout(state.signalPeakTimer);
state.signalPeakTimer = null;
}
state.signalPeakTimer = window.setTimeout(() => {
state.peakSignalLevel = state.currentSignalLevel;
state.signalPeakTimer = null;
state.hasPeak = false;
notifyPeakHolderChange();
}, state.signalPeakHoldTime);
}
// ============================================
// ОСТАЛЬНЫЕ ФУНКЦИИ
// ============================================
export function setSignalPeakHoldTime(ms: number): void {
state.signalPeakHoldTime = ms;
}
/**
* Получение информации о пике сигнала
* @returns { peak: number; current: number; timerActive: boolean }
*/
// noinspection JSUnusedGlobalSymbols
export function getSignalPeakInfo(): { peak: number; current: number; timerActive: boolean } {
return {
peak: state.peakSignalLevel,
@@ -68,11 +145,6 @@ export function getSignalPeakInfo(): { peak: number; current: number; timerActiv
};
}
/**
* Добавление нового значения сигнала в историю
* @param value - значение сигнала
*/
// noinspection JSUnusedGlobalSymbols
export function addSignal(value: number): void {
state.signalHistory.push(value);
if (state.signalHistory.length > state.maxPoints) {
@@ -80,65 +152,6 @@ export function addSignal(value: number): void {
}
}
/**
* Установка слушателя для обновления пика
* @param listener - функция-слушатель
*/
// noinspection JSUnusedGlobalSymbols
export function setPeakHolderListener(listener: PeakHolderListener | null): void {
peakHolderListener = listener;
}
/**
* Обновление пикового значения амплитуды
* @param target - текущее значение
* @returns пиковое значение
*/
// noinspection JSUnusedGlobalSymbols
export function updatePeakHolder(target: number): number {
// Обновляем текущее значение
state.currentValue = target;
// Если новое значение больше текущего пика
if (target > state.peakValue) {
// Устанавливаем новый пик
state.peakValue = target;
state.hasPeak = true;
scheduleAmplitudePeakRelease();
} else if (target < state.peakValue && state.peakTimer === null) {
// Пик выше текущего значения, но таймер пропал — перезапускаем удержание
scheduleAmplitudePeakRelease();
} // Возвращаем текущий пик
return state.peakValue;
}
/**
* Обновление пикового уровня сигнала
* @param level - текущий уровень
* @returns пиковый уровень
*/
// noinspection JSUnusedGlobalSymbols
export function updateSignalPeakHolder(level: number): number {
// Обновляем текущее значение
state.currentSignalLevel = level;
// Если новый уровень больше пика — устанавливаем новый пик
if (level > state.peakSignalLevel) {
state.peakSignalLevel = level;
state.hasPeak = true;
scheduleSignalPeakRelease();
} else if (level < state.peakSignalLevel && state.signalPeakTimer === null) {
scheduleSignalPeakRelease();
}
// Возвращаем текущий пик
return state.peakSignalLevel;
}
/**
* Принудительный сброс пика сигнала
*/
// noinspection JSUnusedGlobalSymbols
export function forceResetSignalPeak(): void {
if (state.signalPeakTimer !== null) {
clearTimeout(state.signalPeakTimer);
@@ -147,12 +160,10 @@ export function forceResetSignalPeak(): void {
state.peakSignalLevel = 0;
state.currentSignalLevel = 0;
state.hasPeak = false;
state.lastMaxSignal = 0;
notifyPeakHolderChange();
}
/**
* Принудительный сброс всех пиков
*/
// noinspection JSUnusedGlobalSymbols
export function forceResetPeak(): void {
if (state.peakTimer !== null) {
clearTimeout(state.peakTimer);
@@ -161,15 +172,11 @@ export function forceResetPeak(): void {
state.peakValue = 0;
state.currentValue = 0;
state.hasPeak = false;
state.lastMaxSignal = 0;
forceResetSignalPeak();
notifyPeakHolderChange();
}
/**
* Добавление массива значений сигнала
* @param values - массив значений
* @param bufferSize - размер буфера
*/
// noinspection JSUnusedGlobalSymbols
export function addSignals(values: number[], bufferSize: number): void {
if (!Array.isArray(values) || values.length === 0) return;
@@ -188,55 +195,7 @@ export function addSignals(values: number[], bufferSize: number): void {
}
}
/**
* Сброс отслеживания буфера
*/
// noinspection JSUnusedGlobalSymbols
export function resetBufferTracking(): void {
state.lastBufferSize = 0;
forceResetPeak();
}
// ============================================
// ВНУТРЕННИЕ ФУНКЦИИ
// ============================================
function notifyPeakHolderChange(): void {
if (peakHolderListener) {
peakHolderListener();
}
}
function scheduleAmplitudePeakRelease(): void {
// Сбрасываем старый таймер
if (state.peakTimer !== null) {
clearTimeout(state.peakTimer);
state.peakTimer = null;
}
// Запускаем новый таймер на 3 секунды
state.peakTimer = window.setTimeout(() => {
state.peakValue = state.currentValue;
state.peakTimer = null;
state.hasPeak = false;
console.log(`[Peak] Сброс пика до: ${state.peakValue}`);
notifyPeakHolderChange();
}, state.peakHoldTime);
}
function scheduleSignalPeakRelease(): void {
// Сбрасываем старый таймер
if (state.signalPeakTimer !== null) {
clearTimeout(state.signalPeakTimer);
state.signalPeakTimer = null;
}
// Запускаем новый таймер на 3 секунды
state.signalPeakTimer = window.setTimeout(() => {
// Сбрасываем пик к текущему уровню
state.peakSignalLevel = state.currentSignalLevel;
state.signalPeakTimer = null;
state.hasPeak = false;
notifyPeakHolderChange();
}, state.signalPeakHoldTime);
}

View File

@@ -1,5 +1,4 @@
// ui.ts - ИСПРАВЛЕННАЯ ВЕРСИЯ
// Канал 1 - реальные данные, каналы 2-8 - неактивны
// ui.ts - Управление UI элементами
export interface DOMElements {
serverTime: HTMLElement | null;
@@ -95,7 +94,6 @@ export function setBars(p: number, noConnection: boolean = false): void {
el.classList.remove("no-connection");
const percent = Math.max(0, Math.min(100, p));
el.style.width = `${percent}%`;
// Принудительно устанавливаем рыжий цвет
el.style.background = '#e6852d';
el.style.backgroundColor = '#e6852d';
el.style.backgroundImage = 'none';
@@ -182,7 +180,7 @@ export const channels: ChannelState[] = Array(8).fill(null).map(() => ({
expanded: true
}));
let channelElements: any[] = [];
export let channelElements: any[] = [];
export function initChannels() {
const container = document.getElementById('channelsContainer');
@@ -196,7 +194,6 @@ export function initChannels() {
item.className = 'channel-item';
item.dataset.channel = String(i);
// Только канал 1 активен по умолчанию
if (i === 0) {
item.classList.add('channel-active');
}
@@ -259,15 +256,16 @@ export function toggleChannel(index: number) {
}
}
export function getChannelElement(index: number) {
return channelElements[index] || null;
}
export function setChannelValue(index: number, value: number, strength: number) {
if (index < 0 || index > 7) return;
const ch = channels[index];
const el = channelElements[index];
if (!el) {
console.warn(`[UI] Channel ${index} element not found`);
return;
}
if (!el) return;
ch.value = value;
ch.strength = strength;
@@ -275,7 +273,6 @@ export function setChannelValue(index: number, value: number, strength: number)
const maxValue = 63;
const percent = (value / maxValue) * 100;
// Принудительно устанавливаем стили для прогресс-бара
if (el.bar) {
el.bar.style.width = `${Math.min(100, percent)}%`;
el.bar.style.background = '#e6852d';
@@ -305,13 +302,7 @@ export function setChannelValue(index: number, value: number, strength: number)
}
}
/**
* ОБНОВЛЕНИЕ ВСЕХ КАНАЛОВ
* Канал 1 - реальные данные из эмулятора
* Каналы 2-8 - неактивны (0)
*/
export function updateAllChannels(rawData?: number[]) {
// Сначала сбрасываем все каналы в 0
for (let i = 0; i < 8; i++) {
setChannelValue(i, 0, 0);
if (channelElements[i]) {
@@ -319,52 +310,28 @@ export function updateAllChannels(rawData?: number[]) {
}
}
// Если есть данные - обновляем КАНАЛ 1
if (rawData && rawData.length > 0) {
// Берем последний байт (самый свежий)
const lastByte = rawData[rawData.length - 1];
const value = lastByte & 0x3F;
const strength = (lastByte >> 6) & 0x3;
// ПРАВИЛЬНЫЙ ПАРСИНГ:
// Биты 0-5: значение (количество сигналов) 0-63
// Биты 6-7: уровень сигнала (амплитуда/сила) 0-3
const value = lastByte & 0x3F; // маска 0b00111111 → 0-63
const strength = (lastByte >> 6) & 0x3; // маска 0b00000011 → 0-3
// Обновляем КАНАЛ 1 с реальными данными
setChannelValue(0, value, strength);
if (channelElements[0]) {
channelElements[0].item.classList.add('channel-active');
}
// Обновляем last-byte
if (DOM.lastByte) {
DOM.lastByte.textContent = value.toString();
}
// Обновляем основной прогресс-бар (для синхронизации)
const percent = (value / 63) * 100;
if (DOM.barFill) {
DOM.barFill.style.width = `${percent}%`;
DOM.barFill.style.background = '#e6852d';
DOM.barFill.style.backgroundColor = '#e6852d';
DOM.barFill.style.backgroundImage = 'none';
}
} else {
// Если данных нет - канал 1 тоже неактивен
if (DOM.lastByte) {
DOM.lastByte.textContent = '--';
}
if (DOM.barFill) {
DOM.barFill.style.width = '0%';
}
}
}
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', () => {
initChannels();
// Принудительно обновляем каналы при загрузке
updateAllChannels();
const expandAll = document.getElementById('expandAllBtn');