From 819b011edce569090716fd76672f660e66223d74 Mon Sep 17 00:00:00 2001 From: vishnuEzenito <3.14zenito@gmail.com> Date: Thu, 10 Jul 2025 14:16:16 +0530 Subject: [PATCH] v2 update --- mark6/.DS_Store | Bin 0 -> 6148 bytes mark6/.theia/launch.json | 6 + mark6/ADS1299.cpp | 301 +++++++++++++++++++++++++++++ mark6/ADS1299.h | 92 +++++++++ mark6/config.h | 144 ++++++++++++++ mark6/host/eegstrm.py | 270 ++++++++++++++++++++++++++ mark6/mark6_v2.ino | 399 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 1212 insertions(+) create mode 100644 mark6/.DS_Store create mode 100644 mark6/.theia/launch.json create mode 100644 mark6/ADS1299.cpp create mode 100644 mark6/ADS1299.h create mode 100644 mark6/config.h create mode 100644 mark6/host/eegstrm.py create mode 100644 mark6/mark6_v2.ino diff --git a/mark6/.DS_Store b/mark6/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f69fa4a566e9cf4473738e009bd5bab736f6e2f5 GIT binary patch literal 6148 zcmeHKu};G<5Peq)YSg783sSzIQW=og(kcwh`2lDuN<~PjN@dQ@$MAhj@Xod>;?NbL z>MlAz`*P1#o-I2DAdCHK2222q=!%1c#V@As)q6Gwkqe@@KCW@gXNnTrUbY4PqXIg2 zW6Y7GzzjdR#+3J{rv$Hf#11dGVfKd1CK~%QVz6mu3YY?>z=1Mz}BPNI#{?X0CC8$GuHKIQ8{VAEMV)AZ)oDFL{F7)#Sl+t zJPmnSz}BOuL&D`l!pIVCDB?zE|EWudWFD + For ADS1299 + ESP32-C3 wireless EEG project + * @file ADS1299.cpp + * @author Markus Bäcker (markus.baecker@ovgu.de) + * @brief Class for interfacing ADS1299 + * @version 0.1 + * @date 2022-03-02 + * + * @copyright Copyright (c) 2022 + * + */ + +#include "ADS1299.h" + +/** + * @brief Construct a new ADS1299::ADS1299 object + * + */ +ADS1299::ADS1299() { + pinMode(SCK, OUTPUT); + pinMode(MOSI, OUTPUT); + pinMode(MISO, INPUT); +} + +/** + * @brief Sets up master ADS1299, configures SPI and ADS1299 for + * @details supply slave ADS1299 with clock and BIAS + * @param _DRDY DATA Ready pin goes LOW, when DATA is available + * @param _CS CHIP SELECT pin, goes LOW, when MCU communicates with ADS + */ +void ADS1299::setup(int _DRDY, int _CS) { + CS = _CS; + DRDY = _DRDY; + pinMode(CS, OUTPUT); + pinMode(PIN_RST, OUTPUT); + pinMode(DRDY, INPUT); + // set up slave select pins as outputs as the Arduino API + // doesn't handle automatically pulling SS low + + digitalWrite(SCK, LOW); + digitalWrite(MOSI, LOW); + //digitalWrite(SS, HIGH); + // SPI Setup + SPI.begin(SCK, MISO, MOSI, CS); // Initialize SPI library + SPI.setBitOrder(MSBFIRST); // Most significant Bit first + SPI.setFrequency(spiClk); // Sets SPI clock + SPI.setDataMode(SPI_MODE1); //// 1...2.4 MHz, clock polarity = 0; clock phase = 1 (pg. 8) + + delay(50); + // at startup all inputs must be low + digitalWrite(PIN_RST, LOW); + delayMicroseconds(4); // wait for oscillator startup 20us + digitalWrite(PIN_RST, HIGH); + delayMicroseconds(20); + + delay(40); // Recommended 18 Tclk before using device + RESET(); // DEVICE wakes up in RDATAC so no Registers could be written. + delay(10); // Recommended 18 Tclk before using device + + delayMicroseconds(20 * TCLK_cycle); // Recommended 18 Tclk before using device + SDATAC(); // DEVICE wakes up in RDATAC so no Registers could be written. + delayMicroseconds(20 * TCLK_cycle); // Recommended 18 Tclk before using device + // Setup Registers for master ADS + // CLOCK: CLKSEL pin 1 (through R1); COnf1 1111 0xxx + //WREG(CONFIG1, 0xF5); //F5 Output CLK signal for second ADS, 500 SPS + // BIAS + //WREG(MISC1, 0x20); // connect SRB1 to neg Electrodes + //WREG(CONFIG3, 0xEC); // b’x1xx 1100 Turn on BIAS amplifier, set internal BIASREF voltage + //WREG(BIAS_SENSN, 0x01); // CH1 - bias sensing-> all REFELEC + //WREG(BIAS_SENSP, 0x01); // CH1 + bias sensing + // Give slave time to react to external CLK + delay(1); + WREG(CONFIG1, 0x96); // 250sps + // Below rates are not usable with ESP32-C3 +// WREG(CONFIG1, 0x95); // 500sps + // WREG(CONFIG1, 0x94); // 1000sps + // WREG(CONFIG1, 0x93); // 2000sps + WREG(CONFIG2, 0xC0); + delay(1); + WREG(CONFIG3, 0xEC); + delay(1); + WREG(MISC1, 0x20); + delay(1); + // ADS setup as per EVM + delayMicroseconds(20 * TCLK_cycle); // Recommended 18 Tclk before using device + START(); +} + +// ADS1299 SPI Command Definitions (Datasheet, Pg. 35) +/*--------------------------------------------------*/ +/*---------------------- System Commands ---------------------*/ +/*--------------------------------------------------*/ + +/** + * @brief Wakes up the ADC + * + */ +void ADS1299::WAKEUP() { + digitalWrite(CS, LOW); + SPI.transfer(_WAKEUP); + digitalWrite(CS, HIGH); + delayMicroseconds(4 * TCLK_cycle); // wait 4 Tclk (Pg. 35) +} +/** + * @brief Put the ADC in Standby + * 5.1 mW instead of 22 mW in running mode + */ +void ADS1299::STANDBY() { + digitalWrite(CS, LOW); + SPI.transfer(_STANDBY); + digitalWrite(CS, HIGH); +} + +/** + * Reset all Registers + * sends command 0x06 + * @return nothing + */ +void ADS1299::RESET() { + digitalWrite(CS, LOW); + SPI.transfer(_RESET); + digitalWrite(CS, HIGH); + delayMicroseconds(18 * TCLK_cycle); // Recommended 18 Tclk before using device +} +/** + * @brief Send SPI command or pull START pin LOW, sync multiple ADS + * + */ +void ADS1299::START() { + digitalWrite(CS, LOW); + //SPI.transfer(_START); + SPI.transfer(_START); + delayMicroseconds(4 * TCLK_cycle); + digitalWrite(CS, HIGH); +} +/** + * @brief STOP data conversion, allows REGISTER reading/writing + * + */ +void ADS1299::STOP() { + digitalWrite(CS, LOW); + SPI.transfer(_STOP); + delayMicroseconds(4 * TCLK_cycle); // wait 4 clk cycles after this command (DS pg.36) + digitalWrite(CS, HIGH); +} +/** + * @brief Read one data chunk from ADS. Transmits 0001 0010 (12h) + */ +void ADS1299::RDATA() { + digitalWrite(CS, LOW); + SPI.transfer(_RDATA); + digitalWrite(CS, HIGH); +} + +/** + * @brief Read Data Continuously mode. Transmits 0001 0000 (10h) + * Reads data from ADC Channels continious + * @return nothing + */ +void ADS1299::RDATAC() { + digitalWrite(CS, LOW); + SPI.transfer(_RDATAC); + digitalWrite(CS, HIGH); +} + +/** + * @brief Stop Read Data Continuously mode. Transmits 0001 0001 (11h) + * @return nothing + */ +void ADS1299::SDATAC() { + digitalWrite(CS, LOW); + SPI.transfer(_SDATAC); + digitalWrite(CS, HIGH); + delayMicroseconds(4 * TCLK_cycle); // wait 4 clk cycles after this command (DS pg.36) +} + +/*--------------------------------------------------*/ +/*---------------------- Register Commands ---------------------*/ +/*--------------------------------------------------*/ + +/** + * @brief Read Register from ADS1299. + * It answers with the contained values, call SDATAC before reading/writing registers + * @param _address Single Register adress to read + */ +byte ADS1299::RREG(byte _address) { + SDATAC(); // RDATAC must be stopped before reading + byte opcode1 = _address + 0x20; // RREG expects 001rrrrr where rrrrr = _address + digitalWrite(CS, LOW); // open SPI + SPI.transfer(opcode1); // opcode1 + SPI.transfer(0x00); // opcode2 + regData[_address] = SPI.transfer(0x00); // update mirror location with returned byte + digitalWrite(CS, HIGH); // close SPI + RDATAC(); // Continue reading + return regData[_address]; // return requested register value +} + +/** + * @brief Write Settings as 1 Byte of Data to Registers + * + * @param _address of Regsiter + * @param _value of Register + */ +void ADS1299::WREG(byte _address, byte _value) { + digitalWrite(CS,LOW); + // write one Register + uint8_t opcode1 = _address + 0x40; + // Send WREG command & address + SPI.transfer(opcode1); + // Send number of registers to write + SPI.transfer(0x00); + // Write the value to the register + SPI.transfer(_value); + digitalWrite(CS, HIGH); + // a 4 t_CLK (~2 us) period must separate the end of one byte (or command) and the next +} // + + +/** + * @brief Ask ADS1299 for device ID; + * @details returns REV_ID[2:0] 1 DEV_ID[1:0] NU_CH[1:0] + * @result desired answer is 0b 0011 1110 + * @return byte ID value + */ +byte ADS1299::getDeviceID() { + digitalWrite(CS, LOW); // Low to communicated + + SPI.transfer(_SDATAC); // 0001 0001 SDATAC Stop Data continous + SPI.transfer(_RREG); // 0010 0000 RREG Read Register + SPI.transfer(0x00); // 0000 0000 Asking for 1 byte ID + byte data = SPI.transfer(0x00); // byte to read (hopefully 0b???1 1110) should be 3E or 62 + // Device ID you should get: REV_ID[2:0] 1 DEV_ID[1:0] NU_CH[1:0] + digitalWrite(CS, HIGH); // HIGH to stop communication + return data; +} + +/** + * @brief activate testsignal on passed channel; changes CONFIG1/2/3 ! + * + * @param _channeladdress + */ + +void ADS1299::activateTestSignals(byte _channeladdress) { + SDATAC(); // 0001 0001 Stop Data reading, to write new settings + WREG(CONFIG3, 0xEC); // internal Reference Voltage 1110 0000 no bias + //WREG(CONFIG1, 0xD6); // 0x96= 1001 0110 Daisy En 250 SPS | + WREG(CONFIG2, 0xD0); // Config2: 0100(x40)0010(x02)->x00-> 11010000(D0) internal test signal + // 0xD5 for faster higher test signal + WREG(_channeladdress, 0x05); // CHnSET: 0100 0101 Setting: 00000101 no PGA, just activate a (1 mV x V REF / 2.4) Square-Wave Test Signal + // RDATAC(); // read data continous +} + +/** + * @brief setup device for singleended measurement against SRB1 + * + */ + +void ADS1299::setSingleended(int configvalue) { + SDATAC(); + // GAIN 1, normal electrode input -> 0x00 + WREG(CH1SET, configvalue); // measures normal on CH1 + WREG(CH2SET, configvalue); // measures normal on CH1 + WREG(CH3SET, configvalue); // measures normal on CH1 + WREG(CH4SET, configvalue); // measures normal on CH1 + WREG(CH5SET, configvalue); // measures normal on CH1 + WREG(CH6SET, configvalue); // measures normal on CH1 + WREG(CH7SET, configvalue); // measures normal on CH1 + WREG(CH8SET, configvalue); + RDATAC(); + +} + +uint8_t ADS1299::readData(uint8_t *status, uint8_t *data) { + // portDISABLE_INTERRUPTS(); + // Get ADS status + // SDATAC(); + digitalWrite(CS, LOW); + delayMicroseconds(7 * TCLK_cycle); + + memset(status, 0, ADS_STATUS_SIZE); + SPI.transfer(status, ADS_STATUS_SIZE); + // res.status[0] = SPI.transfer(0x00); + // res.status[1] = SPI.transfer(0x00); + // res.status[2] = SPI.transfer(0x00); + + // Get ADS Data + memset(data, 0, ADS_DATA_SIZE); + SPI.transfer(data, ADS_DATA_SIZE); + // for (int i = 0; i < 7; i++) { + // int offset = i*3; + // res.data[offset + 0] = SPI.transfer(0x00); + // res.data[offset + 1] = SPI.transfer(0x00); + // res.data[offset + 2] = SPI.transfer(0x00); + // } + // portENABLE_INTERRUPTS(); + // RDATAC(); + digitalWrite(CS,HIGH); + return 0; +} diff --git a/mark6/ADS1299.h b/mark6/ADS1299.h new file mode 100644 index 0000000..2311c03 --- /dev/null +++ b/mark6/ADS1299.h @@ -0,0 +1,92 @@ +/** + * Updated by Deepak Khatri + * For ADS1299 + ESP32-C3 wireless EEG project + * @file ADS1299.hh + * @author Markus Bäcker (markus.baecker@ovgu.de) + * @brief Driver class for TI ADS1299 in combination with ESP32-S1 + * @version 0.1 + * @date 2022-02-21 + * + * @copyright Copyright (c) 2022 + * + */ +#ifndef ____ADS1299__ +#define ____ADS1299__ + +#include +#include +#include "config.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/timers.h" +#include "freertos/event_groups.h" + +struct results { + uint8_t status[ADS_STATUS_SIZE]; + uint8_t data[ADS_DATA_SIZE]; +}; + +class ADS1299 { +public: + //Attributes + int DRDY; + int CS; //pin numbers for "Data Ready" (DRDY) and "Chip Select" CS (Datasheet, pg. 26) + struct results res; // contains Data + + ADS1299(); + // SETUP for devices + void setup(int _DRDY, int _CS); + //ADS1299 SPI Command Definitions (Datasheet, Pg. 35) + //System Commands + void WAKEUP(); + void STANDBY(); + void RESET(); + void START(); + void STOP(); + + //Data Read Commands + void RDATAC(); // Reads data from ADC Channels continious + void SDATAC(); // Stops continous reading + void RDATA(); // Read one + + //Register Read/Write Commands + + byte getDeviceID(); + byte RREG(byte _address); + void printRegisterName(byte _address); + void WREG(byte _address, byte _value); // + void WREG(byte _address, byte _value, byte _numRegistersMinusOne); // + + + + + //SPI Library + byte transfer(byte _data); + + //------------------------// + void activateTestSignals(byte _channeladdress); + float convertHEXtoVolt(long hexdata); //convert Data Bytes to float Voltage values + float* updateData(); + uint8_t readData(uint8_t *status, uint8_t *data); + + void attachInterrupt(); + void detachInterrupt(); // Default + + + void calculateLSB(uint8_t gain, float vref); + void setSingleended(int configvalue = ADS1299_PGA_GAIN01); + + //------------------------// + void TI_setup(); // for Debugging purpose + void Task_data(void * argument); //const + TaskHandle_t task_handle; + +private: + + + static void Task_read_DRDY(){}; // Task called by RTOS + byte regData [24]; // array is used to mirror register data + +}; + +#endif diff --git a/mark6/config.h b/mark6/config.h new file mode 100644 index 0000000..426fe45 --- /dev/null +++ b/mark6/config.h @@ -0,0 +1,144 @@ +/** + Updated by Deepak Khatri + For ADS1299 + ESP32-C3 wireless EEG project + + @brief CONFIGs for ESP32 and ADS1299 interface + @file config.h + @author Markus Baecker + @version 11/2021 +*/ + +#ifndef _config_h +#define _config_h + +#define PIN_CS 7 // active low +#define PIN_RST 1 // active lOW +#define PIN_DRDY 0 // active lOW + +#define PIN_MISO 10 +#define PIN_MOSI 3 +#define PIN_SCLK 2 + +#define PIXEL_BRIGHTNESS 7 +#define TRIGGER_PIN 9 + +#define spiClk 4000000 //2400000 //valid: 2.4 Mhz; to 1 MHz +#define TCLK_cycle (1) //us for 1MHz SPIclk + +#define CHANNELS 8 +#define ADS_DATA_SIZE CHANNELS * 3 +#define ADS_STATUS_SIZE 3 +#define PACKET_SIZE 640 +#define BLOCK_SIZE 32 +#define TOTAL_BLOCKS PACKET_SIZE/BLOCK_SIZE + +#define SPI_DATA_MODE 0x04 //clock polarity = 0; clock phase = 1 (pg. 8) +#define SPI_MODE_MASK 0x0C // CPOL = bit 3, CPHA = bit 2 on SPCR +#define SPI_CLOCK_MASK 0x03 // SPR1 = bit 1, SPR0 = bit 0 on SPCR +#define SPI_2XCLOCK_MASK 0x01 // SPI2X = bit 0 on SPSR + +// ADS Registers +//SPI Command Definition Byte Assignments (Datasheet, pg. 35) +#define _WAKEUP 0x02 // Wake-up from standby mode +#define _STANDBY 0x04 // Enter Standby mode +#define _RESET 0x06 // Reset the device, +#define _START 0x08 // Start and restart (synchronize) conversions +#define _STOP 0x0A // Stop conversion +#define _RDATAC 0x10 // Enable Read Data Continuous mode (default mode at power-up) +#define _SDATAC 0x11 // Stop Read Data Continuous mode +#define _RDATA 0x12 // Read data by command; supports multiple read back + +#define _RREG 0x20 // (also = 00100000) is the first opcode that the address must be added to for RREG communication +#define _WREG 0x40 // 01000000 in binary (Datasheet, pg. 35) + +//Register Addresses +#define ID 0x00 +#define CONFIG1 0x01 +#define CONFIG2 0x02 +#define CONFIG3 0x03 +#define LOFF 0x04 +#define CH1SET 0x05 +#define CH2SET 0x06 +#define CH3SET 0x07 +#define CH4SET 0x08 +#define CH5SET 0x09 +#define CH6SET 0x0A +#define CH7SET 0x0B +#define CH8SET 0x0C +#define BIAS_SENSP 0x0D +#define BIAS_SENSN 0x0E +#define LOFF_SENSP 0x0F +#define LOFF_SENSN 0x10 +#define LOFF_FLIP 0x11 +#define LOFF_STATP 0x12 +#define LOFF_STATN 0x13 +#define GPIO 0x14 +#define MISC1 0x15 +#define MISC2 0x16 +#define CONFIG4 0x17 + +// SETTINGS for Registers +// ads1299 config 1 register bits +#define CFG1RES7 0x80 // 10000000 +#define DAISY_EN 0x40 // 01000000 +#define CLK_EN 0x20 // 00100000 +#define CFG1RES43 0x10 // 00010000 +#define SAMPLING_RATE_00250HZ 0x06 // 00000110 +#define SAMPLING_RATE_00500HZ 0x05 // 00000101 +#define SAMPLING_RATE_01000HZ 0x04 // 00000100 +#define SAMPLING_RATE_02000HZ 0x03 // 00000011 +#define SAMPLING_RATE_04000HZ 0x02 // 00000010 +#define SAMPLING_RATE_08000HZ 0x01 // 00000001 +#define SAMPLING_RATE_16000HZ 0x00 // 00000000 + +// ads1299 config 2 register bits +#define CFG2RES75 0xC0 // 11000000 +#define INT_CAL 0x10 // 00010000 +#define CFG2RES3 0x00 // 00000000 +#define CAL_AMP 0x04 // 00000100 +#define CAL_FREQ_221 0x00 // 00000000 +#define CAL_FREQ_220 0x01 // 00000001 +#define CAL_FREQ_DC 0x03 // 00000011 + +// ads1299 config 3 register bits +#define PD_REFBUF 0x80 // 10000000 +#define CFG3RES_65 0x42 // 01100000 +#define BIAS_MEAS 0x00 // 00010000 +#define BIASREF_INT 0x08 // 00001000 +#define PD_BIAS 0x04 // 00000100 +#define BIAS_LOFF_SENS 0x00 // 00000000 +#define BIAS_STAT 0x01 // 00000001 + +// ads1299 config 4 register bits +#define CFG4RES_74 0xF0 // 11110000 +#define SINGLE_SHOT 0x08 // 00010000 +#define CFG4RES_2 0x04 // 00000100 +#define PD_LOFF_COMP 0x02 // 00000010 +#define CFG4RES_0 0x00 // 00000001 + +// ads1299 individual channel settings bits +#define CHANNEL_POWER_DOWN 0x80 // 10000000 +#define SRB2 0x08 //00001000 + +#define ADS1299_INPUT_NORMAL 0x00 // 00000000 +#define ADS1299_INPUT_SHORTED 0x01 // 00000001 +#define ADS1299_INPUT_MEAS_BIAS 0x02 // 00000010 +#define ADS1299_INPUT_SUPPLY 0x03 // 00000011 +#define ADS1299_INPUT_TEMP 0x04 // 00000100 +#define ADS1299_INPUT_TESTSIGNAL 0x05 // 00000101 +#define ADS1299_INPUT_SET_BIASP 0x06 // 00000110 +#define ADS1299_INPUT_SET_BIASN 0x07 // 00000111 + +//Gain +#define ADS1299_PGA_GAIN01 0x00 // 00000000 +#define ADS1299_PGA_GAIN02 0x10 // 00010000 +#define ADS1299_PGA_GAIN04 0x20 // 00100000 +#define ADS1299_PGA_GAIN06 0x30 // 00110000 +#define ADS1299_PGA_GAIN08 0x40 // 01000000 +#define ADS1299_PGA_GAIN12 0x50 // 01010000 +#define ADS1299_PGA_GAIN24 0x60 // 01100000 + +#define GAIN 24 +#define SCALE_FACT 1000000 *((4.5/8388607)/GAIN) // Vref= 4.5V; 2^23-1 = 8388607 + +#endif \ No newline at end of file diff --git a/mark6/host/eegstrm.py b/mark6/host/eegstrm.py new file mode 100644 index 0000000..e6402a9 --- /dev/null +++ b/mark6/host/eegstrm.py @@ -0,0 +1,270 @@ +from pylsl import StreamInfo, StreamOutlet +import websocket +import socket +import time +import math +import statistics +from pynput import keyboard as pyn_keyboard +import csv +import struct +from websocket import ABNF + +stop_flag = False + +def on_press(key): + global stop_flag + try: + if key.char == 'q': + print("Shutdown requested (q pressed). Exiting gracefully.") + stop_flag = True + except AttributeError: + if key == pyn_keyboard.Key.esc: + print("Shutdown requested (ESC pressed). Exiting gracefully.") + stop_flag = True + +def calculate_rate(data_size, elapsed_time): + return data_size / elapsed_time if elapsed_time > 0 else 0 + +def analyze_timestamp_format(timestamp): + current_time = time.time() + if abs(timestamp - current_time) < 86400: + return "unix_seconds", timestamp + timestamp_ms = timestamp / 1000.0 + if abs(timestamp_ms - current_time) < 86400: + return "unix_milliseconds", timestamp_ms + timestamp_us = timestamp / 1000000.0 + if abs(timestamp_us - current_time) < 86400: + return "unix_microseconds", timestamp_us + if timestamp > 1000 and timestamp < 86400000: + return "relative_milliseconds", None + return "unknown", None + +def calculate_relative_latency(device_timestamps, receive_times): + if len(device_timestamps) < 2 or len(receive_times) < 2: + return None + device_interval = device_timestamps[-1] - device_timestamps[-2] # in milliseconds + receive_interval = (receive_times[-1] - receive_times[-2]) * 1000 # ms + return receive_interval - device_interval + +def calculate_latency(device_timestamp, receive_timestamp, timestamp_format="auto"): + if timestamp_format == "auto": + fmt, converted_timestamp = analyze_timestamp_format(device_timestamp) + if fmt == "unknown" or fmt == "relative_milliseconds": + return None, fmt + else: + return receive_timestamp - converted_timestamp, fmt + else: + if timestamp_format == "unix_seconds": + return receive_timestamp - device_timestamp, timestamp_format + elif timestamp_format == "unix_milliseconds": + return receive_timestamp - (device_timestamp / 1000.0), timestamp_format + elif timestamp_format == "unix_microseconds": + return receive_timestamp - (device_timestamp / 1000000.0), timestamp_format + else: + return None, "unsupported" + +def main(): + global stop_flag + + # LSL setup + stream_name = 'ORIC' + info = StreamInfo(stream_name, 'EEG', 8, 250, 'float32', 'uid007') + outlet = StreamOutlet(info) + print("Trying to connect") + ws = websocket.create_connection("ws://" + socket.gethostbyname("oric.local") + ":81") + print(f"{stream_name} LSL Stream started!") + + # —— SYNC handshake —— + # 1) Notify device to prepare for sync + ws.send("SYNC") + resp = ws.recv() + if resp != "SYNC_READY": + print("⚠️ Unexpected handshake response:", resp) + else: + # 2) Send current host UTC in ms (little-endian u64) + utc_ms = int(time.time() * 1000) + payload = struct.pack("= 2: + rel = calculate_relative_latency(device_timestamps, receive_times) + if rel is not None: + relative_latency_changes.append(rel) + if len(device_timestamps) > 100: + device_timestamps.pop(0) + receive_times.pop(0) + + latency, detected_format = calculate_latency(timestamp, packet_receive_time, timestamp_format) + if not format_detected and detected_format != "unknown": + timestamp_format = detected_format + format_detected = True + print(f"Detected timestamp format: {timestamp_format}") + + # sample ordering checks + duplicate = lost = order_error = 0 + if previousSampleNumber == -1: + previousSampleNumber = sample_number + previousTimeStamp = timestamp + else: + if sample_number - previousSampleNumber > 1: + lost = sample_number - previousSampleNumber - 1 + lost_samples += lost + elif sample_number == previousSampleNumber: + duplicate = 1 + duplicate_samples += 1 + elif sample_number - previousSampleNumber < 1: + order_error = 1 + out_of_order_samples += 1 + previousSampleNumber = sample_number + previousTimeStamp = timestamp + + # data quality & LSL push + if not (all(v == 0 for v in channel_data[:3]) and all(v > 0 for v in channel_data[4:])): + outlet.push_sample(channel_data) + + # write sample log + sample_writer.writerow([ + packet_number, sample_in_packet, global_sample_idx, + packet_receive_time, timestamp, sample_number, + (latency*1000) if latency is not None else None, + duplicate, lost, order_error, str(channel_data) + ]) + + if first_sample_in_packet: + packet_first_sample_latency = (latency*1000) if latency is not None else None + first_sample_in_packet = False + + # write packet log + packet_writer.writerow([ + packet_number, packet_receive_time, samples_in_this_packet, + packet_first_sample_latency, + interval_since_last_packet_ms, len(data), + timestamp_format + ]) + + # periodic stats + if elapsed_time >= 1.0: + sps = calculate_rate(sample_size, elapsed_time) + fps = calculate_rate(packet_size, elapsed_time) + bps = calculate_rate(data_size, elapsed_time) + avg_sample_lat = statistics.mean(latency_list) if latency_list else 0 + avg_packet_lat = statistics.mean(packet_latencies) if packet_latencies else 0 + avg_rel = statistics.mean(relative_latency_changes) if relative_latency_changes else 0 + jitter = statistics.stdev(packet_intervals)*1000 if len(packet_intervals)>1 else 0 + + if timestamp_format == "relative_milliseconds": + print(f"{math.ceil(fps)} FPS : {math.ceil(sps)} SPS : {math.ceil(bps)} BPS | " + f"Rel Latency Δ: {avg_rel:.2f} ms | Jitter: {jitter:.2f} ms | " + f"Lost: {lost_samples} | Dup: {duplicate_samples} | Format: {timestamp_format}") + else: + print(f"{math.ceil(fps)} FPS : {math.ceil(sps)} SPS : {math.ceil(bps)} BPS | " + f"Sample Lat: {avg_sample_lat*1000:.2f} ms | " + f"Packet Lat: {avg_packet_lat*1000:.2f} ms | " + f"Jitter: {jitter:.2f} ms | Lost: {lost_samples} | Dup: {duplicate_samples} | Format: {timestamp_format}") + + # reset stats counters + packet_size = sample_size = data_size = 0 + latency_list.clear() + packet_intervals.clear() + packet_latencies.clear() + relative_latency_changes.clear() + start_time = packet_receive_time + + except Exception as e: + print(f"Error: {e}") + + finally: + ws.close() + listener.stop() + packet_csvfile.close() + sample_csvfile.close() + print("Resources closed. Program ended.") + +if __name__ == "__main__": + main() diff --git a/mark6/mark6_v2.ino b/mark6/mark6_v2.ino new file mode 100644 index 0000000..083d070 --- /dev/null +++ b/mark6/mark6_v2.ino @@ -0,0 +1,399 @@ +/** + * @file main.cpp + * @brief Adds host-driven UTC time sync atop the original ADS1299 streaming code. + */ + +#include +#include +#include +#include +#include +#include "config.h" +#include "ADS1299.h" + +// 24-bit 4/6/8-channel Analog frontend +ADS1299 ADS; + +// WebSockets on port 81 +WebSocketsServer webSocket(81); + +// Onboard NeoPixel on pin 6 +Adafruit_NeoPixel pixels(1, 6, NEO_GRB + NEO_KHZ800); + +// WiFi AP credentials (fallback) +const char *ssid = "ORIC-EEG"; +const char *password = ""; + +// --- UTC Sync Globals --- +unsigned long long utc_offset_ms = 0; // Base UTC time in ms from host +bool time_synced = false; // Have we received a SYNC yet? +bool ADS_connected = false; // True if device detected + +unsigned long sync_micros = 0; // micros() when sync arrived + +// Returns current UTC timestamp (ms) as host-provided offset + elapsed +unsigned long getUTCTimestamp() { + if (!time_synced) return 0; + unsigned long elapsed_ms = (micros() - sync_micros) / 1000; + return utc_offset_ms + elapsed_ms; +} + +// Only send once both ADS and time are ready +bool isReadyToSendData() { + return time_synced && ADS_connected; +} + +// ESP memory & ADS error helpers +void ESPmemcheck(); +void ADSerrorcheck(); + +// Global variables +const char *hardware_type = ""; // can be 4/6/8 ch device type +int max_channels = 0; // can be 4/6/8 +bool data_ready = false; // Data ready pin (DRDY) state +static int dataQueueLen = 4000; // Queue length for ADC data +static QueueHandle_t dataQueue; // Queue +static TaskHandle_t ws_task = NULL; // WebSockets task +static TaskHandle_t ads_task = NULL; // Data acquisition task +static portMUX_TYPE spinlock = portMUX_INITIALIZER_UNLOCKED; +bool wm = false; // WiFiManager portal access + +// Data Buffers +uint8_t packetBytes[PACKET_SIZE]; // Single packet of length PACKET_SIZE in Bytes +uint8_t blockBytes[BLOCK_SIZE]; // Single block of BLOCK_SIZE (Default:32) in Bytes +uint8_t statusBytes[ADS_STATUS_SIZE]; // Status Bytes of size ADS_STATUS_SIZE + +// UTC timestamp (8 bytes for 64-bit ms precision) +#define UTC_TIMESTAMP_SIZE_IN_BYTES 8 +union { + char timestamp_bytes[UTC_TIMESTAMP_SIZE_IN_BYTES]; + unsigned long long timestamp; +} utc_timestamp_union; + +// sample number counter +#define SAMPLE_NUMBER_SIZE_IN_BYTES 4 +union { + char sample_number_bytes[SAMPLE_NUMBER_SIZE_IN_BYTES]; + unsigned long sample_number = 0; +} sample_number_union; + +// Calculate SPI data sizes +int num_spi_bytes = (3 * (max_channels + 1)); +int num_timestamped_spi_bytes = num_spi_bytes + UTC_TIMESTAMP_SIZE_IN_BYTES + SAMPLE_NUMBER_SIZE_IN_BYTES; + +// Data ready interrupt service routine +void IRAM_ATTR DRDY_ISR(void) { + portDISABLE_INTERRUPTS(); + BaseType_t task_woken = pdFALSE; + + vTaskNotifyGiveFromISR(ads_task, &task_woken); + + if (task_woken) { + portYIELD_FROM_ISR(); + } +} + +// Data acquisition and storing in queue +void ads_receiveQueue(void *parameter) { + while(1) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Only process if we have time sync + if (!time_synced) { + continue; + } + + portENTER_CRITICAL(&spinlock); + + // Get UTC timestamp (8 bytes, little-endian) + utc_timestamp_union.timestamp = getUTCTimestamp(); + + // Add UTC timestamp bytes to data (first 8 bytes) + for(int i = 0; i < UTC_TIMESTAMP_SIZE_IN_BYTES; i++) { + blockBytes[i] = utc_timestamp_union.timestamp_bytes[i]; + } + + // Add sample counter bytes to data (next 4 bytes) + for(int i = 0; i < SAMPLE_NUMBER_SIZE_IN_BYTES; i++) { + blockBytes[UTC_TIMESTAMP_SIZE_IN_BYTES + i] = sample_number_union.sample_number_bytes[i]; + } + + // ADS1299.cpp -> ADS1299::readData (after timestamp and sample number) + bool dataStat = ADS.readData(statusBytes, blockBytes + UTC_TIMESTAMP_SIZE_IN_BYTES + SAMPLE_NUMBER_SIZE_IN_BYTES); + + if(!dataStat){ + if(xQueueSend(dataQueue, (void*)&blockBytes, 0) != pdTRUE) { + Serial.println("Data Queue full"); + } + } else { + Serial.println("Data reading issue"); + } + sample_number_union.sample_number++; + portEXIT_CRITICAL(&spinlock); + } +} + +// Send data over websockets +void ws_sendQueue(void *parameter) { + while(1) { + // ALWAYS call webSocket.loop() first to process events (including sync messages) + webSocket.loop(); + + // Only send data when both ADS and time are synced + if (!isReadyToSendData()) { + vTaskDelay(10 / portTICK_PERIOD_MS); // Shorter delay for more responsive sync + continue; + } + + memset(packetBytes, 0, sizeof(packetBytes)); + for(int block=0; block= 4 && strncmp((char*)payload, "SYNC", 4) == 0) { + Serial.println("SYNC command received"); + webSocket.sendTXT(num, "SYNC_READY"); + } + // Log other text data + for (int i=0; i