Inital commit
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# ESP-IDF
|
||||
/build/
|
||||
/sdkconfig
|
||||
/sdkconfig.old
|
||||
/managed_components/
|
||||
/dependencies.lock
|
||||
/.cache
|
||||
/.clangd
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
4
CMakeLists.txt
Normal file
4
CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(esp-scope)
|
||||
5
main/CMakeLists.txt
Normal file
5
main/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(SRCS "main.c"
|
||||
INCLUDE_DIRS "."
|
||||
EMBED_TXTFILES "index.html" "index.js"
|
||||
REQUIRES esp_http_server esp_adc nvs_flash esp_wifi driver esp_netif freertos esp_event)
|
||||
|
||||
15
main/Kconfig
Normal file
15
main/Kconfig
Normal file
@@ -0,0 +1,15 @@
|
||||
menu "espScope Configuration"
|
||||
|
||||
config ESP_WIFI_SSID
|
||||
string "WiFi SSID"
|
||||
default "myssid"
|
||||
help
|
||||
SSID (network name) to connect to.
|
||||
|
||||
config ESP_WIFI_PASSWORD
|
||||
string "WiFi Password"
|
||||
default "mypassword"
|
||||
help
|
||||
WiFi password.
|
||||
|
||||
endmenu
|
||||
2
main/idf_component.yml
Normal file
2
main/idf_component.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
espressif/cjson: "^1.7.0"
|
||||
177
main/index.html
Normal file
177
main/index.html
Normal file
@@ -0,0 +1,177 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>espScope</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9em;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 80%;
|
||||
padding: 8px;
|
||||
background: #3d3d3d;
|
||||
border: 1px solid #555;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #4ade80;
|
||||
color: #1a1a1a;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-family: monospace;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-family: monospace;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#deltaPanel {
|
||||
position: absolute;
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
display: none;
|
||||
}
|
||||
|
||||
#triggerLevel {
|
||||
/* appearance: slider-vertical; */
|
||||
position: relative;
|
||||
height: 408px;
|
||||
top: -16px;
|
||||
margin-left: 10px;
|
||||
width: 1px;
|
||||
writing-mode: vertical-lr;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
#resetBtn {
|
||||
background:#ef4444;
|
||||
color:#fff;
|
||||
padding:4px 8px;
|
||||
font-size:0.8rem;
|
||||
height: 4em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div style="display: flex; position: relative;">
|
||||
<canvas id="adcChart"></canvas>
|
||||
<input type="range" id="triggerLevel" orient="vertical" min="0" max="4096" value="2048">
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<div class="status" id="status">Connecting...</div>
|
||||
<button id="reconnectBtn"
|
||||
style="display:none; background:#eab308; color:#000; padding:4px 8px; font-size:0.8rem;">Reconnect
|
||||
Now</button>
|
||||
<div class="info" id="info"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form id="configForm" class="controls">
|
||||
<div>
|
||||
<label>Sample Rate (Hz)</label>
|
||||
<input type="number" id="sampleRate" value="10000" min="1" max="100000">
|
||||
</div>
|
||||
<div>
|
||||
<label>Bit Width</label>
|
||||
<select id="bitWidth">
|
||||
<option value="12" selected>12-bit</option>
|
||||
<option value="11">11-bit</option>
|
||||
<option value="10">10-bit</option>
|
||||
<option value="9">9-bit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Attenuation</label>
|
||||
<select id="atten">
|
||||
<option value="0">0 dB</option>
|
||||
<option value="1">2.5 dB</option>
|
||||
<option value="2">6 dB</option>
|
||||
<option value="3" selected>11 dB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<div>
|
||||
<label>Test Hz</label>
|
||||
<input type="number" id="testHz" value="100" min="1" max="10000">
|
||||
</div>
|
||||
<button id="resetBtn">Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="deltaPanel"></div>
|
||||
</div>
|
||||
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
445
main/index.js
Normal file
445
main/index.js
Normal file
@@ -0,0 +1,445 @@
|
||||
const canvas = document.getElementById('adcChart');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const statusEl = document.getElementById('status');
|
||||
const deltaPanel = document.getElementById('deltaPanel');
|
||||
const infoEl = document.getElementById('info');
|
||||
const triggerLevel = document.getElementById('triggerLevel');
|
||||
|
||||
// Current active configuration for display scaling
|
||||
let activeConfig = {
|
||||
desiredRate: 10000,
|
||||
sample_rate: 10000,
|
||||
atten: 3, // 11dB Default
|
||||
bit_width: 12,
|
||||
test_hz: 100,
|
||||
trigger: 2048
|
||||
};
|
||||
|
||||
// Accumulator for virtual low sample rates
|
||||
let lowRateState = {
|
||||
accMin: 4096,
|
||||
accMax: 0,
|
||||
count: 0,
|
||||
targetCount: 1
|
||||
};
|
||||
|
||||
// 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];
|
||||
|
||||
// Resize canvas
|
||||
function resize() {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
let lastMousePosition = { x: null, y: null };
|
||||
|
||||
let isFrozen = false; // Track freeze state
|
||||
|
||||
function toggleFreeze() {
|
||||
isFrozen = !isFrozen;
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', toggleFreeze);
|
||||
|
||||
let referencePosition = null; // Store the reference position for deltas
|
||||
|
||||
// Helper function to calculate voltage
|
||||
function calculateVoltage(offsetY) {
|
||||
const maxV = ATTEN_TO_MAX_V[activeConfig.atten] || 3.3;
|
||||
return ((canvas.height - offsetY) / canvas.height * maxV).toFixed(2);
|
||||
}
|
||||
|
||||
// Helper function to calculate effective sample rate
|
||||
function getEffectiveSampleRate() {
|
||||
return activeConfig.desiredRate < 1000
|
||||
? activeConfig.desiredRate / lowRateState.targetCount
|
||||
: activeConfig.desiredRate;
|
||||
}
|
||||
|
||||
// Helper function to calculate time offset
|
||||
function calculateTimeOffset(offsetX, effectiveSampleRate) {
|
||||
let effectivePoints = maxPoints;
|
||||
if (activeConfig.desiredRate < 1000) {
|
||||
effectivePoints = maxPoints / (lowRateState.targetCount * 2);
|
||||
}
|
||||
|
||||
const totalTimeMs = (effectivePoints / effectiveSampleRate) * 1000;
|
||||
return ((offsetX / canvas.width) * totalTimeMs).toFixed(2);
|
||||
}
|
||||
|
||||
// Helper function to reset lowRateState
|
||||
function resetLowRateState() {
|
||||
lowRateState.accMin = 4096;
|
||||
lowRateState.accMax = 0;
|
||||
lowRateState.count = 0;
|
||||
}
|
||||
|
||||
// Helper function to schedule WebSocket reconnection
|
||||
function scheduleReconnect() {
|
||||
statusEl.textContent = 'Disconnected. Retrying in 2s...';
|
||||
statusEl.style.color = '#ef4444';
|
||||
reconnectTimeout = setTimeout(connect, 2000);
|
||||
}
|
||||
|
||||
// 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([]);
|
||||
}
|
||||
|
||||
// Refactor duplicated code to use helper functions
|
||||
function updateInfo(event) {
|
||||
const voltage = calculateVoltage(event.offsetY);
|
||||
const effectiveSampleRate = getEffectiveSampleRate();
|
||||
const timeOffset = calculateTimeOffset(event.offsetX, effectiveSampleRate);
|
||||
infoEl.textContent = `Voltage: ${voltage}V, Time: ${timeOffset}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 maxV = ATTEN_TO_MAX_V[activeConfig.atten] || 3.3;
|
||||
const deltaVoltage = (referencePosition.voltage - ((canvas.height - lastMousePosition.y) / canvas.height * maxV)).toFixed(2);
|
||||
const deltaTime = (referencePosition.time - ((lastMousePosition.x / canvas.width) * maxPoints / effectiveSampleRate * 1000)).toFixed(2);
|
||||
|
||||
deltaPanel.style.left = `${event.pageX + 10}px`;
|
||||
deltaPanel.style.top = `${event.pageY + 10}px`;
|
||||
deltaPanel.style.display = 'block';
|
||||
deltaPanel.innerHTML = `<div>ΔVoltage: ${deltaVoltage}V</div><div>ΔTime: ${deltaTime}ms</div>`;
|
||||
} else {
|
||||
deltaPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
// Force redraw when frozen
|
||||
if (isFrozen) {
|
||||
draw();
|
||||
}
|
||||
}
|
||||
canvas.addEventListener('mousemove', updateInfo);
|
||||
|
||||
canvas.addEventListener('click', (event) => {
|
||||
if (isFrozen) {
|
||||
const voltage = calculateVoltage(event.offsetY);
|
||||
const effectiveSampleRate = getEffectiveSampleRate();
|
||||
const timeOffset = calculateTimeOffset(event.offsetX, effectiveSampleRate);
|
||||
|
||||
// Set reference position for deltas
|
||||
referencePosition = {
|
||||
x: event.offsetX,
|
||||
y: event.offsetY,
|
||||
voltage,
|
||||
time: timeOffset
|
||||
};
|
||||
|
||||
draw();
|
||||
}
|
||||
});
|
||||
|
||||
// Data buffer
|
||||
const maxPoints = 1000;
|
||||
let dataBuffer = new Array(maxPoints).fill(0);
|
||||
|
||||
// WebSocket
|
||||
// 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.onopen = () => {
|
||||
statusEl.textContent = 'Connected via WebSocket';
|
||||
statusEl.style.color = '#4ade80';
|
||||
ws.send("hello");
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.data && Array.isArray(msg.data)) {
|
||||
processData(msg.data);
|
||||
draw();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse error:', e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function processData(newData) {
|
||||
if (isFrozen) {
|
||||
return; // Skip updating the buffer when frozen
|
||||
}
|
||||
|
||||
// Recalculate target count based on ratio
|
||||
// If virtual rate < 1000, we forced hardware to 1000
|
||||
if (activeConfig.desiredRate < 1000) {
|
||||
lowRateState.targetCount = activeConfig.sample_rate / activeConfig.desiredRate;
|
||||
} else {
|
||||
lowRateState.targetCount = 1;
|
||||
}
|
||||
|
||||
if (lowRateState.targetCount <= 1) {
|
||||
// Passthrough mode
|
||||
pushToBuffer(newData);
|
||||
} else {
|
||||
// Accumulation mode (Peak Detect)
|
||||
let pointsToPush = [];
|
||||
for (let val of newData) {
|
||||
if (val < lowRateState.accMin) lowRateState.accMin = val;
|
||||
if (val > lowRateState.accMax) lowRateState.accMax = val;
|
||||
lowRateState.count++;
|
||||
|
||||
if (lowRateState.count >= lowRateState.targetCount) {
|
||||
// Push min and max to draw a vertical line
|
||||
pointsToPush.push(lowRateState.accMin);
|
||||
pointsToPush.push(lowRateState.accMax);
|
||||
|
||||
// Reset
|
||||
resetLowRateState();
|
||||
}
|
||||
}
|
||||
if (pointsToPush.length > 0) {
|
||||
pushToBuffer(pointsToPush);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pushToBuffer(newItems) {
|
||||
if (newItems.length >= maxPoints) {
|
||||
dataBuffer = newItems.slice(-maxPoints);
|
||||
} else {
|
||||
dataBuffer.splice(0, newItems.length);
|
||||
dataBuffer.push(...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;
|
||||
}
|
||||
|
||||
function drawGrid(w, h) {
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '15px monospace';
|
||||
|
||||
// Y-Axis: Voltage (Nice Ticks)
|
||||
ctx.textAlign = 'left';
|
||||
const maxV = ATTEN_TO_MAX_V[activeConfig.atten] || 3.3;
|
||||
|
||||
const ticks = calculateNiceTicks(0, maxV, 6); // Aim for ~6 ticks
|
||||
|
||||
for (let val of ticks) {
|
||||
if (val > maxV) continue; // Don't draw above max
|
||||
|
||||
const y = h - (val / maxV * h);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
|
||||
// Don't draw label at 0 (overlaps time)
|
||||
if (val > 0.01) ctx.fillText(val.toFixed(2) + 'V', 5, y + 3);
|
||||
}
|
||||
|
||||
// X-Axis: Time
|
||||
// For peak detect mode, we push 2 points per 1 virtual sample.
|
||||
// So the buffer effectively holds (maxPoints / 2) * timePerSample
|
||||
let effectivePoints = maxPoints;
|
||||
const effectiveSampleRate = getEffectiveSampleRate();
|
||||
|
||||
if (activeConfig.desiredRate < 1000) {
|
||||
effectivePoints = maxPoints / (lowRateState.targetCount * 2);
|
||||
}
|
||||
|
||||
const totalTimeMs = (effectivePoints / effectiveSampleRate) * 1000;
|
||||
const xSteps = 5;
|
||||
ctx.textAlign = 'center';
|
||||
for (let i = 0; i <= xSteps; i++) {
|
||||
const x = (i / xSteps * w);
|
||||
const tMs = (i / xSteps * totalTimeMs);
|
||||
let timeStr;
|
||||
if (tMs >= 1000) {
|
||||
timeStr = (tMs / 1000).toFixed(2) + 's';
|
||||
} else {
|
||||
timeStr = tMs.toFixed(1) + 'ms';
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, h);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillText(timeStr, x, h - 5);
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw Background Grid
|
||||
drawGrid(w, h);
|
||||
|
||||
// Always draw the last waveform data
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#4ade80';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
const step = w / (maxPoints - 1);
|
||||
const maxAdcVal = 4096; // 12-bit fixed scale
|
||||
|
||||
// Trigger values
|
||||
let drawData = dataBuffer;
|
||||
const triggerVal = parseInt(triggerLevel.value) || 2048;
|
||||
for (let i = 0; i < dataBuffer.length; i++) {
|
||||
if (dataBuffer[i] < triggerVal && dataBuffer[i + 1] > triggerVal) {
|
||||
drawData = dataBuffer.slice(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
drawData.forEach((val, i) => {
|
||||
const x = i * step;
|
||||
const y = h - (val / maxAdcVal * h);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Crosshairs if mouse is over the canvas
|
||||
if (lastMousePosition.x !== null && lastMousePosition.y !== null) {
|
||||
drawCrosshairs(lastMousePosition.x, lastMousePosition.y, '#4ade80');
|
||||
}
|
||||
|
||||
// Draw reference crosshairs and deltas if frozen
|
||||
if (isFrozen && referencePosition) {
|
||||
drawCrosshairs(referencePosition.x, referencePosition.y, '#eab308');
|
||||
}
|
||||
}
|
||||
|
||||
function setParams() {
|
||||
const desiredRate = parseInt(document.getElementById('sampleRate').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)
|
||||
};
|
||||
|
||||
fetch('/params', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
resetLowRateState();
|
||||
|
||||
// Update active config
|
||||
activeConfig = { ...payload, desiredRate, trigger: parseInt(triggerLevel.value) || 2048 };
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('esp32_adc_config', JSON.stringify(activeConfig));
|
||||
} else {
|
||||
alert('Error updating configuration');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Network error: ' + err));
|
||||
};
|
||||
|
||||
// Load config from localStorage on startup
|
||||
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.trigger) document.getElementById('triggerLevel').value = cfg.trigger;
|
||||
setParams();
|
||||
} catch (e) {
|
||||
console.error("Failed to load config", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('reconnectBtn').addEventListener('click', connect);
|
||||
document.querySelectorAll('#sampleRate, #bitWidth, #atten, #testHz, #triggerLevel').forEach(input => input.addEventListener('change', setParams));
|
||||
document.getElementById('resetBtn').addEventListener('click', () => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
});
|
||||
setParams();
|
||||
connect();
|
||||
|
||||
567
main/main.c
Normal file
567
main/main.c
Normal file
@@ -0,0 +1,567 @@
|
||||
#include "cJSON.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/ledc.h"
|
||||
#include "esp_adc/adc_continuous.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "freertos/ringbuf.h"
|
||||
#include "freertos/task.h"
|
||||
#include "nvs_flash.h"
|
||||
#include <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// Tag for logging
|
||||
static const char *TAG = "ESP-SCOPE";
|
||||
|
||||
// WiFi configuration from Kconfig
|
||||
#define ESP_WIFI_SSID "SSID"
|
||||
#define ESP_WIFI_PASS "PASSWORD"
|
||||
#define ESP_MAXIMUM_RETRY 5
|
||||
|
||||
/* FreeRTOS event group to signal when we are connected*/
|
||||
static EventGroupHandle_t s_wifi_event_group;
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
static int s_retry_num = 0;
|
||||
|
||||
// Embedded index.html
|
||||
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
|
||||
extern const uint8_t index_html_end[] asm("_binary_index_html_end");
|
||||
extern const uint8_t index_js_start[] asm("_binary_index_js_start");
|
||||
extern const uint8_t index_js_end[] asm("_binary_index_js_end");
|
||||
|
||||
// Forward declarations
|
||||
static void wifi_init_sta(void);
|
||||
static void start_webserver(void);
|
||||
|
||||
// ADC Configuration
|
||||
#define ADC_UNIT ADC_UNIT_1
|
||||
#define _ADC_UNIT_STR(unit) #unit
|
||||
#define ADC_UNIT_STR(unit) _ADC_UNIT_STR(unit)
|
||||
#define ADC_CONV_MODE ADC_CONV_SINGLE_UNIT_1
|
||||
#define ADC_ATTEN ADC_ATTEN_DB_11
|
||||
#define ADC_BIT_WIDTH ADC_BITWIDTH_12
|
||||
|
||||
#define ADC_OUTPUT_TYPE ADC_DIGI_OUTPUT_FORMAT_TYPE2
|
||||
#define ADC_GET_CHANNEL(p_data) ((p_data)->type2.channel)
|
||||
#define ADC_GET_DATA(p_data) ((p_data)->type2.data)
|
||||
|
||||
#define ADC_READ_LEN 512
|
||||
#define ADC_MAX_STORE_BUF_SIZE 1024
|
||||
|
||||
static adc_continuous_handle_t adc_handle = NULL;
|
||||
static TaskHandle_t s_task_handle;
|
||||
static RingbufHandle_t s_ringbuf_handle;
|
||||
|
||||
// Single client support for simplicity, or use a list for multiple
|
||||
static int s_ws_client_fd = -1;
|
||||
|
||||
// Global configuration state
|
||||
static volatile bool s_reconfig_needed = false;
|
||||
static uint32_t s_sample_rate = 20000;
|
||||
static adc_atten_t s_atten = ADC_ATTEN_DB_12;
|
||||
static adc_bitwidth_t s_bit_width = ADC_BIT_WIDTH;
|
||||
static uint16_t s_test_hz = 100;
|
||||
|
||||
// Forward declarations
|
||||
static void wifi_init_sta(void);
|
||||
static void continuous_adc_init(adc_channel_t *channel, uint8_t channel_num,
|
||||
adc_continuous_handle_t *out_handle);
|
||||
|
||||
/*
|
||||
* Task to read from ADC Continuous driver
|
||||
*/
|
||||
static void adc_read_task(void *arg) {
|
||||
esp_err_t ret;
|
||||
uint32_t ret_num = 0;
|
||||
uint8_t result[ADC_READ_LEN] = {0};
|
||||
memset(result, 0xcc, ADC_READ_LEN);
|
||||
|
||||
s_task_handle = xTaskGetCurrentTaskHandle();
|
||||
|
||||
// ADC Init (Moved from app_main)
|
||||
// TODO: Make this configurable or find a good default pin.
|
||||
// For ESP32C6 ADC1 Channel 0 is usually GPIO 0. Let's use Channel 0 for now.
|
||||
adc_channel_t channel[1] = {ADC_CHANNEL_0};
|
||||
continuous_adc_init(channel, sizeof(channel) / sizeof(adc_channel_t),
|
||||
&adc_handle);
|
||||
ESP_ERROR_CHECK(adc_continuous_start(adc_handle));
|
||||
|
||||
while (1) {
|
||||
if (s_reconfig_needed) {
|
||||
ESP_LOGI(TAG, "Reconfiguring ADC...");
|
||||
if (adc_handle) {
|
||||
ESP_LOGI(TAG, "Stopping ADC...");
|
||||
ret = adc_continuous_stop(adc_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "adc_continuous_stop failed: %s", esp_err_to_name(ret));
|
||||
}
|
||||
ESP_LOGI(TAG, "Deinitializing ADC...");
|
||||
ret = adc_continuous_deinit(adc_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "adc_continuous_deinit failed: %s",
|
||||
esp_err_to_name(ret));
|
||||
}
|
||||
adc_handle = NULL;
|
||||
}
|
||||
|
||||
// Small delay to ensure hardware state clears
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
|
||||
// Update global defaults for next init
|
||||
// Note: In a robust app, we should pass these to init function
|
||||
// For now, we rely on the global s_sample_rate etc being read by init
|
||||
adc_channel_t channel[1] = {ADC_CHANNEL_0};
|
||||
continuous_adc_init(channel, 1, &adc_handle);
|
||||
|
||||
ESP_LOGI(TAG, "Starting ADC...");
|
||||
ESP_ERROR_CHECK(adc_continuous_start(adc_handle));
|
||||
ESP_LOGI(TAG, "ADC Reconfigured and Restarted");
|
||||
s_reconfig_needed = false;
|
||||
}
|
||||
|
||||
ret = adc_continuous_read(adc_handle, result, ADC_READ_LEN, &ret_num, 0);
|
||||
if (ret == ESP_OK) {
|
||||
// ESP_LOGI(TAG, "ret is %x, ret_num is %"PRIu32" bytes", ret, ret_num);
|
||||
for (int i = 0; i < ret_num; i += sizeof(adc_digi_output_data_t)) {
|
||||
adc_digi_output_data_t *p = (adc_digi_output_data_t *)&result[i];
|
||||
uint32_t chan_num = ADC_GET_CHANNEL(p);
|
||||
uint32_t data = ADC_GET_DATA(p);
|
||||
/* Check the channel number validation, the data is invalid if the
|
||||
* channel num exceed the maximum channel */
|
||||
if (chan_num < SOC_ADC_CHANNEL_NUM(ADC_UNIT)) {
|
||||
// ESP_LOGI(TAG, "Unit: %s, Channel: %"PRIu32", Value: %"PRIu32,
|
||||
// ADC_UNIT_STR(ADC_UNIT), chan_num, data);
|
||||
// ADC_UNIT_STR(ADC_UNIT), chan_num, data);
|
||||
// Send data to RingBuffer
|
||||
// We send raw value or processed? Sending raw 12-bit value is
|
||||
// smaller. Let's send the 32-bit `adc_digi_output_data_t` itself or
|
||||
// just the value. Sending just value (uint16_t) saves space.
|
||||
uint16_t val = (uint16_t)data;
|
||||
xRingbufferSend(s_ringbuf_handle, &val, sizeof(val), 0);
|
||||
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid data [%" PRIu32 "_%" PRIu32 "]", chan_num,
|
||||
data);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Because printing is slow, potentially delay here or yield if we fill
|
||||
* buffers too fast. But for continuous mode we usually just want to drain
|
||||
* the buffer. For now, just a small yield to prevent watchdog if we spin
|
||||
* tight.
|
||||
*/
|
||||
vTaskDelay(1);
|
||||
} else if (ret == ESP_ERR_TIMEOUT) {
|
||||
// We try to read `ADC_READ_LEN` until API returns timeout, which means
|
||||
// there's no available data
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void continuous_adc_init(adc_channel_t *channel, uint8_t channel_num,
|
||||
adc_continuous_handle_t *out_handle) {
|
||||
adc_continuous_handle_cfg_t adc_config = {
|
||||
.max_store_buf_size = 1024,
|
||||
.conv_frame_size = ADC_READ_LEN,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_config, out_handle));
|
||||
|
||||
adc_continuous_config_t dig_cfg = {
|
||||
.sample_freq_hz = s_sample_rate,
|
||||
.conv_mode = ADC_CONV_MODE,
|
||||
.format = ADC_OUTPUT_TYPE,
|
||||
};
|
||||
|
||||
adc_digi_pattern_config_t adc_pattern[SOC_ADC_PATT_LEN_MAX] = {0};
|
||||
dig_cfg.pattern_num = channel_num;
|
||||
|
||||
for (int i = 0; i < channel_num; i++) {
|
||||
adc_pattern[i].atten = s_atten;
|
||||
adc_pattern[i].channel = channel[i] & 0x7;
|
||||
adc_pattern[i].unit = ADC_UNIT;
|
||||
adc_pattern[i].bit_width = s_bit_width;
|
||||
|
||||
ESP_LOGI(TAG, "adc_pattern[%d].atten is :%" PRIx8, i, adc_pattern[i].atten);
|
||||
ESP_LOGI(TAG, "adc_pattern[%d].channel is :%" PRIx8, i,
|
||||
adc_pattern[i].channel);
|
||||
ESP_LOGI(TAG, "adc_pattern[%d].unit is :%" PRIx8, i, adc_pattern[i].unit);
|
||||
}
|
||||
dig_cfg.adc_pattern = adc_pattern;
|
||||
ESP_ERROR_CHECK(adc_continuous_config(*out_handle, &dig_cfg));
|
||||
}
|
||||
static bool ledc_inited = false;
|
||||
static void start_test_signal(uint32_t hz) {
|
||||
if (ledc_inited) {
|
||||
ESP_LOGI(TAG, "De-init test signal");
|
||||
gpio_reset_pin(GPIO_NUM_1);
|
||||
|
||||
// Stop the PWM signal on channel 0
|
||||
ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0);
|
||||
|
||||
// Reset the timer configuration
|
||||
ledc_timer_rst(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0);
|
||||
|
||||
// (Optional) Uninstall fade functionality if used
|
||||
ledc_fade_func_uninstall();
|
||||
}
|
||||
ESP_LOGI(TAG, "Starting test signal at %u Hz", hz);
|
||||
ledc_timer_config_t ledc_timer = {
|
||||
.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
.duty_resolution = LEDC_TIMER_14_BIT,
|
||||
.timer_num = LEDC_TIMER_0,
|
||||
.freq_hz = hz,
|
||||
.clk_cfg = LEDC_AUTO_CLK
|
||||
};
|
||||
ledc_channel_config_t ledc_channel = {
|
||||
.gpio_num = 1, // GPIO data pin 1
|
||||
.speed_mode = LEDC_LOW_SPEED_MODE,
|
||||
.channel = LEDC_CHANNEL_0,
|
||||
.timer_sel = LEDC_TIMER_0,
|
||||
.duty = 1 << (ledc_timer.duty_resolution - 1), // 512, // 50% duty cycle (1024 / 2 for 10-bit resolution)
|
||||
.hpoint = 0
|
||||
};
|
||||
|
||||
// Initialize the PWM
|
||||
ledc_timer_config(&ledc_timer);
|
||||
ledc_channel_config(&ledc_channel);
|
||||
// Start the PWM signal
|
||||
ledc_set_duty(ledc_channel.speed_mode, ledc_channel.channel, ledc_channel.duty); // 50% duty cycle
|
||||
ledc_update_duty(ledc_channel.speed_mode, ledc_channel.channel);
|
||||
ledc_inited = true;
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
// Initialize NVS
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
|
||||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
|
||||
/* Board-specific WiFi init (if any) */
|
||||
// Seeed XIAO ESP32C6: Configure GPIO 3 and GPIO 14 as outputs
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << 3) | (1ULL << 14),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE
|
||||
};
|
||||
gpio_config(&io_conf);
|
||||
|
||||
// Set GPIO 3 and GPIO 14 to low
|
||||
gpio_set_level(3, 0);
|
||||
gpio_set_level(14, 0);
|
||||
/* end board-specific WiFi init (if any) */
|
||||
|
||||
wifi_init_sta();
|
||||
|
||||
start_test_signal(100);
|
||||
|
||||
// Create RingBuffer (e.g. 8KB)
|
||||
s_ringbuf_handle = xRingbufferCreate(8 * 1024, RINGBUF_TYPE_BYTEBUF);
|
||||
if (s_ringbuf_handle == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create ring buffer");
|
||||
}
|
||||
|
||||
xTaskCreate(adc_read_task, "adc_read_task", 4 * 1024, NULL, 5, NULL);
|
||||
|
||||
// Wait for WiFi connection
|
||||
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
|
||||
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
|
||||
pdFALSE, pdFALSE, portMAX_DELAY);
|
||||
|
||||
if (bits & WIFI_CONNECTED_BIT) {
|
||||
ESP_LOGI(TAG, "connected to ap SSID:%s", ESP_WIFI_SSID);
|
||||
start_webserver();
|
||||
} else if (bits & WIFI_FAIL_BIT) {
|
||||
ESP_LOGI(TAG, "Failed to connect to SSID:%s", ESP_WIFI_SSID);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "UNEXPECTED EVENT");
|
||||
}
|
||||
}
|
||||
|
||||
static void event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data) {
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (event_base == WIFI_EVENT &&
|
||||
event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
if (s_retry_num < ESP_MAXIMUM_RETRY) {
|
||||
esp_wifi_connect();
|
||||
s_retry_num++;
|
||||
ESP_LOGI(TAG, "retry to connect to the AP");
|
||||
} else {
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
}
|
||||
ESP_LOGI(TAG, "connect to the AP fail");
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
|
||||
s_retry_num = 0;
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
static void wifi_init_sta(void) {
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
esp_netif_t* netif = esp_netif_create_default_wifi_sta();
|
||||
esp_netif_set_hostname(netif, "esp-scope"); // Set hostname for the STA interface
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
esp_event_handler_instance_t instance_any_id;
|
||||
esp_event_handler_instance_t instance_got_ip;
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(
|
||||
IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.sta =
|
||||
{
|
||||
.ssid = ESP_WIFI_SSID,
|
||||
.password = ESP_WIFI_PASS,
|
||||
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "wifi_init_sta finished.");
|
||||
}
|
||||
|
||||
/*
|
||||
* Web Server Configuration
|
||||
*/
|
||||
static httpd_handle_t s_server = NULL;
|
||||
|
||||
/*
|
||||
* WebSocket Handler
|
||||
*/
|
||||
static esp_err_t ws_handler(httpd_req_t *req) {
|
||||
if (req->method == HTTP_GET) {
|
||||
// Handshake
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
httpd_ws_frame_t ws_pkt;
|
||||
uint8_t *buf = NULL;
|
||||
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
|
||||
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
|
||||
|
||||
// Get frame len
|
||||
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
|
||||
if (ret != ESP_OK)
|
||||
return ret;
|
||||
|
||||
if (ws_pkt.len) {
|
||||
buf = calloc(1, ws_pkt.len + 1);
|
||||
if (buf == NULL)
|
||||
return ESP_ERR_NO_MEM;
|
||||
ws_pkt.payload = buf;
|
||||
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
|
||||
if (ret != ESP_OK) {
|
||||
free(buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Check for "hello"
|
||||
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT &&
|
||||
strcmp((char *)ws_pkt.payload, "hello") == 0) {
|
||||
ESP_LOGI(TAG, "New WS client connected, fd=%d", httpd_req_to_sockfd(req));
|
||||
s_ws_client_fd = httpd_req_to_sockfd(req);
|
||||
}
|
||||
free(buf);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Control Params Handler (POST /params)
|
||||
*/
|
||||
static esp_err_t params_handler(httpd_req_t *req) {
|
||||
char buf[256];
|
||||
int ret, remaining = req->content_len;
|
||||
if (remaining >= sizeof(buf)) {
|
||||
httpd_resp_send_500(req);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ret = httpd_req_recv(req, buf, remaining);
|
||||
if (ret <= 0)
|
||||
return ESP_FAIL;
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (root) {
|
||||
cJSON *sample_rate = cJSON_GetObjectItem(root, "sample_rate");
|
||||
if (sample_rate && s_sample_rate != sample_rate->valueint) {
|
||||
s_reconfig_needed = true;
|
||||
s_sample_rate = sample_rate->valueint;
|
||||
}
|
||||
cJSON *atten = cJSON_GetObjectItem(root, "atten");
|
||||
if (atten && s_atten != (adc_atten_t)atten->valueint) {
|
||||
s_reconfig_needed = true;
|
||||
s_atten = (adc_atten_t)atten->valueint;
|
||||
}
|
||||
cJSON *bit_width = cJSON_GetObjectItem(root, "bit_width");
|
||||
if (bit_width && s_bit_width != (adc_bitwidth_t)bit_width->valueint) {
|
||||
s_reconfig_needed = true;
|
||||
s_bit_width = (adc_bitwidth_t)bit_width->valueint;
|
||||
}
|
||||
cJSON *test_hz = cJSON_GetObjectItem(root, "test_hz");
|
||||
if (test_hz) {
|
||||
if (s_test_hz != (adc_bitwidth_t)test_hz->valueint) {
|
||||
s_test_hz = (adc_bitwidth_t)test_hz->valueint;
|
||||
start_test_signal(s_test_hz);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Config Request: Rate=%lu, Atten=%d, Width=%d, TestHz=%u, s_reconfig_needed=%d", s_sample_rate,
|
||||
s_atten, s_bit_width, s_test_hz,
|
||||
s_reconfig_needed);
|
||||
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
|
||||
httpd_resp_send(req, "OK", 2);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const httpd_uri_t uri_ws = {.uri = "/signal",
|
||||
.method = HTTP_GET,
|
||||
.handler = ws_handler,
|
||||
.user_ctx = NULL,
|
||||
.is_websocket = true};
|
||||
|
||||
static const httpd_uri_t uri_params = {.uri = "/params",
|
||||
.method = HTTP_POST,
|
||||
.handler = params_handler,
|
||||
.user_ctx = NULL};
|
||||
|
||||
/*
|
||||
* Task to pull from RingBuffer and send to WS
|
||||
*/
|
||||
static void ws_sender_task(void *arg) {
|
||||
size_t item_size;
|
||||
|
||||
while (1) {
|
||||
// Receive item from ring buffer
|
||||
// usage: void *xRingbufferReceive(RingbufHandle_t xRingbuffer, size_t
|
||||
// *pxItemSize, TickType_t xTicksToWait); But we sent `val` using
|
||||
// `xRingbufferSend`. RingBuffer bytebuf mode sends stream. We used
|
||||
// send/recv. Wait, we used `xRingbufferSend`. In bytebuf mode, receive
|
||||
// returns a pointer to the buffer.
|
||||
|
||||
uint16_t *data = (uint16_t *)xRingbufferReceive(
|
||||
s_ringbuf_handle, &item_size, pdMS_TO_TICKS(10));
|
||||
|
||||
if (data != NULL) {
|
||||
int num_samples = item_size / sizeof(uint16_t);
|
||||
if (s_ws_client_fd != -1) {
|
||||
// Create JSON object
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON *data_array = cJSON_CreateArray();
|
||||
for (int i = 0; i < num_samples; i++) {
|
||||
cJSON_AddItemToArray(data_array, cJSON_CreateNumber(data[i]));
|
||||
}
|
||||
cJSON_AddItemToObject(root, "data", data_array);
|
||||
|
||||
// Serialize JSON to string
|
||||
char *json_str = cJSON_PrintUnformatted(root);
|
||||
if (json_str) {
|
||||
httpd_ws_frame_t ws_frame = {
|
||||
.final = true,
|
||||
.fragmented = false,
|
||||
.type = HTTPD_WS_TYPE_TEXT,
|
||||
.payload = (uint8_t *)json_str,
|
||||
.len = strlen(json_str)};
|
||||
|
||||
// Send JSON frame
|
||||
esp_err_t ret = httpd_ws_send_frame_async(s_server, s_ws_client_fd, &ws_frame);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "WS Send failed, invalidating FD");
|
||||
s_ws_client_fd = -1;
|
||||
}
|
||||
|
||||
free(json_str);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
vRingbufferReturnItem(s_ringbuf_handle, (void *)data);
|
||||
} else {
|
||||
// No data
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Handler for serving index.html */
|
||||
static esp_err_t index_handler(httpd_req_t *req) {
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
httpd_resp_set_hdr(req, "Content-Type", "text/html; charset=utf-8");
|
||||
uint32_t len = index_html_end - index_html_start;
|
||||
// Workaround for some build systems adding null byte
|
||||
while (len && index_js_start[len-1] == 0) len--;
|
||||
httpd_resp_send(req, (const char *)index_html_start, len);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const httpd_uri_t uri_index = {
|
||||
.uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL};
|
||||
|
||||
/* Handler for serving index.js */
|
||||
static esp_err_t index_js_handler(httpd_req_t *req) {
|
||||
httpd_resp_set_type(req, "text/javascript");
|
||||
httpd_resp_set_hdr(req, "Content-Type", "text/javascript; charset=utf-8");
|
||||
uint32_t len = index_js_end - index_js_start;
|
||||
// Workaround for some build systems adding null byte
|
||||
while (len && index_js_start[len-1] == 0) len--;
|
||||
httpd_resp_send(req, (const char *)index_js_start, len);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const httpd_uri_t uri_index_js = {
|
||||
.uri = "/index.js", .method = HTTP_GET, .handler = index_js_handler, .user_ctx = NULL};
|
||||
|
||||
static void start_webserver(void) {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.lru_purge_enable = true;
|
||||
|
||||
ESP_LOGI(TAG, "Starting webserver on port: '%d'", config.server_port);
|
||||
if (httpd_start(&s_server, &config) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Registering URI handlers");
|
||||
httpd_register_uri_handler(s_server, &uri_index);
|
||||
httpd_register_uri_handler(s_server, &uri_index_js);
|
||||
httpd_register_uri_handler(s_server, &uri_ws);
|
||||
httpd_register_uri_handler(s_server, &uri_params);
|
||||
|
||||
// Start sender task
|
||||
xTaskCreate(ws_sender_task, "ws_sender", 4096, NULL, 5, NULL);
|
||||
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Error starting server!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user