Генератор прототипов 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>
155 lines
9.5 KiB
C
155 lines
9.5 KiB
C
#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>)====="
|