Restore safety cutoff to 800F, faster PID loop timing, enhanced simple UI with stats/charts/flight controls, compact TTY display
This commit is contained in:
+141
-8
@@ -1,8 +1,10 @@
|
||||
let pollInterval = 500;
|
||||
let chartMaxPoints = 300;
|
||||
let lastTimestamp = 0;
|
||||
let lastTimestampByNail = { nail1: 0, nail2: 0 };
|
||||
let chart = null;
|
||||
let firstTimestamp = null;
|
||||
let simpleChart = null;
|
||||
let simpleFirstTimestamp = null;
|
||||
let lastApiError = '';
|
||||
let actionBannerTimer = null;
|
||||
let heartbeatMisses = 0;
|
||||
@@ -12,6 +14,7 @@ let deferredInstallPrompt = null;
|
||||
let uiMode = 'nail1';
|
||||
let activeNailId = 'nail1';
|
||||
let historyBuffer = [];
|
||||
let simpleHistoryByNail = { nail1: [], nail2: [] };
|
||||
let schedulerTimesByNail = { nail1: [], nail2: [] };
|
||||
let states = { nail1: null, nail2: null };
|
||||
|
||||
@@ -166,6 +169,26 @@ function schedulerEnabledChanged() {
|
||||
saveSchedulerSettings();
|
||||
}
|
||||
|
||||
function formatSeconds(sec) {
|
||||
if (typeof sec !== 'number' || !isFinite(sec)) return '--';
|
||||
const s = Math.max(0, Math.round(sec));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const r = s % 60;
|
||||
if (h > 0) return h + 'h ' + m + 'm ' + r + 's';
|
||||
return m + 'm ' + r + 's';
|
||||
}
|
||||
|
||||
function formatUptime(sec) {
|
||||
if (typeof sec !== 'number' || !isFinite(sec)) return '--';
|
||||
const s = Math.max(0, Math.round(sec));
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const r = s % 60;
|
||||
if (h > 0) return h + 'h ' + m + 'm';
|
||||
return m + 'm ' + r + 's';
|
||||
}
|
||||
|
||||
function updateSimpleCards() {
|
||||
['nail1', 'nail2'].forEach(function(id) {
|
||||
const s = states[id];
|
||||
@@ -176,10 +199,35 @@ function updateSimpleCards() {
|
||||
const spEl = document.getElementById('simple-target-' + suffix);
|
||||
const pEl = document.getElementById('simple-power-' + suffix);
|
||||
const inEl = document.getElementById('simple-setpoint-' + suffix);
|
||||
const relayEl = document.getElementById('simple-relay-' + suffix);
|
||||
const outEl = document.getElementById('simple-output-' + suffix);
|
||||
const tcEl = document.getElementById('simple-tc-' + suffix);
|
||||
const upEl = document.getElementById('simple-uptime-' + suffix);
|
||||
const etaEl = document.getElementById('simple-eta-' + suffix);
|
||||
const cutoffEl = document.getElementById('simple-cutoff-' + suffix);
|
||||
const alertEl = document.getElementById('simple-alert-' + suffix);
|
||||
if (tEl) tEl.textContent = s.temp.toFixed(1);
|
||||
if (mEl) mEl.textContent = s.mode || 'grounded';
|
||||
if (spEl) spEl.textContent = s.effective_setpoint.toFixed(0);
|
||||
if (inEl && document.activeElement !== inEl) inEl.value = s.setpoint;
|
||||
if (relayEl) relayEl.textContent = s.relay_on ? 'ON' : 'OFF';
|
||||
if (outEl) outEl.textContent = (typeof s.output === 'number' ? s.output.toFixed(0) : '--');
|
||||
if (tcEl) tcEl.textContent = s.thermocouple_connected ? 'OK' : 'DISC';
|
||||
if (upEl) upEl.textContent = formatUptime(s.uptime_seconds);
|
||||
if (etaEl) etaEl.textContent = formatSeconds(s.mode_eta_seconds);
|
||||
if (cutoffEl) cutoffEl.textContent = s.scheduler && s.scheduler.enabled ? formatSeconds(s.next_cutoff_seconds) : 'Scheduler off';
|
||||
if (alertEl) {
|
||||
if (s.safety_tripped) {
|
||||
alertEl.className = 'simple-alert bad';
|
||||
alertEl.textContent = 'Safety trip: ' + (s.safety_reason || 'Unknown');
|
||||
} else if (!s.thermocouple_connected) {
|
||||
alertEl.className = 'simple-alert warn';
|
||||
alertEl.textContent = 'Thermocouple disconnected.';
|
||||
} else {
|
||||
alertEl.className = 'simple-alert';
|
||||
alertEl.textContent = 'No active safety alerts.';
|
||||
}
|
||||
}
|
||||
if (pEl) {
|
||||
pEl.textContent = s.enabled ? 'ON' : 'OFF';
|
||||
pEl.className = 'power-mini ' + (s.enabled ? 'on' : 'off');
|
||||
@@ -282,9 +330,50 @@ function initChart() {
|
||||
});
|
||||
}
|
||||
|
||||
function initSimpleChart() {
|
||||
const canvas = document.getElementById('simple-temp-chart');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
simpleChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { datasets: [
|
||||
{ label: 'Nail 1 Temp', borderColor: '#ff6b35', borderWidth: 2, pointRadius: 0, fill: false, data: [] },
|
||||
{ label: 'Nail 2 Temp', borderColor: '#4ecdc4', borderWidth: 2, pointRadius: 0, fill: false, data: [] },
|
||||
{ label: 'Nail 1 Target', borderColor: '#ff9f6b', borderWidth: 1, borderDash: [5, 4], pointRadius: 0, fill: false, data: [] },
|
||||
{ label: 'Nail 2 Target', borderColor: '#7df7ef', borderWidth: 1, borderDash: [5, 4], pointRadius: 0, fill: false, data: [] },
|
||||
]},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
color: '#888',
|
||||
callback: function(value) { return Math.round(value) + 's'; },
|
||||
maxTicksLimit: 10,
|
||||
},
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
ticks: { color: '#ccc' },
|
||||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 700,
|
||||
title: { display: true, text: 'Temperature (F)', color: '#aaa' },
|
||||
},
|
||||
},
|
||||
plugins: { legend: { labels: { color: '#ccc', boxWidth: 12 } } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resetChartForNail() {
|
||||
firstTimestamp = null;
|
||||
lastTimestamp = 0;
|
||||
lastTimestampByNail[currentNailId()] = 0;
|
||||
historyBuffer = [];
|
||||
if (chart) {
|
||||
chart.data.datasets.forEach(function(ds) { ds.data = []; });
|
||||
@@ -293,6 +382,28 @@ function resetChartForNail() {
|
||||
updateErrorStats();
|
||||
}
|
||||
|
||||
function addSimpleChartData(nailId, points) {
|
||||
if (!simpleChart || !points || !points.length) return;
|
||||
if (simpleFirstTimestamp === null) simpleFirstTimestamp = points[0].timestamp;
|
||||
const tempDatasetIndex = (nailId === 'nail2') ? 1 : 0;
|
||||
const targetDatasetIndex = (nailId === 'nail2') ? 3 : 2;
|
||||
|
||||
points.forEach(function(p) {
|
||||
const x = p.timestamp - simpleFirstTimestamp;
|
||||
simpleChart.data.datasets[tempDatasetIndex].data.push({ x: x, y: p.temp });
|
||||
simpleChart.data.datasets[targetDatasetIndex].data.push({ x: x, y: p.setpoint });
|
||||
simpleHistoryByNail[nailId].push(p);
|
||||
});
|
||||
|
||||
const newest = simpleHistoryByNail[nailId].length ? simpleHistoryByNail[nailId][simpleHistoryByNail[nailId].length - 1].timestamp : 0;
|
||||
simpleHistoryByNail[nailId] = simpleHistoryByNail[nailId].filter(function(p) { return p.timestamp >= (newest - 600); });
|
||||
|
||||
simpleChart.data.datasets.forEach(function(ds) {
|
||||
if (ds.data.length > chartMaxPoints) ds.data = ds.data.slice(ds.data.length - chartMaxPoints);
|
||||
});
|
||||
simpleChart.update('none');
|
||||
}
|
||||
|
||||
function addChartData(points) {
|
||||
if (!chart || !points.length) return;
|
||||
if (firstTimestamp === null) firstTimestamp = points[0].timestamp;
|
||||
@@ -422,7 +533,7 @@ function renderAdvanced(status) {
|
||||
if (document.activeElement.id !== 'flight-descent-seconds') document.getElementById('flight-descent-seconds').value = flight.descent_seconds || 90;
|
||||
if (document.activeElement.id !== 'flight-descent-target') document.getElementById('flight-descent-target').value = flight.descent_target_f || 120;
|
||||
const fm = document.getElementById('flight-mode-status');
|
||||
if (fm) fm.textContent = 'Current mode: ' + (status.mode || 'grounded') + ' (Power button handles Grounded/Cruise)';
|
||||
if (fm) fm.textContent = '';
|
||||
|
||||
if (document.activeElement.id !== 'sched-enabled') document.getElementById('sched-enabled').value = status.scheduler.enabled ? 'true' : 'false';
|
||||
const incoming = (status.scheduler.cutoff_times || []).slice().sort();
|
||||
@@ -445,12 +556,23 @@ async function pollStatus() {
|
||||
renderAdvanced(states[currentNailId()]);
|
||||
}
|
||||
|
||||
async function pollHistory() {
|
||||
const nailNum = nailNumFromId(currentNailId());
|
||||
const data = await fetchJSON('/api/history?since=' + lastTimestamp + '&nail=' + nailNum);
|
||||
async function pollHistoryForNail(nailId) {
|
||||
const since = lastTimestampByNail[nailId] || 0;
|
||||
const nailNum = nailNumFromId(nailId);
|
||||
const data = await fetchJSON('/api/history?since=' + since + '&nail=' + nailNum);
|
||||
if (!data || !data.length) return;
|
||||
lastTimestamp = data[data.length - 1].timestamp;
|
||||
addChartData(data);
|
||||
lastTimestampByNail[nailId] = data[data.length - 1].timestamp;
|
||||
addSimpleChartData(nailId, data);
|
||||
if (nailId === currentNailId()) {
|
||||
addChartData(data);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollHistory() {
|
||||
await Promise.all([
|
||||
pollHistoryForNail('nail1'),
|
||||
pollHistoryForNail('nail2'),
|
||||
]);
|
||||
}
|
||||
|
||||
async function pollHeartbeat() {
|
||||
@@ -492,6 +614,16 @@ async function simpleTogglePower(num) {
|
||||
setLastAck('power ' + (result.enabled ? 'ON' : 'OFF') + ' (' + id + ')', true);
|
||||
}
|
||||
|
||||
async function simpleSetFlightMode(num, mode) {
|
||||
const id = nailIdFromNum(num);
|
||||
const result = await apiPost('/api/flight', { mode: mode }, id);
|
||||
if (!result) {
|
||||
setLastAck('mode ' + mode + ' failed', false);
|
||||
return;
|
||||
}
|
||||
setLastAck('mode ' + mode + ' (' + id + ')', true);
|
||||
}
|
||||
|
||||
async function applySetpoint() {
|
||||
const value = parseFloat(document.getElementById('setpoint-input').value);
|
||||
if (isNaN(value)) return;
|
||||
@@ -696,6 +828,7 @@ window.addEventListener('appinstalled', function() {
|
||||
});
|
||||
|
||||
initChart();
|
||||
initSimpleChart();
|
||||
setInterval(pollStatus, pollInterval);
|
||||
setInterval(pollHistory, pollInterval);
|
||||
setInterval(pollHeartbeat, 2000);
|
||||
|
||||
@@ -212,6 +212,76 @@ body.ui-simple .app-footer {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.simple-flight-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.simple-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.simple-stat {
|
||||
font-size: 0.74rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 5px 7px;
|
||||
color: var(--text-dim);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.simple-stat strong {
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.simple-alert {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.73rem;
|
||||
color: var(--text-dim);
|
||||
border-left: 2px solid var(--border);
|
||||
padding-left: 8px;
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.simple-alert.warn {
|
||||
color: #f5b041;
|
||||
border-left-color: #f5b041;
|
||||
}
|
||||
|
||||
.simple-alert.bad {
|
||||
color: var(--accent-red);
|
||||
border-left-color: var(--accent-red);
|
||||
}
|
||||
|
||||
.simple-chart-wrap {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
height: 280px;
|
||||
box-shadow: inset 0 0 0 1px rgba(211, 84, 0, 0.12);
|
||||
}
|
||||
|
||||
.simple-chart-wrap h3 {
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.simple-chart-wrap canvas {
|
||||
width: 100% !important;
|
||||
height: calc(100% - 26px) !important;
|
||||
}
|
||||
|
||||
.power-mini {
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
@@ -681,6 +751,13 @@ button:disabled {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
#control-loop-size,
|
||||
#flight-takeoff-seconds,
|
||||
#flight-descent-seconds,
|
||||
#flight-descent-target {
|
||||
width: 112px;
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
@@ -783,6 +860,10 @@ button:disabled {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.simple-chart-wrap {
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user