/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
BLE connectivity adapted from the ESP32 BLE Server example by Random Nerd Tutorials:
https://randomnerdtutorials.com/esp32-bluetooth-low-energy-ble-arduino-ide/.
Copyright (c) 2025 Krishnanshu Mittal - krishnanshu@upsidedownlabs.tech
Copyright (c) 2025 Deepak Khatri - deepak@upsidedownlabs.tech
Copyright (c) 2025 Upside Down Labs - contact@upsidedownlabs.tech
At Upside Down Labs, we create open‐source DIY neuroscience hardware and software.
Our mission is to make neuroscience affordable and accessible for everyone.
By supporting us with your purchase, you help spread innovation and open science.
Thank you for being part of this journey with us!
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include "hal/efuse_hal.h"
#include "esp_gap_ble_api.h"
#include "esp_idf_version.h"
#include "esp_mac.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
// ADC includes
#include "esp_bt.h" // release Classic BT memory
#include "esp_adc/adc_continuous.h" // ADC continuous (DMA) driver
#include "hal/adc_types.h" // adc_atten_t, bit width, etc.
#include "soc/soc_caps.h" // SOC_ADC_DIGI_RESULT_BYTES
// ----- Chip-specific Pin Definitions -----
//
// Use the ESP-IDF config macros to detect the chip.
#if defined(CONFIG_IDF_TARGET_ESP32C6)
// Store chip revision number (for optional raw fixup if needed)
uint32_t chiprev = efuse_hal_chip_revision();
#define LED_BUILTIN 7
#define PIXEL_PIN 15
#define PIXEL_COUNT 6
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
#define LED_BUILTIN 6
#define PIXEL_PIN 3
#define PIXEL_COUNT 4
#else
#error "Unsupported board: Please target either ESP32-C6 or ESP32-C3 in your Board Manager."
#endif
#define NUM_CHANNELS_MAX 7 // Max channels supported (Beast Playmate)
#define BLE_PAYLOAD_BUFFERS 2 // Number of buffers for BLE payloads to prevent overflow (Change if you need more buffers)
#define PIXEL_BRIGHTNESS 7 // Brightness of Neopixel LED
#define BLOCK_COUNT 10 // Batch size: 10 samples per notification
#define SAMP_RATE 500.0 // Sampling rate per channel (500 Hz)
#define BATTERY_PIN A6 // Battery voltage pin
#define BOOT_MIN_BATTERY 10.0 // Minimum battery percentage to boot
#define STREAMING_MIN_BATTERY 5.0 // Minimum battery percentage to start streaming
// Global variables for Channel count and packet size
static uint8_t NUM_CHANNELS = 4; // Number of BioAmp channels + 1 channel for battery
static uint8_t SINGLE_SAMPLE_LEN = 0; // Each sample: (No. of bioAmp channels * 2 bytes) + 1 counter
static uint16_t NEW_PACKET_LEN = 0; // Packet length (BLOCK_COUNT * SINGLE_SAMPLE_LEN)
static bool isBeastPlaymate = false;
// Recompute packet sizes to adjust for channel count changes
static inline void recomputePacketSizes()
{
SINGLE_SAMPLE_LEN = (uint8_t)(2 * (NUM_CHANNELS - 1) + 1);
NEW_PACKET_LEN = (uint16_t)(BLOCK_COUNT * SINGLE_SAMPLE_LEN);
}
// Onboard Neopixel at PIXEL_PIN
Adafruit_NeoPixel pixels(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);
// Battery monitoring variables
static unsigned long lastBatteryCheck = 0;
static const unsigned long BATTERY_CHECK_INTERVAL = 10000; // Interval in milliseconds
static BLEServer *pBLEServer = nullptr; // Store server reference for disconnect
// LUT for 1S LiPo (Voltage in ascending order)
const float voltageLUT[] = {
3.27, 3.61, 3.69, 3.71, 3.73, 3.75, 3.77, 3.79, 3.80, 3.82,
3.84, 3.85, 3.87, 3.91, 3.95, 3.98, 4.02, 4.08, 4.11, 4.15, 4.20};
const int percentLUT[] = {
0, 5, 10, 15, 20, 25, 30, 35, 40, 45,
50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100};
const int lutSize = sizeof(voltageLUT) / sizeof(voltageLUT[0]);
// Linear interpolation function
float interpolatePercentage(float voltage)
{
// Handle out-of-range voltages
if (voltage <= voltageLUT[0])
return 0;
if (voltage >= voltageLUT[lutSize - 1])
return 100;
// Find the nearest LUT entries
int i = 0;
while (i < lutSize - 1 && voltage > voltageLUT[i + 1])
i++;
// Interpolate
float v1 = voltageLUT[i], v2 = voltageLUT[i + 1];
int p1 = percentLUT[i], p2 = percentLUT[i + 1];
return p1 + (voltage - v1) * (p2 - p1) / (v2 - v1);
}
// BLE UUIDs
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define DATA_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" // For ADC data (Notify only)
#define CONTROL_CHAR_UUID "0000ff01-0000-1000-8000-00805f9b34fb" // For commands (Read/Write/Notify)
#define BATTERY_CHAR_UUID "f633d0ec-46b4-43c1-a39f-1ca06d0602e1" // For battery status (Notify only)
// ----- Global Variables -----
uint8_t blePayload[BLE_PAYLOAD_BUFFERS][BLOCK_COUNT * (2 * (NUM_CHANNELS_MAX - 1) + 1)] = {0};
static uint8_t payload_wr = 0; // buffer currently being filled
static uint8_t payload_rd = 0; // next full buffer to notify
static uint8_t payload_full = 0; // number of full buffers ready
static uint8_t sampleIndex = 0; // How many samples accumulated in current batch
volatile bool streaming = false; // True when "START" command is received
uint8_t mac[6]; // Array to store 6-byte MAC address
// Flags to start/stop adc_continuous_mode
static volatile bool adc_start_requested = false;
static volatile bool adc_stop_requested = false;
BLECharacteristic *pDataCharacteristic;
BLECharacteristic *pControlCharacteristic;
BLECharacteristic *pBatteryCharacteristic;
// Global sample counter (each sample's packet counter)
uint8_t overallCounter = 0;
// Battery monitoring - stores latest ADC reading from A6
static volatile uint16_t latestBatteryRaw = 0;
// Battery averaging: log one battery ADC value per completed data packet
// Average all collected samples once per battery check interval
static uint32_t batteryWinStartMs = 0;
static uint32_t batteryWinSum = 0;
static uint16_t batteryWinCount = 0;
static uint16_t batteryAvgToSend = 0; // 0 when not ready yet
static uint16_t isCharging = 0; // 0 when not charging
static uint8_t lastBatteryPct = 255; // 255 is unset
static uint8_t consecutiveChargingCheck = 3;
static TaskHandle_t ledBlinkTask = NULL;
static volatile int ledBlinkCycles = -1; // -1=off, 0=indefinite, >0 = that many cycles
// Sample assembly state (reset on start/stop/disconnect)
static uint16_t last_vals[NUM_CHANNELS_MAX] = {0};
static uint32_t have_mask = 0;
static inline void resetSampleState()
{
have_mask = 0;
for (uint8_t i = 0; i < NUM_CHANNELS_MAX; i++)
{
last_vals[i] = 0;
}
}
// ----- ADC DMA (continuous mode) globals -----
static adc_continuous_handle_t adc_handle = nullptr;
static bool adc_started = false;
static SemaphoreHandle_t adc_data_semaphore = nullptr;
static esp_ble_adv_params_t advParams = {
.adv_int_min = 0x0128,
.adv_int_max = 0x0128,
.adv_type = ADV_TYPE_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY};
// Helper macros to parse DMA results as TYPE2 format on C3/C6
#define ADC_OUTPUT_TYPE ADC_DIGI_OUTPUT_FORMAT_TYPE2
#define ADC_GET_CHANNEL(p) ((p)->type2.channel)
#define ADC_GET_DATA(p) ((p)->type2.data)
// Forward declarations
static void adc_dma_init();
static void adc_dma_start();
static void adc_dma_stop();
static void handle_adc_dma_and_notify();
static inline uint16_t fix_raw_if_needed(uint16_t raw);
// ----- BLE Server Callbacks -----
class MyServerCallbacks : public BLEServerCallbacks
{
void onConnect(BLEServer *pServer) override
{
pixels.setPixelColor(0, pixels.Color(0, PIXEL_BRIGHTNESS, 0)); // Green
pixels.show();
digitalWrite(LED_BUILTIN, HIGH);
vTaskDelay(200 / portTICK_PERIOD_MS);
digitalWrite(LED_BUILTIN, LOW);
// Apply -3 dBm to the active connection
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_CONN_HDL0, ESP_PWR_LVL_N3);
esp_ble_gap_stop_advertising(); // Explicitly stop advertising
}
void onDisconnect(BLEServer *pServer) override
{
pixels.setPixelColor(0, pixels.Color(PIXEL_BRIGHTNESS, 0, 0)); // Red on disconnect
pixels.show();
// Vibrate twice on disconnect
digitalWrite(LED_BUILTIN, HIGH);
vTaskDelay(200 / portTICK_PERIOD_MS);
digitalWrite(LED_BUILTIN, LOW);
vTaskDelay(100 / portTICK_PERIOD_MS);
digitalWrite(LED_BUILTIN, HIGH);
vTaskDelay(200 / portTICK_PERIOD_MS);
digitalWrite(LED_BUILTIN, LOW);
streaming = false;
// Reset payload state and battery states on disconnect
sampleIndex = 0;
payload_wr = 0;
payload_rd = 0;
payload_full = 0;
resetSampleState();
lastBatteryPct = 255;
isCharging = 0;
ledBlinkCycles = -1;
adc_stop_requested = true; // Request stop
esp_ble_gap_start_advertising(&advParams);
}
};
// ----- BLE Control Characteristic Callback -----
// Handles incoming commands ("START", "STOP", "WHORU", "STATUS")
class ControlCallback : public BLECharacteristicCallbacks
{
void onWrite(BLECharacteristic *characteristic) override
{
String cmd = characteristic->getValue();
cmd.trim();
cmd.toUpperCase();
if (cmd == "START")
{
pixels.setPixelColor(0, pixels.Color(0, 0, PIXEL_BRIGHTNESS)); // Blue
pixels.show();
overallCounter = 0;
sampleIndex = 0;
payload_wr = 0;
payload_rd = 0;
payload_full = 0;
resetSampleState();
// Start battery averaging window on streaming start
batteryWinStartMs = millis();
batteryWinSum = 0;
batteryWinCount = 0;
batteryAvgToSend = 0;
isCharging = 0;
lastBatteryPct = 255;
ledBlinkCycles = -1;
streaming = true;
adc_start_requested = true; // Request start
}
else if (cmd == "STOP")
{
pixels.setPixelColor(0, pixels.Color(0, PIXEL_BRIGHTNESS, 0)); // Green
pixels.show();
streaming = false;
resetSampleState();
adc_stop_requested = true; // Request stop
}
else if (cmd == "WHORU")
{
characteristic->setValue("NPG-LITE");
characteristic->notify();
}
else if (cmd == "STATUS")
{
characteristic->setValue(streaming ? "RUNNING" : "STOPPED");
characteristic->notify();
}
else
{
characteristic->setValue("UNKNOWN COMMAND");
characteristic->notify();
}
}
};
void neoPixelTask(void *parameter)
{
while (true)
{
if (ledBlinkCycles == -1)
{
vTaskDelay(100 / portTICK_PERIOD_MS);
continue;
}
uint8_t cycles = 0;
uint8_t fader = 100;
bool decreasing = true;
// run indefinitely when ledBlinkCycles == 0, else run ledBlinkCycles times
while (ledBlinkCycles != -1 && (ledBlinkCycles == 0 || cycles < (uint8_t)ledBlinkCycles))
{
pixels.clear();
pixels.setPixelColor(PIXEL_COUNT - 1, pixels.Color(fader, 0, 0));
pixels.show();
vTaskDelay(20 / portTICK_PERIOD_MS);
if (decreasing)
{
fader = fader - 2;
if (fader < 10)
{
decreasing = false;
}
}
else
{
fader = fader + 2;
if (fader > 100)
{
decreasing = true;
cycles++;
}
}
}
// If it was a finite request, clear neopixel after completion
if (ledBlinkCycles > 0)
{
pixels.clear();
pixels.show();
ledBlinkCycles = -1; // stop repeating the 10-cycle blink forever
}
}
}
// -------Battery Functions-------
// Check battery status while streaming and notify every 10 seconds
void checkBatteryAndDisconnect()
{
if (batteryAvgToSend == 0)
return;
float voltage = (batteryAvgToSend / 1000.0) * 2; // for ESP32C6 v0.1
voltage = voltage - 0.02;
float percentage = ceil(interpolatePercentage(voltage));
// Send decreased battery percentage immediately
// Send increased battery percentage after 4 consecutive increases
uint8_t currentBatteryPct = (uint8_t)percentage;
if (lastBatteryPct == 255)
{
// First valid percentage
lastBatteryPct = currentBatteryPct;
isCharging = 0;
}
else if (currentBatteryPct <= lastBatteryPct)
{
lastBatteryPct = currentBatteryPct;
isCharging = 0;
}
else // currentBatteryPct > lastBatteryPct
{
if (isCharging >= consecutiveChargingCheck)
{
lastBatteryPct = currentBatteryPct;
}
isCharging++;
}
// Send battery percentage as single byte
uint8_t batteryByte = lastBatteryPct;
pBatteryCharacteristic->setValue(&batteryByte, 1);
pBatteryCharacteristic->notify();
if (percentage > 70.0)
{
ledBlinkCycles = -1;
pixels.setPixelColor(PIXEL_COUNT - 1, pixels.Color(0, PIXEL_BRIGHTNESS, 0)); // Green when above 70%
pixels.show();
}
else if (percentage <= 70.0 && percentage > 20.0)
{
ledBlinkCycles = -1;
pixels.setPixelColor(PIXEL_COUNT - 1, pixels.Color(15, 4, 0)); // Orange when between 20 and 70
pixels.show();
}
else if (percentage <= 20.0 && percentage > 10.0)
{
ledBlinkCycles = -1;
pixels.setPixelColor(PIXEL_COUNT - 1, pixels.Color(PIXEL_BRIGHTNESS, 0, 0)); // Red when below 20%
pixels.show();
}
else if (percentage <= 10.0 && percentage >= STREAMING_MIN_BATTERY)
{
ledBlinkCycles = 0; // blink until battery is in the range of 5-10%
}
else if (percentage < STREAMING_MIN_BATTERY)
{
// Stop streaming
streaming = false;
// Stop ADC since loop() won't run again before deep sleep
adc_dma_stop();
// Stop advertising to save power while blinking before deep sleep
esp_ble_gap_stop_advertising();
// Disconnect BLE client if connected
if (pBLEServer != nullptr && pBLEServer->getConnectedCount() > 0)
{
// Get connection ID from first connected client
std::map peerDevices = pBLEServer->getPeerDevices(false);
for (auto const &entry : peerDevices)
{
uint16_t connId = entry.first;
pBLEServer->disconnect(connId); // Use public disconnect method
}
}
sleepWhenLowBattery();
}
}
// Set device to deep sleep when battery is low
void sleepWhenLowBattery()
{
ledBlinkCycles = 10; // request 10 cycles (task runs the algo)
while (ledBlinkCycles != -1) // wait until task finishes
{
vTaskDelay(50 / portTICK_PERIOD_MS);
}
esp_deep_sleep_start(); // Enter deep sleep after blinking sequence
}
// Check battery on boot and go to deep sleep if battery < BOOT_MIN_BATTERY
void checkInitialBattery()
{
int count = 0;
float sum = 0.0;
unsigned long startMillis = millis();
while (millis() - startMillis < 100) // Collect battery voltage samples for 100ms
{
int analogValue = analogRead(BATTERY_PIN);
sum += analogValue;
count++;
}
// Avoid divide-by-zero
if (count == 0)
return;
float initialBatteryRaw = sum / count;
float voltage = (initialBatteryRaw / 1000.0) * 2; // for ESP32C6 v0.1
voltage = voltage - 0.02;
float initialBatteryPercentage = ceil(interpolatePercentage(voltage)); // Calculate battery percentage from LUT
// If battery is low, slowly blink the neopixel
if (initialBatteryPercentage < BOOT_MIN_BATTERY)
{
sleepWhenLowBattery();
}
}
// --------Check for Beast Playmate-------
void checkChannelCount()
{
isBeastPlaymate = false;
// Check for Beast Playmate (6 channels)
pinMode(A3, INPUT_PULLUP);
pinMode(A4, INPUT_PULLUP);
pinMode(A5, INPUT_PULLUP);
for (int i = 0; i < 10; i++)
{ // Collect samples for 10ms
if (digitalRead(A3) == LOW)
isBeastPlaymate = true;
if (digitalRead(A4) == LOW)
isBeastPlaymate = true;
if (digitalRead(A5) == LOW)
isBeastPlaymate = true;
vTaskDelay(1 / portTICK_PERIOD_MS);
}
// Restore high-impedance inputs before ADC use
pinMode(A3, INPUT);
pinMode(A4, INPUT);
pinMode(A5, INPUT);
// Configure active channels and packet sizes
if (isBeastPlaymate)
NUM_CHANNELS = 7;
else
NUM_CHANNELS = 4;
recomputePacketSizes();
}
void setup()
{
// ----- LEDs -----
pixels.begin();
xTaskCreatePinnedToCore(neoPixelTask, "NeoPixelTask", 2048, NULL, 1, &ledBlinkTask, 0);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
setCpuFrequencyMhz(80);
checkChannelCount(); // Check for Beast Playmate
checkInitialBattery(); // Check initial battery status
pixels.setPixelColor(0, pixels.Color(PIXEL_BRIGHTNESS, 0, 0)); // Red (power on)
pixels.show();
// Create binary semaphore for ADC data ready signaling
adc_data_semaphore = xSemaphoreCreateBinary();
if (adc_data_semaphore == nullptr)
{
while (1)
; // Halt
}
esp_read_mac(mac, ESP_MAC_EFUSE_FACTORY);
// ----- BLE-only memory footprint (free Classic BT) -----
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
// ----- Initialize BLE -----
char deviceName[36];
if (isBeastPlaymate)
sprintf(deviceName, "NPG-Lite-6CH:%02X:%02X", mac[4], mac[5]);
else
sprintf(deviceName, "NPG-Lite-3CH:%02X:%02X", mac[4], mac[5]);
BLEDevice::init(deviceName);
// Set BLE TX power to -3 dBm for default/advertising/scan
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_N3);
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_N3);
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_N3);
// larger MTU for efficiency (doesn't change packet format)
BLEDevice::setMTU(500);
pBLEServer = BLEDevice::createServer();
pBLEServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pBLEServer->createService(SERVICE_UUID);
// Data Characteristic (Notify only) for ADC data
pDataCharacteristic = pService->createCharacteristic(
DATA_CHAR_UUID,
BLECharacteristic::PROPERTY_NOTIFY);
pDataCharacteristic->addDescriptor(new BLE2902());
// Control Characteristic (Read/Write/Notify)
pControlCharacteristic = pService->createCharacteristic(
CONTROL_CHAR_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY);
pControlCharacteristic->setCallbacks(new ControlCallback());
// Battery Characteristic (Read/Notify)
pBatteryCharacteristic = pService->createCharacteristic(
BATTERY_CHAR_UUID,
BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
pBatteryCharacteristic->addDescriptor(new BLE2902());
pService->start();
// Configure advertising data to include device name
esp_ble_adv_data_t adv_data = {};
adv_data.set_scan_rsp = false;
adv_data.include_name = true;
adv_data.include_txpower = false;
adv_data.min_interval = 0x0006;
adv_data.max_interval = 0x0010;
adv_data.appearance = 0x00;
adv_data.manufacturer_len = 0;
adv_data.p_manufacturer_data = nullptr;
adv_data.service_data_len = 0;
adv_data.p_service_data = nullptr;
adv_data.service_uuid_len = 0;
adv_data.p_service_uuid = nullptr;
adv_data.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
esp_ble_gap_config_adv_data(&adv_data);
// Stop Arduino's advertising helper and start with our params
BLEDevice::getAdvertising()->stop(); // if it was started elsewhere
esp_ble_gap_start_advertising(&advParams);
}
void loop()
{
if (adc_stop_requested)
{
adc_dma_stop();
adc_stop_requested = false;
}
// Handle start/stop requests of adc_continuous_mode
if (adc_start_requested)
{
adc_dma_start();
adc_start_requested = false;
}
if (streaming)
{
// Battery check only when streaming (every battery check interval)
unsigned long currentMillis = millis();
if (currentMillis - lastBatteryCheck >= BATTERY_CHECK_INTERVAL)
{
lastBatteryCheck = currentMillis;
checkBatteryAndDisconnect();
}
// Block until semaphore is given by ISR
if (xSemaphoreTake(adc_data_semaphore, portMAX_DELAY) == pdTRUE)
{
handle_adc_dma_and_notify();
}
}
else
{
// Longer delay when idle
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// ----- ADC DMA implementation -----
// Maps physical ADC channel id → logical index 0..NUM_CHANNELS-1
static int8_t hw2idx[10];
static const uint8_t hw_chs_4[4] = {0, 1, 2, 6}; // Configuration for 3 BioAmp Channels
static const uint8_t hw_chs_7[7] = {0, 1, 2, 3, 4, 5, 6}; // Configuration for 6 BioAmp Channels
static void adc_dma_init()
{
static adc_digi_pattern_config_t pattern[NUM_CHANNELS_MAX];
const uint8_t *hw_chs = (NUM_CHANNELS == 7) ? hw_chs_7 : hw_chs_4; // Use appropriate channel configuration
// Build pattern from the single channel list
for (int i = 0; i < NUM_CHANNELS; i++)
{
pattern[i].atten = ADC_ATTEN_DB_11;
pattern[i].channel = hw_chs[i]; // Use physical channel id
pattern[i].unit = ADC_UNIT_1;
pattern[i].bit_width = ADC_BITWIDTH_12;
}
for (int i = 0; i < (int)sizeof(hw2idx); i++)
hw2idx[i] = -1;
for (int i = 0; i < NUM_CHANNELS; i++)
hw2idx[hw_chs[i]] = i;
// Create driver handle and configure continuous conversion
adc_continuous_handle_cfg_t handle_cfg = {
.max_store_buf_size = NUM_CHANNELS * SOC_ADC_DIGI_RESULT_BYTES * BLOCK_COUNT * 5,
.conv_frame_size = NUM_CHANNELS * SOC_ADC_DIGI_RESULT_BYTES * BLOCK_COUNT,
#if defined(ESP_IDF_VERSION) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0))
.flags = {.flush_pool = 1}, // Only for newer IDF that supports it
#endif
};
if (adc_handle == nullptr)
{
ESP_ERROR_CHECK(adc_continuous_new_handle(&handle_cfg, &adc_handle));
}
adc_continuous_evt_cbs_t cbs = {
.on_conv_done = [](adc_continuous_handle_t handle,
const adc_continuous_evt_data_t *edata,
void *user_data) -> bool
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(adc_data_semaphore, &xHigherPriorityTaskWoken);
return (xHigherPriorityTaskWoken == pdTRUE); // Yield if higher priority task woken
},
};
ESP_ERROR_CHECK(adc_continuous_register_event_callbacks(adc_handle, &cbs, nullptr));
adc_continuous_config_t dig_cfg = {
.pattern_num = NUM_CHANNELS,
.adc_pattern = pattern,
// total sample rate = per-channel * number of channels
.sample_freq_hz = (uint32_t)(SAMP_RATE * NUM_CHANNELS), // 2000 SPS total
.conv_mode = ADC_CONV_SINGLE_UNIT_1,
.format = ADC_OUTPUT_TYPE, // TYPE2 (unit, channel, data)
};
ESP_ERROR_CHECK(adc_continuous_config(adc_handle, &dig_cfg));
}
static void adc_dma_start()
{
// Reinitialize ADC if it was deinited
if (adc_handle == nullptr)
{
adc_dma_init();
}
if (adc_handle && !adc_started)
{
ESP_ERROR_CHECK(adc_continuous_start(adc_handle));
adc_started = true;
}
}
static void adc_dma_stop()
{
if (adc_handle && adc_started)
{
ESP_ERROR_CHECK(adc_continuous_stop(adc_handle));
adc_started = false;
// DEINITIALIZE ADC to save power
ESP_ERROR_CHECK(adc_continuous_deinit(adc_handle));
adc_handle = nullptr;
}
}
static inline uint16_t fix_raw_if_needed(uint16_t raw)
{
#if defined(CONFIG_IDF_TARGET_ESP32C6)
// Optional: match your prior scaling workaround for C6 rev1 if needed
if (chiprev == 1)
{
// scale raw (0..~3249) to 0..4095
uint32_t v = (uint32_t)raw * 4095u / 3249u;
if (v > 4095u)
v = 4095u;
return (uint16_t)v;
}
#endif
return raw;
}
static void handle_adc_dma_and_notify()
{
// Assemble Channel data into sample packets
// use global last_vals/have_mask
const uint32_t FULL_MASK = (1u << NUM_CHANNELS) - 1;
uint8_t dma_buf[NUM_CHANNELS_MAX * SOC_ADC_DIGI_RESULT_BYTES * BLOCK_COUNT];
// Drain ADC driver until empty OR until our payload buffers are full
while (payload_full < BLE_PAYLOAD_BUFFERS)
{
// Read whatever DMA has buffered
uint32_t ret_len = 0;
esp_err_t ret = adc_continuous_read(adc_handle, dma_buf, sizeof(dma_buf), &ret_len, 0);
if (ret != ESP_OK || ret_len == 0)
{
break;
}
for (uint32_t i = 0; i + SOC_ADC_DIGI_RESULT_BYTES <= ret_len; i += SOC_ADC_DIGI_RESULT_BYTES)
{
auto *p = (const adc_digi_output_data_t *)&dma_buf[i];
uint8_t ch_hw = ADC_GET_CHANNEL(p); // physical channel id from TYPE2
uint16_t raw = ADC_GET_DATA(p);
// map physical channel → logical index (0..NUM_CHANNELS-1)
int8_t idx = (ch_hw < (uint8_t)sizeof(hw2idx)) ? hw2idx[ch_hw] : -1;
if (idx >= 0 && idx < (int8_t)NUM_CHANNELS)
{
// Apply fix only to BioAmp channels (A0-A5), NOT battery (A6)
if (idx < (NUM_CHANNELS - 1))
{
last_vals[idx] = fix_raw_if_needed(raw);
}
else
{
last_vals[idx] = raw; // Battery channel: use raw value
}
have_mask |= (1u << idx);
// Track latest battery raw for reference/debug (averaging happens per completed packet)
if (idx == (NUM_CHANNELS - 1))
{
latestBatteryRaw = last_vals[idx];
}
}
// When we have all channels, emit one record into current payload buffer
if (have_mask == FULL_MASK)
{
// If all payload buffers are full, stop reading
if (payload_full >= BLE_PAYLOAD_BUFFERS)
{
have_mask = 0;
break;
}
// Calculate offset in blePayload buffer
uint16_t offset = sampleIndex * SINGLE_SAMPLE_LEN;
// Write counter directly to blePayload buffer
blePayload[payload_wr][offset] = overallCounter;
overallCounter = (overallCounter + 1) & 0xFF;
// Big-endian packing directly to blePayload buffer
for (uint8_t c = 0; c < NUM_CHANNELS - 1; c++)
{
uint16_t v = last_vals[c];
blePayload[payload_wr][offset + 1 + c * 2] = (uint8_t)((v >> 8) & 0xFF);
blePayload[payload_wr][offset + 1 + c * 2 + 1] = (uint8_t)(v & 0xFF);
}
sampleIndex++;
if (sampleIndex >= BLOCK_COUNT)
{
sampleIndex = 0;
// Log one battery ADC value per completed packet
if (NUM_CHANNELS > 0)
{
uint16_t batt = last_vals[NUM_CHANNELS - 1];
batteryWinSum += batt;
batteryWinCount++;
}
// Every BATTERY_CHECK_INTERVAL, compute window average and update latch (decreasing-only)
uint32_t nowMs = millis();
if ((uint32_t)(nowMs - batteryWinStartMs) >= (uint32_t)BATTERY_CHECK_INTERVAL)
{
if (batteryWinCount > 0)
{
uint16_t currentAvg = (uint16_t)(batteryWinSum / batteryWinCount);
batteryAvgToSend = currentAvg;
}
// Reset window
batteryWinStartMs = nowMs;
batteryWinSum = 0;
batteryWinCount = 0;
}
payload_full++;
payload_wr = (payload_wr + 1) % BLE_PAYLOAD_BUFFERS;
// If all payload buffers are full, stop reading
if (payload_full >= BLE_PAYLOAD_BUFFERS)
{
have_mask = 0;
break;
}
}
have_mask = 0; // reset for next triplet
}
}
}
while (payload_full > 0 && streaming)
{
pDataCharacteristic->setValue(blePayload[payload_rd], NEW_PACKET_LEN);
pDataCharacteristic->notify();
payload_rd = (uint8_t)((payload_rd + 1) % BLE_PAYLOAD_BUFFERS);
payload_full--;
}
}