commit f6553d8e41cf305974baaf4ba2ba884529081e6b Author: Matt Date: Fri Dec 12 21:55:13 2025 +0000 Inital commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25a0389 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b604c2a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(esp-scope) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..5cd0879 --- /dev/null +++ b/main/CMakeLists.txt @@ -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) + diff --git a/main/Kconfig b/main/Kconfig new file mode 100644 index 0000000..547b5c2 --- /dev/null +++ b/main/Kconfig @@ -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 diff --git a/main/idf_component.yml b/main/idf_component.yml new file mode 100644 index 0000000..ea2b306 --- /dev/null +++ b/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/cjson: "^1.7.0" diff --git a/main/index.html b/main/index.html new file mode 100644 index 0000000..c7ee53d --- /dev/null +++ b/main/index.html @@ -0,0 +1,177 @@ + + + + + + + espScope + + + + +
+
+
+ + +
+
+
Connecting...
+ +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+ +
+
+ + + + + diff --git a/main/index.js b/main/index.js new file mode 100644 index 0000000..63a5141 --- /dev/null +++ b/main/index.js @@ -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 = `
ΔVoltage: ${deltaVoltage}V
ΔTime: ${deltaTime}ms
`; + } 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(); + diff --git a/main/main.c b/main/main.c new file mode 100644 index 0000000..9118f13 --- /dev/null +++ b/main/main.c @@ -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 +#include +#include +#include +#include + +// 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!"); + } +}