@@ -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="/">← 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+
182438static 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+
12961570static void sendTankJson (EthernetClient &client) {
12971571 DynamicJsonDocument doc (4096 );
12981572 JsonArray arr = doc.createNestedArray (" tanks" );
0 commit comments