diff --git a/main/index.html b/main/index.html
index 3078b40..de74eb2 100644
--- a/main/index.html
+++ b/main/index.html
@@ -67,7 +67,9 @@
width: 4px;
margin: 0;
z-index: 10;
- -webkit-appearance: none; /* Reset to control gradient */
+ -webkit-appearance: none;
+ border: 1px solid #333;
+ /* Reset to control gradient */
appearance: none;
writing-mode: vertical-lr;
}
@@ -240,10 +242,11 @@
-
+
+
Connecting...
@@ -289,7 +292,8 @@
-
+
diff --git a/main/index.js b/main/index.js
index 1d55939..278aa08 100644
--- a/main/index.js
+++ b/main/index.js
@@ -1,4 +1,32 @@
-// Current active configuration for display scaling
+///
+
+/*
+ * ==========================================
+ * 1. Constants & Configuration
+ * ==========================================
+ */
+
+// Approximate full-scale voltages for ESP32C6/ESP32 ADC attenuations
+// 0dB: ~950mV, 2.5dB: ~1250mV, 6dB: ~1750mV, 11dB: ~3100mV+ (use 3.3V)
+/** @type {number[]} */
+const ATTEN_TO_MAX_V = [0.95, 1.25, 1.75, 3.3];
+
+// Data buffer size
+/** @type {number} */
+const countPoints = 4000;
+
+/**
+ * @typedef {Object} ActiveConfig
+ * @property {number} desiredRate - Desired sample rate in Hz
+ * @property {number} sample_rate - Actual hardware sample rate
+ * @property {number} atten - Attenuation setting index
+ * @property {number} bit_width - Bit width (e.g. 12)
+ * @property {number} test_hz - Test signal frequency for simulation
+ * @property {number} trigger - Trigger level (-1-4097)
+ * @property {boolean} invert - Whether trigger logic is inverted
+ */
+
+/** @type {ActiveConfig} */
let activeConfig = {
desiredRate: 10000,
sample_rate: 10000,
@@ -9,7 +37,17 @@ let activeConfig = {
invert: false
};
-// Accumulator for virtual low sample rates
+/**
+ * @typedef {Object} LowRateState
+ * @property {number} accMin - Accumulated minimum value
+ * @property {number} accMax - Accumulated maximum value
+ * @property {number} accSum - Accumulated sum for average
+ * @property {number} accCount - Count of samples in accumulator
+ * @property {number} progress - Fractional progress counter
+ * @property {number} targetCount - Target samples per output point
+ */
+
+/** @type {LowRateState} */
let lowRateState = {
accMin: 4096,
accMax: 0,
@@ -19,65 +57,87 @@ let lowRateState = {
targetCount: 1.0
};
-// Approximate full-scale voltages for ESP32C6/ESP32 ADC attenuations
-// 0dB: ~950mV, 2.5dB: ~1250mV, 6dB: ~1750mV, 11dB: ~3100mV+ (use 3.3V)
-const ATTEN_TO_MAX_V = [0.95, 1.25, 1.75, 3.3];
-// Data buffer
-const countPoints = 4000;
-/** @type Array */
+/*
+ * ==========================================
+ * 2. Global UI Elements & State
+ * ==========================================
+ */
+
+/** @type {HTMLCanvasElement} */ const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('adcChart'));
+/** @type {CanvasRenderingContext2D} */ const ctx = canvas.getContext('2d');
+/** @type {HTMLElement} */ const statusEl = document.getElementById('status');
+/** @type {HTMLElement} */ const triggerStatusEl = document.getElementById('trigger-status');
+/** @type {HTMLElement} */ const deltaPanel = document.getElementById('deltaPanel');
+/** @type {HTMLInputElement & { invert?: boolean, downValue?: string }} */ const triggerLevel = /** @type {HTMLInputElement & { invert?: boolean, downValue?: string }} */ (document.getElementById('triggerLevel'));
+
+// Config Elements
+/** @type {HTMLElement} */ const reconnectBtn = document.getElementById('reconnectBtn');
+/** @type {HTMLSelectElement} */ const sampleRateSelect = /** @type {HTMLSelectElement} */ (document.getElementById('sampleRate'));
+/** @type {HTMLSelectElement} */ const bitWidthSelect = /** @type {HTMLSelectElement} */ (document.getElementById('bitWidth'));
+/** @type {HTMLSelectElement} */ const attenSelect = /** @type {HTMLSelectElement} */ (document.getElementById('atten'));
+/** @type {HTMLSelectElement} */ const testHzSelect = /** @type {HTMLSelectElement} */ (document.getElementById('testHz'));
+/** @type {HTMLButtonElement} */ const resetBtn = /** @type {HTMLButtonElement} */ (document.getElementById('resetBtn'));
+/** @type {HTMLButtonElement} */ const powerOffBtn = /** @type {HTMLButtonElement} */ (document.getElementById('powerOff'));
+
+// Wifi Elements
+/** @type {HTMLElement} */ const wifiModal = document.getElementById('wifiModal');
+/** @type {HTMLButtonElement} */ const wifiBtn = /** @type {HTMLButtonElement} */ (document.getElementById('wifiBtn'));
+/** @type {HTMLElement} */ const closeWifiBtn = document.getElementById('closeWifi');
+/** @type {HTMLButtonElement} */ const saveWifiBtn = /** @type {HTMLButtonElement} */ (document.getElementById('saveWifi'));
+/** @type {HTMLInputElement} */ const wifiSsidInput = /** @type {HTMLInputElement} */ (document.getElementById('wifiSsid'));
+/** @type {HTMLInputElement} */ const wifiPassInput = /** @type {HTMLInputElement} */ (document.getElementById('wifiPass'));
+
+/**
+ * @typedef {Object} DownsampledPoint
+ * @property {number} min
+ * @property {number} max
+ * @property {number} avg
+ */
+
+/** @type {Array} */
let dataBuffer = new Array(countPoints).fill(0);
-/** @type HTMLCanvas */ const canvas = document.getElementById('adcChart');
-const ctx = canvas.getContext('2d');
-const statusEl = document.getElementById('status');
-const deltaPanel = document.getElementById('deltaPanel');
-const triggerLevel = document.getElementById('triggerLevel');
-
-function triggerColor() {
- activeConfig.trigger = triggerLevel.value;
- const value = (triggerLevel.value - triggerLevel.min) / (triggerLevel.max - triggerLevel.min) * 100;
- triggerLevel.style.background = triggerLevel.invert
- ? `linear-gradient(to bottom, #00000070 0%, #ff020270 ${value}%, #1302ff70 ${value}%, #00000070 100%)`
- : `linear-gradient(to bottom, #00000070 0%, #1302ff70 ${value}%, #ff020270 ${value}%, #00000070 100%)`
-}
-
-triggerLevel.addEventListener('input', triggerColor);
-triggerLevel.addEventListener('mousedown', function () {
- this.downValue = this.value;
-});
-triggerLevel.addEventListener('mouseup', function () {
- if (this.downValue == this.value) {
- this.invert = !this.invert;
- activeConfig.invert = this.invert;
- triggerColor();
- this.dispatchEvent(new Event("change"));
- }
- delete this.downValue;
-});
-triggerColor();
-
-// Resize canvas & data
-function resize() {
- canvas.width = canvas.offsetWidth;
- canvas.height = canvas.offsetHeight;
-}
-
-window.addEventListener('resize', resize);
-resize();
-
+/** @type {{x: number|null, y: number|null}} */
let lastMousePosition = { x: null, y: null };
-let isFrozen = false; // Track freeze state
-canvas.addEventListener('click', () => isFrozen = !isFrozen);
+/** @type {boolean} */
+let isFrozen = false;
+/** @type {{t: number, v: number}|null} */
+let referencePosition = null; // Store the reference position for deltas
+
+/**
+ * @typedef {Object} ViewTransform
+ * @property {number} scale
+ * @property {number} offsetX
+ * @property {number} offsetY
+ */
+
+/** @type {ViewTransform} */
let viewTransform = {
scale: 1,
offsetX: 0,
offsetY: 0
};
-// Helper to get total time (width of buffer in ms)
+// WebSocket vars
+/** @type {WebSocket} */
+let ws;
+/** @type {number} */
+let reconnectTimeout;
+
+
+/*
+ * ==========================================
+ * 3. Coordinate System & Math Utils
+ * ==========================================
+ */
+
+/**
+ * Helper to get total time (width of buffer in ms)
+ * @returns {number} Total time in milliseconds
+ */
function getTotalTimeMs() {
let msPerPoint;
// If we are in low-rate mode (peak detect), we emit 1 point (object)
@@ -94,211 +154,115 @@ function getTotalTimeMs() {
return msPerPoint * canvas.width;
}
-// Helper to get max voltage
+/**
+ * Helper to get max voltage
+ * @returns {number} Max voltage stored in ATTEN_TO_MAX_V or 3.3
+ */
function getMaxVoltage() {
return ATTEN_TO_MAX_V[activeConfig.atten] || 3.3;
}
-// Coordinate Transforms
+/**
+ * Convert X pixel coordinate to Time (ms)
+ * @param {number} px - Pixel X coordinate
+ * @returns {number} Time in milliseconds
+ */
function XtoTime(px) {
const totalTime = getTotalTimeMs();
- // px = (t / totalTime * width) * scale + offsetX
- // t = ((px - offsetX) / scale) * (totalTime / width)
if (canvas.width === 0) return 0;
return ((px - viewTransform.offsetX) / viewTransform.scale) * (totalTime / canvas.width);
}
+/**
+ * Convert Time (ms) to X pixel coordinate
+ * @param {number} t - Time in milliseconds
+ * @returns {number} Pixel X coordinate
+ */
function TimeToX(t) {
const totalTime = getTotalTimeMs();
- if (totalTime === 0) return 0;
+ if (totalTime === 0 || canvas.width === 0) return 0;
const xp = (t / totalTime) * canvas.width;
return xp * viewTransform.scale + viewTransform.offsetX;
}
+/**
+ * Convert Y pixel coordinate to Voltage
+ * @param {number} py - Pixel Y coordinate
+ * @returns {number} Voltage in volts
+ */
function YtoVolts(py) {
const maxV = getMaxVoltage();
- // sy = yp * scale + offsetY
- // yp = (sy - offsetY) / scale
- // yp = h * (1 - v/maxV)
- // v = maxV * (1 - yp/h)
if (canvas.height === 0) return 0;
const yp = (py - viewTransform.offsetY) / viewTransform.scale;
return maxV * (1 - yp / canvas.height);
}
+/**
+ * Convert Voltage to Y pixel coordinate
+ * @param {number} v - Voltage in volts
+ * @returns {number} Pixel Y coordinate
+ */
function VoltsToY(v) {
const maxV = getMaxVoltage();
const yp = canvas.height * (1 - v / maxV);
return yp * viewTransform.scale + viewTransform.offsetY;
}
-canvas.addEventListener('wheel', function (e) {
- e.preventDefault();
- if (e.deltaX > 0) {
-
- } else if (e.deltaX < 0) {
-
- }
- const zoomFactor = 1.1;
- const direction = e.deltaY < 0 ? 1 : -1;
- const factor = direction > 0 ? zoomFactor : 1 / zoomFactor;
-
- let newScale = viewTransform.scale * factor;
-
- if (newScale < 1.001) {
- // Snap to 100% and reset position
- newScale = 1.0;
- viewTransform.offsetX = 0;
- viewTransform.offsetY = 0;
- } else if (newScale > 50) {
- return; // Max limit
+/**
+ * Nice Number Generator for Grid
+ * @param {number} range - Range of values
+ * @param {boolean} round - Whether to round to nice fraction
+ * @returns {number} Nice number interval
+ */
+function niceNum(range, round) {
+ const exponent = Math.floor(Math.log10(range));
+ const fraction = range / Math.pow(10, exponent);
+ let niceFraction;
+ if (round) {
+ if (fraction < 1.5) niceFraction = 1;
+ else if (fraction < 3) niceFraction = 2;
+ else if (fraction < 7) niceFraction = 5;
+ else niceFraction = 10;
} else {
- // Zoom centered on mouse
- const mx = e.offsetX;
- const my = e.offsetY;
-
- viewTransform.offsetX = mx - (mx - viewTransform.offsetX) * factor;
- viewTransform.offsetY = my - (my - viewTransform.offsetY) * factor;
+ if (fraction <= 1) niceFraction = 1;
+ else if (fraction <= 2) niceFraction = 2;
+ else if (fraction <= 5) niceFraction = 5;
+ else niceFraction = 10;
}
-
- viewTransform.scale = newScale;
- if (newScale < 1.001) {
- statusEl.textContent = 'Connected via WebSocket';
- } else {
- statusEl.textContent = 'Scaled to ' + newScale.toFixed(2) + 'x';
- }
-
- draw();
- // Update info if frozen to show correct values under cursor
- if (isFrozen) {
- updateInfo({ offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY });
- }
-});
-
-let referencePosition = null; // Store the reference position for deltas
-
-// Helper function to schedule WebSocket reconnection
-function scheduleReconnect() {
- statusEl.textContent = 'Disconnected. Retrying in 2s...';
- statusEl.style.color = '#ef4444';
- reconnectTimeout = setTimeout(connect, 2000);
+ return niceFraction * Math.pow(10, exponent);
}
-// Helper function to draw crosshairs
-function drawCrosshairs(x, y, color) {
- ctx.setLineDash([5, 5]);
- ctx.strokeStyle = color;
- ctx.lineWidth = 1;
-
- // Draw vertical line
- ctx.beginPath();
- ctx.moveTo(x, 0);
- ctx.lineTo(x, canvas.height);
- ctx.stroke();
-
- // Draw horizontal line
- ctx.beginPath();
- ctx.moveTo(0, y);
- ctx.lineTo(canvas.width, y);
- ctx.stroke();
-
- ctx.setLineDash([]);
+/**
+ * Calculate nice tick marks for axis
+ * @param {number} min - Minimum value
+ * @param {number} max - Maximum value
+ * @param {number} maxTicks - Maximum number of ticks
+ * @returns {number[]} Array of tick values
+ */
+function calculateNiceTicks(min, max, maxTicks) {
+ const range = niceNum(max - min, false);
+ const tickSpacing = niceNum(range / (maxTicks - 1), true);
+ const niceMin = Math.floor(min / tickSpacing) * tickSpacing;
+ const niceMax = Math.ceil(max / tickSpacing) * tickSpacing;
+ const ticks = [];
+ for (let t = niceMin; t <= niceMax + 0.00001; t += tickSpacing) {
+ ticks.push(t);
+ }
+ return ticks;
}
-// Refactor duplicated code to use helper functions
-function updateInfo(event) {
- // Use raw coordinates or event helpers
- const voltage = YtoVolts(event.offsetY);
- const timeOffset = XtoTime(event.offsetX);
- let info = `${voltage.toFixed(3)}V, ${timeOffset.toFixed(2)}ms
`;
- // Store the last mouse position
- lastMousePosition.x = event.offsetX;
- lastMousePosition.y = event.offsetY;
+/*
+ * ==========================================
+ * 4. Data Processing
+ * ==========================================
+ */
- // Update delta panel position and content if frozen
- if (isFrozen && referencePosition) {
- // Delta uses World Coordinates now
- const deltaVoltage = Math.abs(referencePosition.v - voltage);
- const deltaTime = Math.abs(referencePosition.t - timeOffset);
-
- info += `ΔV ${deltaVoltage.toFixed(3)}V, ΔT ${deltaTime.toFixed(2)}ms (${(1000 / deltaTime).toFixed(2)} Hz)
`;
- }
-
- deltaPanel.style.left = `${event.pageX + 10}px`;
- deltaPanel.style.top = `${event.pageY + 10}px`;
- deltaPanel.innerHTML = info;
-
- // Force redraw when frozen
- if (isFrozen) {
- draw();
- }
-}
-canvas.addEventListener('mousemove', updateInfo);
-canvas.addEventListener('mouseenter', () => deltaPanel.style.display = 'block');
-canvas.addEventListener('mouseleave', () => deltaPanel.style.display = 'none');
-
-canvas.addEventListener('click', (event) => {
- if (isFrozen) {
- // Set reference position in World Coordinates
- referencePosition = {
- t: XtoTime(event.offsetX),
- v: YtoVolts(event.offsetY)
- };
- }
-});
-
-// WebSocket
-let ws;
-let reconnectTimeout;
-
-function connect() {
- loadStoredConfig();
-
- clearTimeout(reconnectTimeout);
- const btn = document.getElementById('reconnectBtn');
- if (btn) btn.style.display = 'none';
-
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const wsUrl = `${protocol}//${window.location.host}/signal`;
- // For local testing without ESP hardware, uncomment next line:
- // const wsUrl = 'ws://localhost:8080/signal';
-
- ws = new WebSocket(wsUrl);
- ws.binaryType = 'arraybuffer';
- ws.onopen = () => {
- statusEl.textContent = 'Connected via WebSocket';
- statusEl.style.color = '#4ade80';
- ws.send("hello");
- };
-
- ws.onclose = () => {
- scheduleReconnect();
- };
-
- ws.onmessage = (event) => {
- try {
- const arr = new Uint16Array(event.data);
- if (arr?.length) {
- processData(arr);
- }
- } catch (e) {
- console.error('Parse error:', e);
- }
- };
-}
-
-// Animation loop
-function animationLoop() {
- if (!isFrozen) {
- draw();
- }
- requestAnimationFrame(animationLoop);
-}
-// Start the loop
-requestAnimationFrame(animationLoop);
-
-function processData(/** @type Uint16Array */newData) {
+/**
+ * Process incoming WebSocket data
+ * @param {Uint16Array} newData - Array of ADC samples
+ */
+function processData(newData) {
if (isFrozen) {
return; // Skip updating the buffer when frozen
}
@@ -313,9 +277,10 @@ function processData(/** @type Uint16Array */newData) {
if (lowRateState.targetCount <= 1) {
// Passthrough mode
- pushToBuffer(newData);
+ pushToBuffer(/** @type {Array} */(Array.from(newData)));
} else {
// Accumulation mode (Peak Detect) with Fractional Resampling
+ /** @type {DownsampledPoint[]} */
let pointsToPush = [];
let idx = 0;
@@ -356,7 +321,11 @@ function processData(/** @type Uint16Array */newData) {
}
}
-function pushToBuffer(/** @type Array */ newItems) {
+/**
+ * Push new items to the rolling data buffer
+ * @param {Array} newItems - New items to add
+ */
+function pushToBuffer(newItems) {
if (newItems.length >= countPoints) {
dataBuffer = Array.from(newItems.slice(-countPoints));
} else {
@@ -365,37 +334,18 @@ function pushToBuffer(/** @type Array */ newItems) {
}
}
-// Nice Number Generator
-function niceNum(range, round) {
- const exponent = Math.floor(Math.log10(range));
- const fraction = range / Math.pow(10, exponent);
- let niceFraction;
- if (round) {
- if (fraction < 1.5) niceFraction = 1;
- else if (fraction < 3) niceFraction = 2;
- else if (fraction < 7) niceFraction = 5;
- else niceFraction = 10;
- } else {
- if (fraction <= 1) niceFraction = 1;
- else if (fraction <= 2) niceFraction = 2;
- else if (fraction <= 5) niceFraction = 5;
- else niceFraction = 10;
- }
- return niceFraction * Math.pow(10, exponent);
-}
-function calculateNiceTicks(min, max, maxTicks) {
- const range = niceNum(max - min, false);
- const tickSpacing = niceNum(range / (maxTicks - 1), true);
- const niceMin = Math.floor(min / tickSpacing) * tickSpacing;
- const niceMax = Math.ceil(max / tickSpacing) * tickSpacing;
- const ticks = [];
- for (let t = niceMin; t <= niceMax + 0.00001; t += tickSpacing) {
- ticks.push(t);
- }
- return ticks;
-}
+/*
+ * ==========================================
+ * 5. Visualization & Rendering
+ * ==========================================
+ */
+/**
+ * Draw the grid and axis labels
+ * @param {number} w - Canvas width
+ * @param {number} h - Canvas height
+ */
function drawGrid(w, h) {
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
@@ -415,9 +365,6 @@ function drawGrid(w, h) {
const textWidth = metrics.width;
// Calculate box position
- // We align the text at (x,y) with specified 'align'
- // y is baseline. Visual center of 15px font is roughly y - 4
-
const boxHeight = fontSize + paddingY * 2;
const boxWidth = textWidth + paddingX * 2;
@@ -428,7 +375,7 @@ function drawGrid(w, h) {
// Adjust for padding and visual centering
boxX -= paddingX;
- const boxY = (y - 4) - boxHeight / 2; // Center box around text visual center
+ const boxY = (y - 4) - boxHeight / 2;
// Draw semi-transparent lozenge
ctx.fillStyle = 'rgba(255, 255, 255, 0.55)';
@@ -444,8 +391,8 @@ function drawGrid(w, h) {
};
// Determine Visible Voltage Range
- const minV = YtoVolts(h); // Bottom of screen (normally 0 if unzoomed, or higher/lower if zoomed/panned)
- const maxV = YtoVolts(0); // Top of screen
+ const minV = YtoVolts(h);
+ const maxV = YtoVolts(0);
// Calculate handy ticks in the visible range
const ticks = calculateNiceTicks(minV, maxV, 8);
@@ -468,8 +415,6 @@ function drawGrid(w, h) {
const minT = XtoTime(0);
const maxT = XtoTime(w);
- // Create ticks for time
- // Re-use logic or simple logic
const tTicks = calculateNiceTicks(minT, maxT, 8);
for (let t of tTicks) {
@@ -493,16 +438,40 @@ function drawGrid(w, h) {
}
}
+/**
+ * Draw crosshairs at specific position
+ * @param {number} x - X coordinate
+ * @param {number} y - Y coordinate
+ * @param {string} color - Color string
+ */
+function drawCrosshairs(x, y, color) {
+ ctx.setLineDash([5, 5]);
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 1;
+
+ // Draw vertical line
+ ctx.beginPath();
+ ctx.moveTo(x, 0);
+ ctx.lineTo(x, canvas.height);
+ ctx.stroke();
+
+ // Draw horizontal line
+ ctx.beginPath();
+ ctx.moveTo(0, y);
+ ctx.lineTo(canvas.width, y);
+ ctx.stroke();
+
+ ctx.setLineDash([]);
+}
+
+/**
+ * Main draw loop
+ */
function draw() {
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
- // Always draw the last waveform data
- ctx.beginPath();
- ctx.strokeStyle = '#4ade80';
- ctx.lineWidth = 2;
-
const maxAdcVal = 4096; // 12-bit fixed scale
// Trigger values
@@ -535,32 +504,25 @@ function draw() {
drawIdx -= 1;
}
}
+
+ const triggerVolts = ((maxAdcVal - activeConfig.trigger) * getMaxVoltage() / maxAdcVal).toFixed(2) + "V";
+ const triggerDir = triggerLevel.invert ? '🠉' : '🠋';
if (drawIdx < 0) {
drawIdx = dataBuffer.length - w;
+ triggerStatusEl.innerHTML = `${triggerVolts} ${triggerDir} No trigger`;
+ } else {
+ triggerStatusEl.innerHTML = `${triggerVolts} ${triggerDir} Triggered`;
}
}
- // NOTE: dataBuffer index 'i' maps to x pixel in initial scale.
- // sx = i * scale + offsetX
- // sy : VoltsToY(val_in_volts) Or simpler:
- // yp = h - (val / maxAdcVal * h)
- // sy = yp * scale + offsetY
// Pass 1: Draw Min/Max ranges for downsampled data
ctx.lineWidth = 1;
ctx.strokeStyle = '#2b7044'; // Dark green
ctx.beginPath();
- dataBuffer.slice(drawIdx).forEach((val, i) => {
- // Check if it's an object (downsampled)
+ for (let i = 0; i < w; i++) {
+ const val = dataBuffer[i + drawIdx];
if (typeof val === 'object' && val !== null) {
const sx = i * viewTransform.scale + viewTransform.offsetX;
- const yMin = VoltsToY(YtoVolts(h - (val.min / maxAdcVal * h))); // Convert to Y pixels
- // Note: simplified Y calculation:
- const yTop = h - (val.max / maxAdcVal * h) * viewTransform.scale + viewTransform.offsetY;
- // Wait, simple logic:
- // val -> Y pixel:
- // yp_raw = h - (raw / maxAdcVal * h)
- // yp_screen = yp_raw * scale + offsetY
-
const rawYMin = h - (val.min / maxAdcVal * h);
const rawYMax = h - (val.max / maxAdcVal * h);
@@ -570,7 +532,7 @@ function draw() {
ctx.moveTo(sx, screenYMin);
ctx.lineTo(sx, screenYMax);
}
- });
+ }
ctx.stroke();
// Pass 2: Draw Main Trace (Avg or raw value)
@@ -578,9 +540,10 @@ function draw() {
ctx.strokeStyle = '#4ade80'; // Bright green
ctx.beginPath();
- dataBuffer.slice(drawIdx).forEach((val, i) => {
+ for (let i = 0; i < w; i++) {
+ const val = dataBuffer[i + drawIdx];
const sx = i * viewTransform.scale + viewTransform.offsetX;
- let rawVal;
+ /** @type {number} */ let rawVal;
if (typeof val === 'object' && val !== null) {
rawVal = val.avg;
@@ -593,7 +556,7 @@ function draw() {
if (i === 0) ctx.moveTo(sx, sy);
else ctx.lineTo(sx, sy);
- });
+ }
ctx.stroke();
@@ -614,15 +577,202 @@ function draw() {
}
}
+/**
+ * Loop the animation
+ */
+function animationLoop() {
+ if (!isFrozen) {
+ draw();
+ }
+ requestAnimationFrame(animationLoop);
+}
+
+
+/*
+ * ==========================================
+ * 6. User Interaction
+ * ==========================================
+ */
+
+/**
+ * Update background color of trigger level slider
+ */
+function triggerColor() {
+ activeConfig.trigger = parseInt(triggerLevel.value);
+ const value = (activeConfig.trigger - parseInt(triggerLevel.min)) / (parseInt(triggerLevel.max) - parseInt(triggerLevel.min)) * 100;
+ triggerLevel.style.background = triggerLevel.invert
+ ? `linear-gradient(to bottom, #00000070 0%, #ff020270 ${value}%, #1302ff70 ${value}%, #00000070 100%)`
+ : `linear-gradient(to bottom, #00000070 0%, #1302ff70 ${value}%, #ff020270 ${value}%, #00000070 100%)`
+}
+
+/**
+ * Update Mouse info
+ * @param {MouseEvent | {offsetX: number, offsetY: number, pageX: number, pageY: number}} event
+ */
+function updateInfo(event) {
+ // Use raw coordinates or event helpers
+ const voltage = YtoVolts(event.offsetY);
+ const timeOffset = XtoTime(event.offsetX);
+ let info = `${voltage.toFixed(3)}V, ${timeOffset.toFixed(2)}ms
`;
+
+ // Store the last mouse position
+ lastMousePosition.x = event.offsetX;
+ lastMousePosition.y = event.offsetY;
+
+ // Update delta panel position and content if frozen
+ if (isFrozen && referencePosition) {
+ const deltaVoltage = Math.abs(referencePosition.v - voltage);
+ const deltaTime = Math.abs(referencePosition.t - timeOffset);
+
+ // avoid divide by zero
+ const freq = deltaTime > 0.00001 ? (1000 / deltaTime).toFixed(2) : '---';
+ info += `ΔV ${deltaVoltage.toFixed(3)}V, ΔT ${deltaTime.toFixed(2)}ms (${freq} Hz)
`;
+ }
+
+ deltaPanel.style.left = `${event.pageX + 10}px`;
+ deltaPanel.style.top = `${event.pageY + 10}px`;
+ deltaPanel.innerHTML = info;
+
+ // Force redraw when frozen
+ if (isFrozen) {
+ draw();
+ }
+}
+
+// Trigger Level Events
+triggerLevel.addEventListener('input', triggerColor);
+triggerLevel.addEventListener('mousedown', function () {
+ this.downValue = this.value;
+});
+triggerLevel.addEventListener('mouseup', function () {
+ if (this.downValue == this.value) {
+ this.invert = !this.invert;
+ activeConfig.invert = this.invert;
+ triggerColor();
+ this.dispatchEvent(new Event("change"));
+ }
+ delete this.downValue;
+});
+
+// Canvas Interaction Events
+canvas.addEventListener('click', (event) => {
+ isFrozen = !isFrozen;
+ if (isFrozen) {
+ // Set reference position in World Coordinates
+ referencePosition = {
+ t: XtoTime(event.offsetX),
+ v: YtoVolts(event.offsetY)
+ };
+ }
+});
+
+canvas.addEventListener('mousemove', updateInfo);
+canvas.addEventListener('mouseenter', () => deltaPanel.style.display = 'block');
+canvas.addEventListener('mouseleave', () => deltaPanel.style.display = 'none');
+
+canvas.addEventListener('wheel', function (e) {
+ e.preventDefault();
+ const zoomFactor = 1.1;
+ const direction = e.deltaY < 0 ? 1 : -1;
+ const factor = direction > 0 ? zoomFactor : 1 / zoomFactor;
+
+ let newScale = viewTransform.scale * factor;
+
+ if (newScale < 1.001) {
+ // Snap to 100% and reset position
+ newScale = 1.0;
+ viewTransform.offsetX = 0;
+ viewTransform.offsetY = 0;
+ } else if (newScale > 50) {
+ return; // Max limit
+ } else {
+ // Zoom centered on mouse
+ const mx = e.offsetX;
+ const my = e.offsetY;
+
+ viewTransform.offsetX = mx - (mx - viewTransform.offsetX) * factor;
+ viewTransform.offsetY = my - (my - viewTransform.offsetY) * factor;
+ }
+
+ viewTransform.scale = newScale;
+ if (newScale < 1.001) {
+ statusEl.textContent = 'Connected via WebSocket';
+ } else {
+ statusEl.textContent = 'Scaled to ' + newScale.toFixed(2) + 'x';
+ }
+
+ draw();
+ if (isFrozen) {
+ updateInfo({ offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY });
+ }
+});
+
+
+/*
+ * ==========================================
+ * 7. Network & Configuration Management
+ * ==========================================
+ */
+
+/**
+ * Connect to WebSocket
+ */
+function connect() {
+ loadStoredConfig();
+
+ clearTimeout(reconnectTimeout);
+ if (reconnectBtn) reconnectBtn.style.display = 'none';
+
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsUrl = `${protocol}//${window.location.host}/signal`;
+ // For local testing without ESP hardware, uncomment next line:
+ // const wsUrl = 'ws://localhost:8080/signal';
+
+ ws = new WebSocket(wsUrl);
+ ws.binaryType = 'arraybuffer';
+ ws.onopen = () => {
+ statusEl.textContent = 'Connected via WebSocket';
+ statusEl.style.color = '#4ade80';
+ if (ws.readyState === WebSocket.OPEN) ws.send("hello");
+ };
+
+ ws.onclose = () => {
+ scheduleReconnect();
+ };
+
+ ws.onmessage = (event) => {
+ try {
+ const arr = new Uint16Array(event.data);
+ if (arr?.length) {
+ processData(arr);
+ }
+ } catch (e) {
+ console.error('Parse error:', e);
+ }
+ };
+}
+
+/**
+ * Schedule a reconnect attempt
+ */
+function scheduleReconnect() {
+ statusEl.textContent = 'Disconnected. Retrying in 2s...';
+ statusEl.style.color = '#ef4444';
+ reconnectTimeout = window.setTimeout(connect, 2000); // Use window.setTimeout explicit
+}
+
+/**
+ * Send configuration to ESP32
+ */
function setParams() {
- const desiredRate = parseInt(document.getElementById('sampleRate').value);
+ const desiredRate = parseInt(sampleRateSelect.value);
const hardwareRate = desiredRate < 1000 ? 1000 : desiredRate;
const payload = {
sample_rate: hardwareRate,
- bit_width: parseInt(document.getElementById('bitWidth').value),
- atten: parseInt(document.getElementById('atten').value),
- test_hz: parseInt(document.getElementById('testHz').value)
+ bit_width: parseInt(bitWidthSelect.value),
+ atten: parseInt(attenSelect.value),
+ test_hz: parseInt(testHzSelect.value)
};
fetch('/params', {
@@ -637,7 +787,7 @@ function setParams() {
lowRateState.accCount = 0;
// Update active config
- activeConfig = { ...payload, desiredRate, trigger: parseInt(triggerLevel.value) || 2048 };
+ activeConfig = { ...payload, desiredRate, trigger: parseInt(triggerLevel.value) || 2048, invert: triggerLevel.invert };
// Save to localStorage
localStorage.setItem('esp32_adc_config', JSON.stringify(activeConfig));
@@ -645,20 +795,22 @@ function setParams() {
alert('Error updating configuration');
}
}).catch(err => alert('Network error: ' + err));
-};
+}
-// Load config from localStorage on startup
+/**
+ * Load configuration from LocalStorage
+ */
function loadStoredConfig() {
const stored = localStorage.getItem('esp32_adc_config');
if (stored) {
try {
const cfg = JSON.parse(stored);
- if (cfg.desiredRate) document.getElementById('sampleRate').value = cfg.desiredRate;
- if (cfg.bit_width) document.getElementById('bitWidth').value = cfg.bit_width;
- if (cfg.atten !== undefined) document.getElementById('atten').value = cfg.atten;
- if (cfg.test_hz) document.getElementById('testHz').value = cfg.test_hz;
+ if (cfg.desiredRate) sampleRateSelect.value = cfg.desiredRate;
+ if (cfg.bit_width) bitWidthSelect.value = cfg.bit_width;
+ if (cfg.atten !== undefined) attenSelect.value = cfg.atten;
+ if (cfg.test_hz) testHzSelect.value = cfg.test_hz;
if (cfg.invert) triggerLevel.invert = Boolean(cfg.invert);
- if (cfg.trigger) triggerLevel.value = cfg.trigger;
+ if (cfg.trigger) triggerLevel.value = String(cfg.trigger);
triggerColor();
setParams();
} catch (e) {
@@ -667,36 +819,37 @@ function loadStoredConfig() {
}
}
-document.getElementById('reconnectBtn').addEventListener('click', connect);
-document.querySelectorAll('#sampleRate, #bitWidth, #atten, #testHz').forEach(input => input.addEventListener('change', setParams));
+// Config Listeners
+if (reconnectBtn) reconnectBtn.addEventListener('click', connect);
+[sampleRateSelect, bitWidthSelect, attenSelect, testHzSelect].forEach(input => {
+ if (input) input.addEventListener('change', setParams)
+});
triggerLevel.addEventListener('change', () => localStorage.setItem('esp32_adc_config', JSON.stringify(activeConfig)));
-document.getElementById('resetBtn').addEventListener('click', () => {
+if (resetBtn) resetBtn.addEventListener('click', () => {
localStorage.clear();
window.location.reload();
});
-document.getElementById('powerOff').addEventListener('click', () => window.location.href = "/poweroff");
+if (powerOffBtn) powerOffBtn.addEventListener('click', () => window.location.href = "/poweroff");
+/**
+ * Setup WiFi modal listeners
+ */
function setupWifiListeners() {
- const modal = document.getElementById('wifiModal');
- const btn = document.getElementById('wifiBtn');
- const closeBtn = document.getElementById('closeWifi');
- const saveBtn = document.getElementById('saveWifi');
-
- if (btn) btn.onclick = () => {
- modal.style.display = "flex";
- document.getElementById('wifiSsid').focus();
+ if (wifiBtn) wifiBtn.onclick = () => {
+ wifiModal.style.display = "flex";
+ wifiSsidInput.focus();
};
- if (closeBtn) closeBtn.onclick = () => modal.style.display = "none";
- if (saveBtn) saveBtn.onclick = () => {
- const ssid = document.getElementById('wifiSsid').value;
- const pass = document.getElementById('wifiPass').value;
+ if (closeWifiBtn) closeWifiBtn.onclick = () => wifiModal.style.display = "none";
+ if (saveWifiBtn) saveWifiBtn.onclick = () => {
+ const ssid = wifiSsidInput.value;
+ const pass = wifiPassInput.value;
if (!ssid) {
alert("SSID is required");
return;
}
- saveBtn.innerText = "Saving...";
+ saveWifiBtn.innerText = "Saving...";
fetch('/api/save_wifi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -709,10 +862,31 @@ function setupWifiListeners() {
})
.catch(err => {
alert("Error: " + err);
- saveBtn.innerText = "Save";
+ saveWifiBtn.innerText = "Save";
});
};
}
+
+
+/*
+ * ==========================================
+ * 8. Initialization
+ * ==========================================
+ */
+
+/**
+ * Resize canvas to fit container
+ */
+function resize() {
+ canvas.width = canvas.offsetWidth;
+ canvas.height = canvas.offsetHeight;
+}
+
+window.addEventListener('resize', resize);
+
+// Startup sequence
+resize();
setupWifiListeners();
-setParams();
+loadStoredConfig();
connect();
+requestAnimationFrame(animationLoop);