Добавление камеры на сервер

This commit is contained in:
Tot Maxim
2026-05-09 18:20:00 +03:00
parent 549b0c5e63
commit e36823d3ba
6 changed files with 420 additions and 37 deletions

View File

@@ -16,6 +16,7 @@ import (
)
// ===== EMBED WEB =====
//
//go:embed web/*
var webFiles embed.FS
@@ -75,6 +76,12 @@ func (a *API) HandleHistory(w http.ResponseWriter, r *http.Request) {
})
}
func (a *API) HandleStream(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]string{
"cam": "http://192.168.77.1:1984/stream.html?src=cam",
})
}
// ===== PIPE READER =====
func startPipeReader(ctx context.Context, buf *adapter.RingBuffer, path string) {
@@ -137,7 +144,6 @@ func main() {
// ===== API =====
http.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/latest":
cors(api.HandleLatest)(w, r)
@@ -145,6 +151,8 @@ func main() {
cors(api.HandleHistory)(w, r)
case "/health":
cors(api.HandleHealth)(w, r)
case "/stream":
cors(api.HandleStream)(w, r)
default:
http.Error(w, "not found", 404)
}

View File

@@ -224,6 +224,165 @@ h1 {
opacity: 0;
}
/* ОТКРЫТОЕ СОСТОЯНИЕ */
.cam-modal.open {
transform: translateX(0);
}
.cam-header {
display: flex;
justify-content: space-between;
padding: 8px;
color: white;
background: #222;
}
.cam-frame {
flex: 1;
width: 100%;
border: none;
object-fit: contain;
background: #000;
min-height: 200px;
}
.cam-loading {
display: flex;
align-items: center;
justify-content: center;
color: #00ff88;
font-size: 18px;
}
.hidden {
display: none;
}
/* ===== SPLIT LAYOUT ===== */
.layout {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.panel {
height: 100%;
overflow: auto;
}
.left {
flex: 1;
min-width: 300px;
transition: all 0.3s ease;
}
.right {
flex: 0 0 40%;
min-width: 300px;
transition: all 0.3s ease;
}
.right.hidden {
flex: 0 0 0 !important;
min-width: 0 !important;
width: 0 !important;
overflow: hidden;
opacity: 0;
}
/* DRAG BAR */
.resizer {
width: 6px;
cursor: col-resize;
background: #00ff88;
opacity: 0.25;
transition: opacity 0.2s;
}
.resizer:hover {
opacity: 1;
}
.resizer.hidden {
display: none;
}
/* ===== CAMERA ===== */
.cam-modal {
height: 100%;
display: flex;
flex-direction: column;
background: black;
}
.cam-header {
display: flex;
justify-content: space-between;
padding: 8px;
background: #111;
color: #00ff88;
}
.cam-frame {
flex: 1;
width: 100%;
object-fit: contain;
background: black;
}
.cam-loading {
display: flex;
align-items: center;
justify-content: center;
color: #00ff88;
}
.cam-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 1001;
padding: 10px 15px;
background: rgba(0, 0, 0, 0.8);
color: #00ff88;
border: 1px solid #00ff88;
border-radius: 8px;
cursor: pointer;
font-family: monospace;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.cam-btn:hover {
background: rgba(0, 255, 136, 0.2);
box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
}
.cam-btn-icon {
font-size: 18px;
}
/* Панель камеры скрыта */
.panel.right[style*="display: none"] {
display: none !important;
}
/* Когда камера скрыта, левая панель на весь экран */
.layout:has(.panel.right[style*="display: none"]) .left {
flex: 1;
}
.layout:has(.panel.right[style*="display: none"]) .resizer {
display: none;
}
/* ===== MOBILE ===== */
@media (max-width: 500px) {
h1 {

View File

@@ -13,55 +13,81 @@
<h1>GPIO МОНИТОРИНГ В РЕАЛЬНОМ ВРЕМЕНИ</h1>
<div class="grid">
<!-- Кнопка переключения камеры -->
<button id="camToggle" class="cam-btn">
<span class="cam-btn-icon">📷</span>
<span class="cam-btn-text">Камера</span>
</button>
<div class="card">
<h3>СОСТОЯНИЕ</h3>
<div id="status">...</div>
<div id="meta"></div>
</div>
<div class="layout">
<!-- LEFT: графики / дашборд -->
<div class="panel left" id="leftPanel">
<div class="grid">
<div class="card wide">
<h3>УРОВНИ КАНАЛОВ</h3>
<div class="card">
<h3>СОСТОЯНИЕ</h3>
<div id="status">...</div>
<div id="meta"></div>
</div>
<div class="progress-row">
<div class="progress-label">Канал 1 — <span id="last-byte">0</span></div>
<div class="progress-with-signal">
<div class="progress progress-90">
<div class="fill" id="bar1"></div>
<div class="card wide">
<h3>УРОВНИ КАНАЛОВ</h3>
<div class="progress-row">
<div class="progress-label">Канал 1 — <span id="last-byte">0</span></div>
<div class="progress-with-signal">
<div class="progress progress-90">
<div class="fill" id="bar1"></div>
</div>
<div class="signal-icon" id="signal-icon" aria-label="Уровень сигнала" role="img">
<div class="sig-bar sig-bar-1"></div>
<div class="sig-bar sig-bar-2"></div>
<div class="sig-bar sig-bar-3"></div>
</div>
</div>
</div>
<div class="signal-icon" id="signal-icon" aria-label="Уровень сигнала" role="img">
<div class="sig-bar sig-bar-1"></div>
<div class="sig-bar sig-bar-2"></div>
<div class="sig-bar sig-bar-3"></div>
<div class="progress-row">
<div class="progress-label">Звук</div>
<div class="progress">
<div class="fill" id="bar2"></div>
</div>
</div>
<div class="progress-row">
<div class="progress-label">Высота</div>
<div class="progress">
<div class="fill" id="bar3"></div>
</div>
</div>
</div>
</div>
<div class="progress-row">
<div class="progress-label">Звук</div>
<div class="progress">
<div class="fill" id="bar2"></div>
<div class="card wide">
<h3>ГРАФИК КАНАЛА 1</h3>
<canvas id="signalChart" height="120"></canvas>
</div>
</div>
<div class="progress-row">
<div class="progress-label">Высота</div>
<div class="progress">
<div class="fill" id="bar3"></div>
<div class="card wide">
<h3 class="accordion-header" id="history-header">▶ ПРИНЯТЫЕ ДАННЫЕ</h3>
<div class="accordion-body" id="history-body">
<div id="history"></div>
</div>
</div>
</div>
</div>
<div class="card wide">
<h3>ГРАФИК КАНАЛА 1</h3>
<canvas id="signalChart" height="120"></canvas>
</div>
<!-- DRAG HANDLE -->
<div class="resizer" id="dragBar"></div>
<div class="card wide">
<h3 class="accordion-header" id="history-header">▶ ПРИНЯТЫЕ ДАННЫЕ</h3>
<div class="accordion-body" id="history-body">
<div id="history"></div>
<!-- RIGHT: камера -->
<div class="panel right" id="rightPanel">
<div class="cam-modal">
<div class="cam-header">
<span>Camera</span>
<button id="camClose"></button>
</div>
<img id="camImage" src="" alt="Camera" class="cam-frame">
</div>
</div>
@@ -70,4 +96,4 @@
<script type="module" src="/js/app.js"></script>
</body>
</html>
</html>

View File

@@ -12,6 +12,8 @@ import {
import { initAccordion } from "./accordion.js";
import { drawChart } from "./chart.js";
import { initResize } from "./layout.js";
import { initCamera } from "./camera.js";
let lastChart = 0;
let connectionLost = false;
@@ -23,6 +25,29 @@ let pollTimer = null;
window.addEventListener("DOMContentLoaded", () => {
initDOM();
initAccordion();
initCamera();
initResize();
});
document.addEventListener("keydown", (e) => {
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";
applyRatio(0.6); // 60% левая, 40% правая
break;
}
});
// ================= UPDATE =================

View File

@@ -0,0 +1,73 @@
//camera.js
let rightPanel, camImage, camClose;
let isOpen = false;
let streamUrl;
export function initCamera() {
rightPanel = document.getElementById("rightPanel");
camImage = document.getElementById("camImage");
camClose = document.getElementById("camClose");
const btn = document.getElementById("camToggle");
streamUrl = "http://192.168.77.1:1984/api/stream.mjpeg?src=cam";
function open() {
if (isOpen) return;
isOpen = true;
rightPanel.classList.remove("hidden");
camImage.src = streamUrl + "&t=" + Date.now();
// Обновляем текст кнопки
if (btn) {
btn.querySelector('.cam-btn-text').textContent = 'Скрыть';
}
console.log("[camera] Камера открыта");
updateResizerVisibility();
}
function close() {
if (!isOpen) return;
isOpen = false;
camImage.src = "about:blank";
setTimeout(() => {
camImage.src = "";
}, 50);
rightPanel.classList.add("hidden");
// Обновляем текст кнопки
if (btn) {
btn.querySelector('.cam-btn-text').textContent = 'Камера';
}
console.log("[camera] Камера закрыта");
updateResizerVisibility();
}
function toggle() {
isOpen ? close() : open();
}
// Обработчики событий
btn?.addEventListener("click", toggle);
camClose?.addEventListener("click", close);
// Горячие клавиши
document.addEventListener("keydown", (e) => {
if (e.key === 'c' || e.key === 'C') {
toggle();
}
if (e.key === 'Escape' && isOpen) {
close();
}
});
// Автозапуск камеры
open();
console.log("[camera] Модуль камеры инициализирован");
}

View File

@@ -0,0 +1,92 @@
//layout.js
let leftPanel, rightPanel, dragBar;
let isDragging = false;
export function initResize() {
leftPanel = document.getElementById("leftPanel");
rightPanel = document.getElementById("rightPanel");
dragBar = document.getElementById("dragBar");
if (!leftPanel || !rightPanel || !dragBar) return;
// Загрузка сохранённых пропорций
const savedRatio = localStorage.getItem('layoutRatio');
if (savedRatio) {
const ratio = parseFloat(savedRatio);
applyRatio(ratio);
}
dragBar.addEventListener("mousedown", (e) => {
isDragging = true;
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
e.preventDefault();
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
document.body.style.cursor = "default";
document.body.style.userSelect = "";
// Сохраняем пропорции
const total = window.innerWidth;
const leftWidth = leftPanel.getBoundingClientRect().width;
const ratio = leftWidth / total;
localStorage.setItem('layoutRatio', ratio.toString());
}
});
// ЕДИНСТВЕННЫЙ обработчик mousemove
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
if (rightPanel.classList.contains("hidden")) return; // Не ресайзим если камера скрыта
const total = window.innerWidth;
let leftWidth = e.clientX;
// Минимальные размеры
const minWidth = 300;
if (leftWidth < minWidth) leftWidth = minWidth;
if (leftWidth > total - minWidth) leftWidth = total - minWidth;
const rightWidth = total - leftWidth - dragBar.offsetWidth;
leftPanel.style.flex = "none";
rightPanel.style.flex = "none";
leftPanel.style.width = leftWidth + "px";
rightPanel.style.width = rightWidth + "px";
});
// Адаптация при ресайзе окна
window.addEventListener("resize", () => {
if (rightPanel.classList.contains("hidden")) return;
const total = window.innerWidth;
const leftWidth = leftPanel.getBoundingClientRect().width;
const ratio = leftWidth / total;
if (ratio > 0.2 && ratio < 0.8) {
applyRatio(ratio);
}
});
}
function applyRatio(ratio) {
const total = window.innerWidth;
const leftWidth = Math.floor(total * ratio);
const rightWidth = total - leftWidth - dragBar.offsetWidth;
leftPanel.style.flex = "none";
rightPanel.style.flex = "none";
leftPanel.style.width = leftWidth + "px";
rightPanel.style.width = rightWidth + "px";
}
export function updateResizerVisibility() {
if (!dragBar) return;
if (rightPanel?.classList.contains("hidden")) {
dragBar.classList.add("hidden");
} else {
dragBar.classList.remove("hidden");
}
}