Skip to content

Commit 676ded7

Browse files
authored
Add HTML configuration generator for client setup and sensor management
1 parent 231d4d7 commit 676ded7

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed

TankAlarm-112025-Server-BluesOpta/TankAlarm-112025-Server-BluesOpta.ino

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,262 @@ static size_t strlcpy(char *dst, const char *src, size_t size) {
179179
}
180180
#endif
181181

182+
static const char CONFIG_GENERATOR_HTML[] PROGMEM = R"HTML(
183+
<!DOCTYPE html>
184+
<html lang="en">
185+
<head>
186+
<meta charset="utf-8">
187+
<meta name="viewport" content="width=device-width, initial-scale=1">
188+
<title>Config Generator</title>
189+
<style>
190+
:root { color-scheme: light dark; font-family: "Segoe UI", Arial, sans-serif; }
191+
body { margin: 0; background: #f4f6f8; color: #1f2933; }
192+
header { padding: 16px 24px; background: #1d3557; color: #fff; box-shadow: 0 2px 6px rgba(0,0,0,0.2); display: flex; justify-content: space-between; align-items: center; }
193+
header h1 { margin: 0; font-size: 1.6rem; }
194+
header a { color: #fff; text-decoration: none; font-size: 0.95rem; }
195+
main { padding: 20px; max-width: 800px; margin: 0 auto; }
196+
.card { background: #fff; border-radius: 12px; box-shadow: 0 10px 30px rgba(15,23,42,0.08); padding: 20px; }
197+
h2 { margin-top: 0; font-size: 1.3rem; }
198+
h3 { margin: 20px 0 10px; font-size: 1.1rem; border-bottom: 1px solid #e2e8f0; padding-bottom: 6px; }
199+
.field { display: flex; flex-direction: column; margin-bottom: 12px; }
200+
.field span { font-size: 0.9rem; color: #475569; margin-bottom: 4px; }
201+
.field input, .field select { padding: 8px 10px; border-radius: 6px; border: 1px solid #cbd5f5; font-size: 0.95rem; }
202+
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
203+
.sensor-card { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; margin-bottom: 16px; position: relative; }
204+
.sensor-header { display: flex; justify-content: space-between; margin-bottom: 12px; }
205+
.sensor-title { font-weight: 600; color: #334155; }
206+
.remove-btn { color: #ef4444; cursor: pointer; font-size: 0.9rem; border: none; background: none; padding: 0; }
207+
.actions { margin-top: 24px; display: flex; gap: 12px; }
208+
button { border: none; border-radius: 6px; padding: 10px 16px; font-size: 0.95rem; cursor: pointer; background: #1d4ed8; color: #fff; }
209+
button.secondary { background: #64748b; }
210+
button:hover { opacity: 0.9; }
211+
</style>
212+
</head>
213+
<body>
214+
<header>
215+
<h1>Config Generator</h1>
216+
<a href="/">&larr; Back to Dashboard</a>
217+
</header>
218+
<main>
219+
<div class="card">
220+
<h2>New Client Configuration</h2>
221+
<form id="generatorForm">
222+
<div class="form-grid">
223+
<label class="field"><span>Site Name</span><input id="siteName" type="text" placeholder="Site Name" required></label>
224+
<label class="field"><span>Device Label</span><input id="deviceLabel" type="text" placeholder="Device Label" required></label>
225+
<label class="field"><span>Server Fleet</span><input id="serverFleet" type="text" value="tankalarm-server"></label>
226+
<label class="field"><span>Sample Seconds</span><input id="sampleSeconds" type="number" value="300"></label>
227+
<label class="field"><span>Report Hour</span><input id="reportHour" type="number" value="5"></label>
228+
<label class="field"><span>Report Minute</span><input id="reportMinute" type="number" value="0"></label>
229+
<label class="field"><span>SMS Primary</span><input id="smsPrimary" type="text"></label>
230+
<label class="field"><span>SMS Secondary</span><input id="smsSecondary" type="text"></label>
231+
<label class="field"><span>Daily Email</span><input id="dailyEmail" type="email"></label>
232+
</div>
233+
234+
<h3>Sensors</h3>
235+
<div id="sensorsContainer"></div>
236+
237+
<div class="actions">
238+
<button type="button" id="addSensorBtn" class="secondary">+ Add Sensor</button>
239+
<button type="button" id="downloadBtn">Download Config</button>
240+
</div>
241+
</form>
242+
</div>
243+
</main>
244+
<script>
245+
const sensorTypes = [
246+
{ value: 0, label: 'Digital Input' },
247+
{ value: 1, label: 'Analog Input (0-10V)' },
248+
{ value: 2, label: 'Current Loop (4-20mA)' }
249+
];
250+
251+
const monitorTypes = [
252+
{ value: 'tank', label: 'Tank Level' },
253+
{ value: 'gas', label: 'Gas Pressure' }
254+
];
255+
256+
const optaPins = [
257+
{ value: 0, label: 'I1' },
258+
{ value: 1, label: 'I2' },
259+
{ value: 2, label: 'I3' },
260+
{ value: 3, label: 'I4' },
261+
{ value: 4, label: 'I5' },
262+
{ value: 5, label: 'I6' },
263+
{ value: 6, label: 'I7' },
264+
{ value: 7, label: 'I8' }
265+
];
266+
267+
const expansionChannels = [
268+
{ value: 0, label: 'I1' },
269+
{ value: 1, label: 'I2' },
270+
{ value: 2, label: 'I3' },
271+
{ value: 3, label: 'I4' },
272+
{ value: 4, label: 'I5' },
273+
{ value: 5, label: 'I6' }
274+
];
275+
276+
let sensorCount = 0;
277+
278+
function createSensorHtml(id) {
279+
return `
280+
<div class="sensor-card" id="sensor-${id}">
281+
<div class="sensor-header">
282+
<span class="sensor-title">Sensor #${id + 1}</span>
283+
<button type="button" class="remove-btn" onclick="removeSensor(${id})">Remove</button>
284+
</div>
285+
<div class="form-grid">
286+
<label class="field"><span>Monitor Type</span>
287+
<select class="monitor-type" onchange="updateMonitorFields(${id})">
288+
${monitorTypes.map(t => `<option value="${t.value}">${t.label}</option>`).join('')}
289+
</select>
290+
</label>
291+
<label class="field tank-num-field"><span>Tank Number</span><input type="number" class="tank-num" value="${id + 1}"></label>
292+
<label class="field"><span><span class="name-label">Tank Name</span></span><input type="text" class="tank-name" placeholder="Name"></label>
293+
<label class="field"><span>Sensor Type</span>
294+
<select class="sensor-type" onchange="updatePinOptions(${id})">
295+
${sensorTypes.map(t => `<option value="${t.value}">${t.label}</option>`).join('')}
296+
</select>
297+
</label>
298+
<label class="field"><span>Pin / Channel</span>
299+
<select class="sensor-pin">
300+
${optaPins.map(p => `<option value="${p.value}">${p.label}</option>`).join('')}
301+
</select>
302+
</label>
303+
<label class="field"><span><span class="height-label">Height (in)</span></span><input type="number" class="tank-height" value="120"></label>
304+
<label class="field"><span>High Alarm</span><input type="number" class="high-alarm" value="100"></label>
305+
<label class="field"><span>Low Alarm</span><input type="number" class="low-alarm" value="20"></label>
306+
</div>
307+
</div>
308+
`;
309+
}
310+
311+
function addSensor() {
312+
const container = document.getElementById('sensorsContainer');
313+
const div = document.createElement('div');
314+
div.innerHTML = createSensorHtml(sensorCount);
315+
container.appendChild(div.firstElementChild);
316+
sensorCount++;
317+
}
318+
319+
window.removeSensor = function(id) {
320+
const el = document.getElementById(`sensor-${id}`);
321+
if (el) el.remove();
322+
};
323+
324+
window.updateMonitorFields = function(id) {
325+
const card = document.getElementById(`sensor-${id}`);
326+
const type = card.querySelector('.monitor-type').value;
327+
const numField = card.querySelector('.tank-num-field');
328+
const nameLabel = card.querySelector('.name-label');
329+
const heightLabel = card.querySelector('.height-label');
330+
331+
if (type === 'gas') {
332+
numField.style.display = 'none';
333+
nameLabel.textContent = 'System Name';
334+
heightLabel.textContent = 'Max Pressure';
335+
} else {
336+
numField.style.display = 'flex';
337+
nameLabel.textContent = 'Tank Name';
338+
heightLabel.textContent = 'Height (in)';
339+
}
340+
};
341+
342+
window.updatePinOptions = function(id) {
343+
const card = document.getElementById(`sensor-${id}`);
344+
const typeSelect = card.querySelector('.sensor-type');
345+
const pinSelect = card.querySelector('.sensor-pin');
346+
const type = parseInt(typeSelect.value);
347+
348+
pinSelect.innerHTML = '';
349+
let options = [];
350+
351+
if (type === 2) { // Current Loop
352+
options = expansionChannels;
353+
} else { // Digital or Analog
354+
options = optaPins;
355+
}
356+
357+
options.forEach(opt => {
358+
const option = document.createElement('option');
359+
option.value = opt.value;
360+
option.textContent = opt.label;
361+
pinSelect.appendChild(option);
362+
});
363+
};
364+
365+
document.getElementById('addSensorBtn').addEventListener('click', addSensor);
366+
367+
document.getElementById('downloadBtn').addEventListener('click', () => {
368+
const config = {
369+
siteName: document.getElementById('siteName').value,
370+
deviceLabel: document.getElementById('deviceLabel').value,
371+
serverFleet: document.getElementById('serverFleet').value,
372+
smsPrimary: document.getElementById('smsPrimary').value,
373+
smsSecondary: document.getElementById('smsSecondary').value,
374+
dailyEmail: document.getElementById('dailyEmail').value,
375+
sampleSeconds: parseInt(document.getElementById('sampleSeconds').value) || 300,
376+
reportHour: parseInt(document.getElementById('reportHour').value) || 5,
377+
reportMinute: parseInt(document.getElementById('reportMinute').value) || 0,
378+
tankCount: 0,
379+
tanks: []
380+
};
381+
382+
const sensorCards = document.querySelectorAll('.sensor-card');
383+
config.tankCount = sensorCards.length;
384+
385+
sensorCards.forEach((card, index) => {
386+
const monitorType = card.querySelector('.monitor-type').value;
387+
const type = parseInt(card.querySelector('.sensor-type').value);
388+
const pin = parseInt(card.querySelector('.sensor-pin').value);
389+
390+
// For gas sensors, we hide the number but still need one for the firmware.
391+
// We'll use the index + 1.
392+
let tankNum = parseInt(card.querySelector('.tank-num').value) || (index + 1);
393+
let name = card.querySelector('.tank-name').value;
394+
395+
if (monitorType === 'gas') {
396+
if (!name) name = `Gas System ${index + 1}`;
397+
} else {
398+
if (!name) name = `Tank ${index + 1}`;
399+
}
400+
401+
const tank = {
402+
id: String.fromCharCode(65 + index), // A, B, C...
403+
name: name,
404+
tankNumber: tankNum,
405+
sensorType: type,
406+
primaryPin: (type !== 2) ? pin : 0,
407+
secondaryPin: -1,
408+
currentLoopChannel: (type === 2) ? pin : -1,
409+
heightInches: parseFloat(card.querySelector('.tank-height').value) || 120,
410+
highAlarmInches: parseFloat(card.querySelector('.high-alarm').value) || 100,
411+
lowAlarmInches: parseFloat(card.querySelector('.low-alarm').value) || 20,
412+
hysteresisInches: 2.0,
413+
enableDailyReport: true,
414+
enableAlarmSms: true,
415+
enableServerUpload: true
416+
};
417+
config.tanks.push(tank);
418+
});
419+
420+
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
421+
const url = URL.createObjectURL(blob);
422+
const a = document.createElement('a');
423+
a.href = url;
424+
a.download = 'client_config.json';
425+
document.body.appendChild(a);
426+
a.click();
427+
document.body.removeChild(a);
428+
URL.revokeObjectURL(url);
429+
});
430+
431+
// Add one sensor by default
432+
addSensor();
433+
</script>
434+
</body>
435+
</html>
436+
)HTML";
437+
182438
static const char DASHBOARD_HTML[] PROGMEM = R"HTML(
183439
<!DOCTYPE html>
184440
<html lang="en">
@@ -358,6 +614,7 @@ static const char DASHBOARD_HTML[] PROGMEM = R"HTML(
358614
<div class="meta">
359615
<span>Server UID: <code id="serverUid">--</code></span>
360616
<span>Next Daily Email: <span id="nextEmail">--</span></span>
617+
<a href="/config-generator" style="color: #fff; text-decoration: underline;">Config Generator</a>
361618
</div>
362619
</header>
363620
<main>
@@ -1151,6 +1408,8 @@ static void handleWebRequests() {
11511408

11521409
if (method == "GET" && path == "/") {
11531410
sendDashboard(client);
1411+
} else if (method == "GET" && path == "/config-generator") {
1412+
sendConfigGenerator(client);
11541413
} else if (method == "GET" && path == "/api/tanks") {
11551414
sendTankJson(client);
11561415
} else if (method == "GET" && path == "/api/clients") {
@@ -1293,6 +1552,21 @@ static void sendDashboard(EthernetClient &client) {
12931552
}
12941553
}
12951554

1555+
static void sendConfigGenerator(EthernetClient &client) {
1556+
size_t htmlLen = strlen_P(CONFIG_GENERATOR_HTML);
1557+
client.println(F("HTTP/1.1 200 OK"));
1558+
client.println(F("Content-Type: text/html; charset=utf-8"));
1559+
client.print(F("Content-Length: "));
1560+
client.println(htmlLen);
1561+
client.println(F("Cache-Control: no-cache, no-store, must-revalidate"));
1562+
client.println();
1563+
1564+
for (size_t i = 0; i < htmlLen; ++i) {
1565+
char c = pgm_read_byte_near(CONFIG_GENERATOR_HTML + i);
1566+
client.write(c);
1567+
}
1568+
}
1569+
12961570
static void sendTankJson(EthernetClient &client) {
12971571
DynamicJsonDocument doc(4096);
12981572
JsonArray arr = doc.createNestedArray("tanks");

0 commit comments

Comments
 (0)