Files
Arduino/pump_controller_8_2_OLED_DONE/web_page.h
T
oskarvitaliiandClaude Opus 5 6240862c27 fix(build): вынести HTML в web_page.h - сборка через arduino-cli падала
Генератор прототипов Arduino не понимает сырые строковые литералы: внутри
JS он нашёл "function draw(d){" и "function poll(){", принял их за
определения функций C++ и вставил для них прототипы в начало файла.
Сборка падала с "error: 'function' does not name a type", а директивы
#line показывали ошибку на строках внутри HTML.

Заголовочные файлы препроцессор не сканирует, поэтому страница переехала
в web_page.h. Тот же приём используется в соседнем проекте.

Ошибка не всплывала раньше потому, что я проверял скетч через
g++ -fsyntax-only по .ino: так файл компилируется как обычный .cpp и
препроцессор Arduino не участвует. Проверять сборку нужно arduino-cli,
это записано в README 11.1.

Прошито и проверено на железе (COM7, LOLIN D1 ESP-WROOM-02, 2MB FS:64KB):
размеры флеша совпадают, LittleFS монтируется, ранее заданные через панель
настройки пережили перепрошивку, Wi-Fi поднимается, датчики читаются.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 01:08:17 +07:00

155 lines
9.5 KiB
C
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.
#pragma once
// ============================================================
// ВЕБ-ПАНЕЛЬ
// ============================================================
// Страница статическая и лежит целиком во флеше (PROGMEM). Состояние в неё
// подставляет JS, забирая /status и /config. Раньше HTML собирался строкой
// в куче на каждый запрос (~14 КБ при ~40 КБ свободной кучи) — теперь куча
// не тратится вообще, а ввод в форме не сбрасывается при обновлении данных.
//
// Дизайн — световое табло аварийной сигнализации, а не дашборд: главный ответ
// («есть подача или нет») занимает верх страницы и читается с расстояния,
// остальное подчинено ему. Цвета по конвенции сигнальных ламп: зелёная
// «работа», ЖЁЛТАЯ «внимание» для резерва (резерв в работе — предупреждение,
// система ещё справляется), красная «авария» только при остановке основных.
//
// Веб-шрифты намеренно не подключаются: сеть на объекте не разрешает внешние
// имена, и @import с Google Fonts там не грузится никогда. Характер задан
// контрастом кеглей, а не гарнитурой.
static const char PAGE_HTML[] PROGMEM = R"=====(<!DOCTYPE html><html lang="ru"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Насосная</title><link id="fav" rel="icon" href="data:,">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0e1216;--sf:#161b21;--ln:#232a32;--ink:#dde3e9;--dim:#8b97a3;
--run:#35c46a;--warn:#f0a516;--flt:#e23c2e;--off:#2a323a}
body{background:var(--bg);color:var(--ink);min-height:100vh;padding:16px;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;
font-size:15px;line-height:1.4;display:flex;justify-content:center}
.wrap{width:100%;max-width:520px;display:flex;flex-direction:column;gap:10px}
.top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
font-size:13px;color:var(--dim)}
.top b{font-weight:600;color:var(--ink);font-size:14px}
.ip{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px}
.st{border-radius:14px;padding:20px;border:1px solid var(--ln);background:var(--sf)}
.st b{display:block;font-size:clamp(28px,8.5vw,44px);line-height:1.05;
letter-spacing:-.02em;font-weight:800}
.st span{display:block;margin-top:6px;font-size:14px;color:var(--dim)}
.st.ok{background:rgba(53,196,106,.10);border-color:rgba(53,196,106,.35)}
.st.ok b{color:var(--run)}
.st.warn{background:rgba(240,165,22,.10);border-color:rgba(240,165,22,.4)}
.st.warn b{color:var(--warn)}
.st.flt{background:rgba(226,60,46,.12);border-color:rgba(226,60,46,.45);
animation:beat 1.6s ease-in-out infinite}
.st.flt b{color:var(--flt)}
@keyframes beat{50%{background:rgba(226,60,46,.26)}}
@media(prefers-reduced-motion:reduce){.st.flt{animation:none}}
.row{display:grid;gap:8px}
.r3{grid-template-columns:repeat(3,1fr)}
.r2{grid-template-columns:repeat(2,1fr)}
.c{background:var(--sf);border:1px solid var(--ln);border-radius:12px;padding:12px}
.c i{display:block;width:13px;height:13px;border-radius:50%;background:var(--off);
margin-bottom:10px;transition:background .2s}
.c.on i{background:var(--run);box-shadow:0 0 10px rgba(53,196,106,.7)}
.c.on.w i{background:var(--warn);box-shadow:0 0 10px rgba(240,165,22,.7)}
.c.on.f i{background:var(--flt);box-shadow:0 0 10px rgba(226,60,46,.7)}
.c em{font-style:normal;display:block;font-size:11px;letter-spacing:.08em;
text-transform:uppercase;color:var(--dim)}
.c b{display:block;margin-top:3px;font-size:14px;font-weight:600}
details{background:var(--sf);border:1px solid var(--ln);border-radius:12px}
summary{padding:12px 14px;cursor:pointer;font-size:14px;font-weight:600;
list-style:none;display:flex;justify-content:space-between;align-items:center}
summary::-webkit-details-marker{display:none}
summary::after{content:"▾";color:var(--dim)}
details[open] summary::after{content:"▴"}
.body{padding:0 14px 14px;display:flex;flex-direction:column;gap:12px}
label{display:block;font-size:12px;color:var(--dim);margin-bottom:5px}
input{width:100%;padding:9px 10px;background:#0f1418;border:1px solid var(--ln);
border-radius:8px;color:var(--ink);font:inherit;font-size:14px}
input:focus{outline:2px solid rgba(53,196,106,.5);outline-offset:1px}
.two{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.hint{font-size:12px;color:var(--dim)}
button{width:100%;padding:11px;border-radius:8px;border:1px solid;font:inherit;
font-size:14px;font-weight:600;cursor:pointer}
.save{background:rgba(53,196,106,.14);border-color:rgba(53,196,106,.45);color:var(--run)}
.wifi{background:transparent;border-color:var(--ln);color:var(--dim);margin-top:2px}
.warnbox{background:rgba(226,60,46,.12);border:1px solid rgba(226,60,46,.4);
border-radius:8px;padding:10px;font-size:13px;color:#f2a49c}
.hide{display:none}
</style></head><body><div class="wrap">
<div class="top"><b id="dev"></b><span class="ip" id="ip"></span></div>
<div class="st init" id="st"><b id="word">Связь…</b><span id="sub">получаем состояние прибора</span></div>
<div class="row r3">
<div class="c" id="l1"><i></i><em>Насос 1</em><b></b></div>
<div class="c" id="l2"><i></i><em>Насос 2</em><b></b></div>
<div class="c w" id="lE"><i></i><em>Резерв</em><b></b></div>
</div>
<div class="row r2">
<div class="c f" id="o1"><i></i><em>Выход «Авария»</em><b></b></div>
<div class="c w" id="o2"><i></i><em>Выход «Резерв»</em><b></b></div>
</div>
<details><summary>Настройки</summary><div class="body">
<div class="warnbox hide" id="fsw">Память недоступна настройки не сохранятся до перезагрузки.</div>
<form method="POST" action="/set_config">
<div><label for="u">Адрес для уведомлений</label>
<input id="u" name="url" type="text" placeholder="оставьте пустым, чтобы не менять">
<div class="hint" id="uh" style="margin-top:5px"></div></div>
<div style="margin-top:12px"><label for="d">Имя прибора в уведомлениях</label>
<input id="d" name="device" maxlength="31">
<div class="hint" style="margin-top:5px">Латиница, цифры, дефис и подчёркивание</div></div>
<div style="margin-top:12px"><label>Задержка сигнала, секунд</label>
<div class="two">
<div><input id="a" name="alarm_delay" type="number" min="0" max="3600" aria-label="Задержка аварии">
<div class="hint" style="margin-top:5px">аварии</div></div>
<div><input id="e" name="emerg_delay" type="number" min="0" max="3600" aria-label="Задержка резерва">
<div class="hint" style="margin-top:5px">резерва</div></div></div></div>
<button class="save" type="submit" style="margin-top:14px">Сохранить</button>
</form>
<form method="POST" action="/reset_wifi"><button class="wifi" type="submit">Сменить сеть Wi-Fi</button></form>
</div></details>
</div><script>
var F={ok:"#35c46a",warn:"#f0a516",flt:"#e23c2e",init:"#8b97a3"};
function lamp(id,on,txt){var e=document.getElementById(id);
e.classList.toggle("on",!!on);e.querySelector("b").textContent=txt}
function fav(c){document.getElementById("fav").href="data:image/svg+xml,"+
encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='13' fill='"+c+"'/></svg>")}
var dev="";
function draw(d){
lamp("l1",d.p1,d.p1?"работает":"стоит");
lamp("l2",d.p2,d.p2?"работает":"стоит");
lamp("lE",d.pE,d.pE?"работает":"ожидание");
lamp("o1",d.rMain,d.rMain?"замкнут":"разомкнут");
lamp("o2",d.rEmg,d.rEmg?"замкнут":"разомкнут");
var k,w,s;
if(!d.rdy){k="init";w="Запуск";s="идёт проверка датчиков"}
else if(!d.p1&&!d.p2){k="flt";w="Насосы стоят";
s=d.pE?"подачу держит резерв":"подачи нет"}
else if(d.pE){k="warn";w="Работает резерв";s="основные насосы в работе"}
else{k="ok";w="Подача есть";
s=d.p1&&d.p2?"работают оба насоса":(d.p1?"работает насос 1":"работает насос 2")}
var st=document.getElementById("st");
st.className="st "+k;
document.getElementById("word").textContent=w;
document.getElementById("sub").textContent=s;
document.title=w+(dev?" · "+dev:"");
fav(F[k])}
function poll(){fetch("/status").then(function(r){return r.json()}).then(draw).catch(function(){})}
fetch("/config").then(function(r){return r.json()}).then(function(c){
dev=c.device;document.getElementById("dev").textContent=c.device;
document.getElementById("ip").textContent=c.ip;
document.getElementById("d").value=c.device;
document.getElementById("a").value=c.alarm;
document.getElementById("e").value=c.emerg;
document.getElementById("uh").textContent=c.host?"Сейчас: "+c.host:"Не задан — уведомления отключены";
if(!c.fs)document.getElementById("fsw").classList.remove("hide")}).catch(function(){});
poll();setInterval(poll,2000);
</script></body></html>)====="