BojanJurca / Esp32_oscilloscope

ESP32 oscilloscope - see the signals through Web browser the way ESP32 sees them
Creative Commons Zero v1.0 Universal
738 stars 84 forks source link

Consider using continuous conversions instead of `delayMicroseconds()` #21

Closed kubark42 closed 7 months ago

kubark42 commented 10 months ago

The use of continuous ADC conversions would allow for consistent measurement windows. This would reduce jitter, and also potentially allow for more datapoints to be measured within a single period. (A typical value for a DSO is 1000 points, vs. this project's 128 points.)

The architecture of this project is too complex for me to instantly see if this would work or not, but looking at oscilloscope.h it seems like a DMA transfer ought to be a compatible replacement for the delayMicroseconds() approach.

BojanJurca commented 10 months ago

Thank you for the tip. I was not aware of the Continuous Mode driver. I'll surely take a look at it when I find the time. Programming is just a hobby of mine, I'm doing it while my wife is solving crosswords ...

kubark42 commented 10 months ago

I'm very impressed for something which is only a hobby...

It looks like the ADC can do from 83kHz down to 611Hz. For samples rates slower than that, the results could be averaged together for noise reduction.

/*!< F_sample = F_digi_con / 2 / interval. F_digi_con = 5M for now. 30 <= interva <= 4095 */
#define SOC_ADC_SAMPLE_FREQ_THRES_HIGH          83333
#define SOC_ADC_SAMPLE_FREQ_THRES_LOW           611
kubark42 commented 10 months ago

Hmm, this is less interesting it looks like. I take it from https://github.com/espressif/esp-idf/blob/32472536715d674c160a2565795895e0273f8cde/components/esp_adc/adc_continuous.c#L431 that this is not a true hardware-driven continuous conversion, and is instead an RTOS thread. That means that the lack of Arduino support for this feature is a big deal.

https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/I2S/HiFreq_ADC/HiFreq_ADC.ino looks to be supported in Arduino, but it's only valid for ESP32, and not ESP32S2, ESP32S3, etc...

So it might be that higher rate sampling is a dead end for the time being. Maybe if Espressif extends their IDF to Arduino core, or if you want to explore I2S high-frequency sampling (it might have wifi issues?), this might be worth revisiting.

BojanJurca commented 10 months ago

I modified the code I found here: https://forum.arduino.cc/t/esp32-i2s-adc-read-two-inputs-possible/1032963/10

It seems it is possible to get 1 12 bit analog sample each 3 us. I haven't figured out everything yet, for example how to read from two GPIOs simultaneously or what would the correct value for I2S_SAMPLE_RATE be.

This is the code that seems to work:

#include <driver/i2s.h>

// I2S
#define I2S_SAMPLE_RATE (500) // (3000)
#define ADC_INPUT (ADC1_CHANNEL_4) //pin 32
#define I2S_DMA_BUF_LEN (1024) // (8)

// The 4 high bits are the channel, and the data is inverted
size_t bytes_read;
uint16_t buffer[I2S_DMA_BUF_LEN] = {0};

unsigned long lastTimePrinted;
unsigned long loopTime = 0;

void i2sInit() {
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
    .sample_rate =  I2S_SAMPLE_RATE,              // The format of the signal using ADC_BUILT_IN
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags = 0, // ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 2,
    .dma_buf_len = I2S_DMA_BUF_LEN,
    .use_apll = false,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
  };

  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_adc_mode(ADC_UNIT_1, ADC_INPUT);
  i2s_adc_enable(I2S_NUM_0);
  adc1_config_channel_atten(ADC_INPUT, ADC_ATTEN_DB_11);
}

void setup() {
  Serial.begin(115200);

  i2sInit();
}

void loop() {

  unsigned long startMicros = micros ();

  esp_err_t e = i2s_read(I2S_NUM_0, &buffer, sizeof(buffer), &bytes_read, portMAX_DELAY); /// 0);

  unsigned long stopMicros = micros ();

  loopTime = stopMicros - startMicros;

  if (millis() - lastTimePrinted >= 100) {
    Serial.printf ("------------------ loop time: %i, bytes_read: %i, e: %i\n", loopTime, bytes_read, e);
    Serial.printf ("sample 0 channel: %i, sample 0 value: %i\n", buffer[0] >> 12, buffer[0] & 0x0FFF);
    Serial.printf ("sample 1 channel: %i, sample 1 value: %i\n", buffer[1] >> 12, buffer[1] & 0x0FFF);
    // ...
    lastTimePrinted = millis();
  }

}
kubark42 commented 10 months ago

Interesting. Not that you should take me as a reference, but for my standard usage patterns, it's rare that I have two signals I need to measure simultaneously. The ESP32's ability to measure many different pins is far more valuable, since that means that I can wire up a small ESP 32 inside a robot and then forget about it until I am curious why something isn't working and want to debug. Having lots of high-speed pins is terribly useful for this.

BojanJurca commented 10 months ago

Hello. This is the most I can squeeze out of the current architecture, I guess. I fixed some of the bugs doing this and probably created some new ones :/ I still need to do some testing and improvements. updates.zip

BojanJurca commented 10 months ago

I made some fixes and I believe this is more or less it. You can have up to 744 samples per screen now (when using continuous 1 signal analog sampling), providing that at least 7 us pass between two samples (max sampling frequency that ESP32 can make is 150 kHz). With higher horizontal frequency than 200 Hz the actual number of samples per screen would get lower.

/*

    oscilloscope.h

    This file is part of Esp32_web_ftp_telnet_server_template project: https://github.com/BojanJurca/Esp32_web_ftp_telnet_server_template

    Issues:
            - when WiFi is in WIFI_AP or WIFI_STA_AP mode is oscillospe causes WDT problem when working at higher frequenceses

    September 9, 2023, Bojan Jurca

*/

    // ----- includes, definitions and supporting functions -----

    #include <WiFi.h>
    #include <soc/gpio_sig_map.h> // to digitalRead PWM and other GPIOs ...
    #include <soc/io_mux_reg.h>   // thanks to gin66: https://github.com/BojanJurca/Esp32_oscilloscope/issues/19    
    #include "driver/adc.h"       // to use adc1_get_raw instead of analogRead
    #include <driver/i2s.h>
    // #include <soc/syscon_reg.h>
    // fixed size strings    
    #include "fsString.h"

    // ----- TUNNING PARAMETERS -----

    #define OSCILLOSCOPE_CONTINUOUS_BUFFER_SIZE 746                   // max number of samples per screen, 746 samples * 2 bytes per sample = 1492 bytes, which must be <= HTTP_WS_FRAME_MAX_SIZE - 8
    #define OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE 373                      // max number of samples per screen, 373 samples * 4 bytes per sample = 1492 bytes, which must be <= HTTP_WS_FRAME_MAX_SIZE - 8
    #define OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE 248                     // max number of samples per screen, 248 samples * 6 bytes per sample = 1488 bytes, which must be <= HTTP_WS_FRAME_MAX_SIZE - 8

    // some ESP32 boards read analog values inverted, uncomment the following line to invert read values back again 
    // #define INVERT_ADC1_GET_RAW
    // some ESP32 boards read continuous analog values inverted, uncomment the following line to invert read values back again 
    // #define INVERT_I2S_READ

    // #define OSCILLOSCOPE_READER_CORE 1 // 1 or 0                   // #define OSCILLOSCOPE_READER_CORE if you want oscilloscope reader to run on specific core    
    #ifndef OSCILLOSCOPE_READER_PRIORITY
        #define OSCILLOSCOPE_READER_PRIORITY 1                        // normal priority if not define differently
    #endif

    // ----- CODE -----

    #include "httpServer.hpp"                 // oscilloscope uses websockets defined in webServer.hpp  

    // oscilloscope samples
    struct oscContinuousSample {                     // one sample
        int16_t signal1;                       // signal value of 1st GPIO read by analogRead or digialRead   
    }; // = 2 bytes per sample

    struct osc1SignalSample {                        // one sample
        int16_t signal1;                       // signal value of 1st GPIO read by analogRead or digialRead   
        int16_t deltaTime;                     // sample time - offset from previous sample in ms or us  
    }; // = 4 bytes per sample

    struct osc2SignalsSample {                        // one sample
        int16_t signal1;                       // signal value of 1st GPIO read by analogRead or digialRead   
        int16_t signal2;                       // signal value of 2nd GPIO if requested   
        int16_t deltaTime;                     // sample time - offset from previous sample in ms or us  
    }; // = 6 bytes per sample

    struct oscSamples {                               // buffer with samples
        union {
            oscContinuousSample samplesContinuousSignal [OSCILLOSCOPE_CONTINUOUS_BUFFER_SIZE];  
            osc1SignalSample    samples1Signal [OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE];  // samples
            osc2SignalsSample   samples2Signals [OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE];  // samples
        };
        unsigned int sampleCount;                      // number of samples in the buffer
        bool samplesAreReady;                          // is the buffer ready for sending
    };

    enum readerState { INITIAL = 0, START = 1, STARTED  = 2, STOP = 3, STOPPED = 4 };
    /* transitions:
          START   - set by osc main thread
          STARTED - set by oscReader
          STOP    - set by osc main thread
          STOPPED - set by oscReader
    */

    struct oscSharedMemory {         // data structure to be shared among oscilloscope tasks
      // basic data
      WebSocket *webSocket;                   // open webSocket for communication with javascript client
      bool clientIsBigEndian;                 // true if javascript client is big endian machine
      // sampling sharedMemory
      char readType [8];                      // analog or digital  
      bool analog;                            // true if readType is analog, false if digital (digitalRead)
      int gpio1;                              // gpio where ESP32 is taking samples from (digitalRead)
      adc1_channel_t adcchannel1;             // channel mapped from gpio ESP32 is taking samples from (adc1_get_raw instead of analogRead)
      adc1_channel_t adcchannel2;             // channel mapped from gpio ESP32 is taking samples from (adc1_get_raw instead of analogRead)
      int gpio2;                              // 2nd gpio if requested
      int samplingTime;                       // time between samples in ms or us
      char samplingTimeUnit [3];              // ms or us
      unsigned long screenWidthTime;          // oscilloscope screen width in ms or us
      char screenWidthTimeUnit [3];           // ms or us 
      bool positiveTrigger;                   // true if posotive slope trigger is set
      int positiveTriggerTreshold;            // positive slope trigger treshold value
      bool negativeTrigger;                   // true if negative slope trigger is set  
      int negativeTriggerTreshold;            // negative slope trigger treshold value
      // buffers holding samples 
      oscSamples readBuffer;                  // we'll read samples into this buffer
      oscSamples sendBuffer;                  // we'll copy red buffer into this buffer before sending samples to the client
      // reader state
      readerState oscReaderState;             // helps to execute a proper stopping sequence
    };

    // oscilloscope reader read samples to read-buffer of shared memory - it will be copied to send buffer when it is ready to be sent

    inline void delayMicrosecondsUntil (unsigned long *lastTime, unsigned long period) __attribute__((always_inline));
    void delayMicrosecondsUntil (unsigned long *lastTime, unsigned long period) {
        unsigned long nextTime = *lastTime + period;
        unsigned long waitTime = nextTime - micros ();

        while (waitTime > 1500 && waitTime < period) {
            vTaskDelay (pdMS_TO_TICKS (1)); 
            waitTime = nextTime - micros ();
        }

        if (waitTime < period)
            delayMicroseconds (waitTime);

        *lastTime = micros ();
    }

    // oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders oscReaders 

    // The most general purpose and slow oscReader
    //  - it can read 1 or 2 digital signals
    //  - it can read 1 or 2 analog signals
    //  - it can work in 'sample at a time' more or 'screen at a time' mode
    // The only drawback is that samplingTime and screenWidthTime must be specified in milliseconds
    void oscReader_millis (void *sharedMemory) {
        bool doAnalogRead =                 !strcmp (((oscSharedMemory *) sharedMemory)->readType, "analog");
        // *not needed* bool unitIsMicroSeconds =           !strcmp (((oscSharedMemory *) sharedMemory)->samplingTimeUnit, "us");
        int samplingTime =                  ((oscSharedMemory *) sharedMemory)->samplingTime;
        bool positiveTrigger =              ((oscSharedMemory *) sharedMemory)->positiveTrigger;
        bool negativeTrigger =              ((oscSharedMemory *) sharedMemory)->negativeTrigger;
        unsigned char gpio1 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio1; // easier to check validity with unsigned char then with integer 
        unsigned char gpio2 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio2; // easier to check validity with unsigned char then with integer
        unsigned char noOfSignals = 1; if (gpio2 <= 39) noOfSignals = 2;  // monitor 1 or 2 signals
        adc1_channel_t adcchannel1 =        ((oscSharedMemory *) sharedMemory)->adcchannel1;
        adc1_channel_t adcchannel2 =        ((oscSharedMemory *) sharedMemory)->adcchannel2;
        int positiveTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->positiveTriggerTreshold;
        int negativeTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->negativeTriggerTreshold;
        unsigned long screenWidthTime =     ((oscSharedMemory *) sharedMemory)->screenWidthTime; 
        oscSamples *readBuffer =            &((oscSharedMemory *) sharedMemory)->readBuffer;
        oscSamples *sendBuffer =            &((oscSharedMemory *) sharedMemory)->sendBuffer;

        // Is samplingTime large enough to fill the whole screen? If not, make a correction.
        if (noOfSignals == 1) {
            if ((unsigned long) samplingTime * (OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE - 2) < screenWidthTime) {
                samplingTime = max ((int) (screenWidthTime / (OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE - 2)) + 1, 1); // + 1 just to be on the safe side due to integer calculation rounding
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_millis] 1 signal samplingTime was too short (regarding to buffer size) and is corrected to %i\n", samplingTime);
            }
        } else {
            if ((unsigned long) samplingTime * (OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE - 2) < screenWidthTime) {
                samplingTime = max ((int) (screenWidthTime / (OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE - 2)) + 1, 1); // + 1 just to be on the safe side due to integer calculation rounding
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_millis] 2 signals samplingTime was too short (regarding to buffer size) and is corrected to %i\n", samplingTime);
            }
        }
        // Is samplingTime is too long for 15 bits, make a correction. Max sample time can be 32767 (15 bits) but since in some case actual sample time can be much larger than required le's keep it below 5000.
        if (samplingTime > 5000) {
                samplingTime = 5000;
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_millis] samplingTime was too long (to fit in 15 bits in (almost?) all cases) and is corrected to %i\n", samplingTime);
        }        

        // Calculate screen refresh period. It sholud be arround 50 ms (sustainable screen refresh rate is arround 20 Hz) but it is better if it is a multiple value of screenWidthTime.
        unsigned long screenRefreshMilliseconds; // screen refresh period
        int noOfSamplesPerScreen = screenWidthTime / samplingTime; if (noOfSamplesPerScreen * samplingTime < screenWidthTime) noOfSamplesPerScreen ++;
        unsigned long correctedScreenWidthTime = noOfSamplesPerScreen * samplingTime;                         
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenWidthTime = %i   noOfSamplesPerScreen = %lu   correctedScreenWidthTime = %i\n", screenWidthTime, noOfSamplesPerScreen, correctedScreenWidthTime);
        screenRefreshMilliseconds = correctedScreenWidthTime >= 50000 ? correctedScreenWidthTime / 1000 : ((50500 / correctedScreenWidthTime) * correctedScreenWidthTime) / 1000;
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenRefreshMilliseconds = %lu ms (should be close to 50 ms) => screen refresh frequency = %f Hz (should be close to 20 Hz)\n", screenRefreshMilliseconds, 1000.0 / screenRefreshMilliseconds);

        // DEBUG: Serial.printf ("[oscilloscope][oscReader_millis] samplingTime = %i   screenWidthTime: %i\n", samplingTime, screenWidthTime);

        // determine mode of operation sample at a time or screen at a time - this only makes sense when screenWidthTime is measured in ms
        bool oneSampleAtATime = screenWidthTime > 1000;
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] oneSampleAtATime mode = %i   unit = %s   screenWidthTime = %i\n", oneSampleAtATime, unitIsMicroSeconds ? "us" : "ms", screenWidthTime);

        // mark sendBuffer as already beeing sent, meaning it is free now
        sendBuffer->samplesAreReady = true;

        // thanks to gin66 (https://github.com/BojanJurca/Esp32_oscilloscope/issues/19 we can also read GPIOs that were configured for OUTPUT or PWM
        if (gpio1 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio1]);
        if (gpio2 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio2]);

        // wait for the START signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != START) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STARTED; 

        // --- do the sampling, samplingTime and screenWidthTime are in us ---

        // triggered or untriggered mode of operation
        bool triggeredMode = positiveTrigger || negativeTrigger;

        TickType_t lastScreenRefreshTicks = xTaskGetTickCount ();               // for timing screen refresh intervals            

        while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { // sampling from the left of the screen - while not getting STOP signal

            int screenTime = 0;                                                 // in ms - how far we have already got from the left of the screen (we'll compare this value with screenWidthTime)
            unsigned long deltaTime = 0;                                        // in ms - delta from previous sample
            TickType_t lastSampleTicks = xTaskGetTickCount ();                  // for sample timing                
            TickType_t newSampleTicks = lastSampleTicks;

            // Insert first dummy sample to read-buffer this tells javascript client to start drawing from the left of the screen. Please note that it also tells javascript client how many signals are in each sample
            if (noOfSignals == 1) readBuffer->samples1Signal [0] = {-2, -2}; // no real data sample can look like this
            else                  readBuffer->samples2Signals [0] = {-3, -3, -3}; // no real data sample can look like this
            readBuffer->sampleCount = 1;
            // DEBUG: Serial.printf ("[oscilloscope][oscReader] first (dummy) sample inserted\n");

            if (triggeredMode) { // if no trigger is set then skip this (waiting) part and start sampling immediatelly

                // take the first sample
                union {
                    osc1SignalSample last1SignalSample;
                    osc2SignalsSample last2SignalsSample;
                };

                #ifdef INVERT_ADC1_GET_RAW
                    if (noOfSignals == 1) { if (doAnalogRead) last1SignalSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) 0}; } // gpio1 should always be valid PIN
                    else                  { if (doAnalogRead) last2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (~adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) 0}; } // gpio1 should always be valid PIN
                #else
                    if (noOfSignals == 1) { if (doAnalogRead) last1SignalSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) 0}; } // gpio1 should always be valid PIN
                    else                  { if (doAnalogRead) last2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) 0}; } // gpio1 should always be valid PIN
                #endif

                // wait for trigger condition
                while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { 
                    // DEBUG: Serial.printf ("[oscilloscope][oscReader] waiting for a trigger event, state = %i\n", ((oscSharedMemory *) sharedMemory)->oscReaderState);

                    // wait befor continuing to next sample and calculate delta offset for it
                    vTaskDelayUntil (&newSampleTicks, pdMS_TO_TICKS (samplingTime));
                    deltaTime = pdTICKS_TO_MS (newSampleTicks - lastSampleTicks); // in ms - this value will be used for the next sample offset
                    lastSampleTicks = newSampleTicks;

                    // take the second sample
                    union {
                        osc1SignalSample new1SignalSample;
                        osc2SignalsSample new2SignalsSample;
                    };

                    #ifdef INVERT_ADC1_GET_RAW
                        if (noOfSignals == 1) { if (doAnalogRead) new1SignalSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) 0}; } // gpio1 should always be valid PIN
                        else                  { if (doAnalogRead) new2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) ~(adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) 0}; } // gpio1 should always be valid PIN
                    #else
                        if (noOfSignals == 1) { if (doAnalogRead) new1SignalSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) 0}; } // gpio1 should always be valid PIN
                        else                  { if (doAnalogRead) new2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; else last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) 0}; } // gpio1 should always be valid PIN
                    #endif

                    // Compare both samples to check if the trigger condition has occured, only gpio1 is used to trigger the sampling. Please note that it doesn't matter wether we compare last1SignalSample or last2SignalsSample since they share the same space and signal1 is always on the same place.
                    if ((positiveTrigger && last2SignalsSample.signal1 < positiveTriggerTreshold && new2SignalsSample.signal1 >= positiveTriggerTreshold) || (negativeTrigger && last2SignalsSample.signal1 > negativeTriggerTreshold && new2SignalsSample.signal1 <= negativeTriggerTreshold)) { 
                        // trigger condition has occured, insert both samples into read buffer
                        if (noOfSignals == 1) { readBuffer->samples1Signal [1] = last1SignalSample; readBuffer->samples1Signal [2] = new1SignalSample; } // timeOffset (from left of the screen) = 0, this is the first sample after triggered
                        else                  { readBuffer->samples2Signals [1] = last2SignalsSample; readBuffer->samples2Signals [2] = new2SignalsSample; } // timeOffset (from left of the screen) = 0, this is the first sample after triggered
                        screenTime = deltaTime;     // start measuring screen time from new sample on
                        readBuffer->sampleCount = 3;

                        // correct screenTime
                        screenTime = deltaTime;

                        // wait befor continuing to next sample and calculate delta offset for it
                        vTaskDelayUntil (&newSampleTicks, pdMS_TO_TICKS (samplingTime));
                        deltaTime = pdTICKS_TO_MS (newSampleTicks - lastSampleTicks); // in ms - this value will be used for the next sample offset
                        lastSampleTicks = newSampleTicks;

                        break; // trigger event occured, stop waiting and proceed to sampling
                    } else {
                        // Just forget the first sample and continue waiting for trigger condition - copy just signal values and let the timing start from 0. Please note that it doesn't matter wether we move new1SignalSample or new2SignalsSample since they share the same space.
                        last2SignalsSample.signal1 = new2SignalsSample.signal1;
                        last2SignalsSample.signal2 = new2SignalsSample.signal2;
                    }
                } // while not triggered
            } // if in trigger mode

            // take (the rest of the) samples that fit on one screen
            while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { // while screenTime < screenWidthTime

                // if we already passed screenWidthMilliseconds then copy read buffer to send buffer so it can be sent to the javascript client
                if (screenTime >= screenWidthTime || (noOfSignals == 1 && readBuffer->sampleCount >= OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE) || (noOfSignals == 2 && readBuffer->sampleCount >= OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE)) { 
                    // DEBUG: Serial.printf ("[oscilloscope] end of packet sampling - full screen: %i >=? %i   samplingTime = %i    samples = %i\n", screenTime, screenWidthTime, samplingTime, readBuffer->sampleCount);
                    // copy read buffer to send buffer so that oscilloscope sender can send it to javascript client 

                    while (oneSampleAtATime && sendBuffer->samplesAreReady) vTaskDelay (pdMS_TO_TICKS (1)); // in oneSampleAtATime mode wait until previous frame is sent
                    if (!sendBuffer->samplesAreReady) 
                        *sendBuffer = *readBuffer; // this also copies 'ready' flag from read buffer which is 'true' - tell oscSender to send the packet, this would refresh client screen
                    // else send buffer with previous frame is still waiting to be sent, do nothing now, skip this frame

                    // break out of the loop and than start taking new samples
                    break; // get out of while loop to start sampling from the left of the screen again
                }

                // one sample at a time mode requires sending (copying) the readBuffer to the sendBuffer so it can be sent to the javascript client even before it gets full (of samples that fit to one screen)
                if (oneSampleAtATime && readBuffer->sampleCount) {
                    // DEBUG: Serial.printf ("[oscilloscope][oscReader] oneSampleAtATime mode, sampleCount = %i (ready to be sent)\n", readBuffer->sampleCount);
                    // copy read buffer to send buffer so that oscilloscope sender can send it to javascript client 
                    if (!sendBuffer->samplesAreReady) {
                        *sendBuffer = *readBuffer; // this also copies 'ready' flag from read buffer which is 'true' - tell oscSender to send the packet, this would refresh client screen
                        readBuffer->sampleCount = 0; // empty read buffer so we don't send the same data again later
                    }
                    // else send buffer with previous frame is still waiting to be sent, but the buffer is not full yet, so just continue sampling into the same frame
                }

                // take the next sample
                union {
                    osc1SignalSample new1SignalSample;
                    osc2SignalsSample new2SignalsSample;
                };

                #ifdef INVERT_ADC1_GET_RAW
                    if (noOfSignals == 1) { if (doAnalogRead) new1SignalSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) deltaTime}; else new1SignalSample = {(int16_t) digitalRead (gpio1), (int16_t) deltaTime}; 
                                            readBuffer->samples1Signal [readBuffer->sampleCount ++] = new1SignalSample;
                    } else                { if (doAnalogRead) new2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (~adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) deltaTime}; else new2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) deltaTime}; 
                                            readBuffer->samples2Signals [readBuffer->sampleCount ++] = new2SignalsSample;
                    }
                #else
                    if (noOfSignals == 1) { if (doAnalogRead) new1SignalSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) deltaTime}; else new1SignalSample = {(int16_t) digitalRead (gpio1), (int16_t) deltaTime}; 
                                            readBuffer->samples1Signal [readBuffer->sampleCount ++] = new1SignalSample;
                    } else                { if (doAnalogRead) new2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) deltaTime}; else new2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) deltaTime}; 
                                            readBuffer->samples2Signals [readBuffer->sampleCount ++] = new2SignalsSample;
                    }
                #endif

                screenTime += deltaTime;

                // wait befor continuing to next sample and calculate delta offset for it
                vTaskDelayUntil (&newSampleTicks, pdMS_TO_TICKS (samplingTime));
                deltaTime = pdTICKS_TO_MS (newSampleTicks - lastSampleTicks); // in ms - this value will be used for the next sample offset
                // DEBUG: Serial.printf ("[oscilloscope][oscReader] seamples in readBuffer: %i   screenTime = %lu   screenWidthTime: %i\n", readBuffer->sampleCount, screenTime, screenWidthTime);

                lastSampleTicks = newSampleTicks;
            } // while screenTime < screenWidthTime

            // wait before next screen refresh
            vTaskDelayUntil (&lastScreenRefreshTicks, pdMS_TO_TICKS (screenRefreshMilliseconds));

        } // while sampling

        // DEBUG: Serial.printf ("[oscilloscope][oscReader] stopping, state = %i\n", ((oscSharedMemory *) sharedMemory)->oscReaderState);

        // wait for the STOP signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 

        vTaskDelete (NULL);
    }

    // oscReader that takes digital samples (digitalRead) on time interval specified in microseconds
    //  - it can only read 1 or 2 digital signals
    //  - it can only work in 'screen at a time' mode (which is not a drawback for the sampling is measured in microseconds)
    void oscReader_digital (void *sharedMemory) {
        // *not needed* bool doAnalogRead =                 !strcmp (((oscSharedMemory *) sharedMemory)->readType, "analog");
        // *not needed* bool unitIsMicroSeconds =           !strcmp (((oscSharedMemory *) sharedMemory)->samplingTimeUnit, "us");
        int samplingTime =                  ((oscSharedMemory *) sharedMemory)->samplingTime;
        bool positiveTrigger =              ((oscSharedMemory *) sharedMemory)->positiveTrigger;
        bool negativeTrigger =              ((oscSharedMemory *) sharedMemory)->negativeTrigger;
        unsigned char gpio1 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio1; // easier to check validity with unsigned char then with integer 
        unsigned char gpio2 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio2; // easier to check validity with unsigned char then with integer
        unsigned char noOfSignals = 1; if (gpio2 <= 39) noOfSignals = 2;  // monitor 1 or 2 signals
        // *not needed* adc1_channel_t adcchannel1 =        ((oscSharedMemory *) sharedMemory)->adcchannel1;
        // *not needed* adc1_channel_t adcchannel2 =        ((oscSharedMemory *) sharedMemory)->adcchannel2;
        int positiveTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->positiveTriggerTreshold;
        int negativeTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->negativeTriggerTreshold;
        unsigned long screenWidthTime =     ((oscSharedMemory *) sharedMemory)->screenWidthTime; 
        oscSamples *readBuffer =            &((oscSharedMemory *) sharedMemory)->readBuffer;
        oscSamples *sendBuffer =            &((oscSharedMemory *) sharedMemory)->sendBuffer;

        // Is samplingTime large enough to fill the whole screen? If not, make a correction.
        if (noOfSignals == 1) {
            if ((unsigned long) samplingTime * (OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE - 2) < screenWidthTime) {
                samplingTime = max ((int) (screenWidthTime / (OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE - 2)) + 1, 1); // + 1 just to be on the safe side due to integer calculation rounding
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] 1 signal samplingTime was too short (regarding to buffer size) and is corrected to %i\n", samplingTime);
            }
        } else {
            if ((unsigned long) samplingTime * (OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE - 2) < screenWidthTime) {
                samplingTime = max ((int) (screenWidthTime / (OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE - 2)) + 1, 1); // + 1 just to be on the safe side due to integer calculation rounding
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] 2 signals samplingTime was too short (regarding to buffer size) and is corrected to %i\n", samplingTime);
            }
        }
        // Is samplingTime is too long for 15 bits, make a correction. Max sample time can be 32767 (15 bits) but since in some case actual sample time can be much larger than required le's keep it below 5000.
        if (samplingTime > 5000) {
                samplingTime = 5000;
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] samplingTime was too long (to fit in 15 bits in (almost?) all cases) and is corrected to %i\n", samplingTime);
        }  

        // Calculate screen refresh period. It sholud be arround 50 ms (sustainable screen refresh rate is arround 20 Hz) but it is better if it is a multiple value of screenWidthTime.
        unsigned long screenRefreshMilliseconds; // screen refresh period
        int noOfSamplesPerScreen = screenWidthTime / samplingTime; if (noOfSamplesPerScreen * samplingTime < screenWidthTime) noOfSamplesPerScreen ++;
        unsigned long correctedScreenWidthTime = noOfSamplesPerScreen * samplingTime;                         
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenWidthTime = %i   noOfSamplesPerScreen = %lu   correctedScreenWidthTime = %i\n", screenWidthTime, noOfSamplesPerScreen, correctedScreenWidthTime);
        screenRefreshMilliseconds = correctedScreenWidthTime >= 50000 ? correctedScreenWidthTime / 1000 : ((50500 / correctedScreenWidthTime) * correctedScreenWidthTime) / 1000;
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenRefreshMilliseconds = %lu ms (should be close to 50 ms) => screen refresh frequency = %f Hz (should be close to 20 Hz)\n", screenRefreshMilliseconds, 1000.0 / screenRefreshMilliseconds);

        // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] samplingTime = %i   screenWidthTime: %i\n", samplingTime, screenWidthTime);

        // mark sendBuffer as already beeing sent, meaning it is free now
        sendBuffer->samplesAreReady = true;

        // thanks to gin66 (https://github.com/BojanJurca/Esp32_oscilloscope/issues/19 we can also read GPIOs that were configured for OUTPUT or PWM
        if (gpio1 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio1]);
        if (gpio2 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio2]);

        // wait for the START signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != START) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STARTED; 

        // --- do the sampling, samplingTime and screenWidthTime are in us ---

        // triggered or untriggered mode of operation
        bool triggeredMode = positiveTrigger || negativeTrigger;

        TickType_t lastScreenRefreshTicks = xTaskGetTickCount ();               // for timing screen refresh intervals            

        while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { // sampling from the left of the screen - while not getting STOP signal

            unsigned long screenTime = 0;                                       // in us - how far we have already got from the left of the screen (we'll compare this value with screenWidthTime)
            unsigned long deltaTime = 0;                                        // in us - delta from previous sample
            unsigned long lastSampleMicroseconds = micros ();                   // for sample timing                
            unsigned long newSampleMicroseconds = lastSampleMicroseconds;

            // Insert first dummy sample to read-buffer this tells javascript client to start drawing from the left of the screen. Please note that it also tells javascript client how many signals are in each sample
            if (noOfSignals == 1) readBuffer->samples1Signal [0] = {-2, -2}; // no real data sample can look like this
            else                  readBuffer->samples2Signals [0] = {-3, -3, -3}; // no real data sample can look like this
            readBuffer->sampleCount = 1;
            // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] first (dummy) sample inserted\n");

            if (triggeredMode) { // if no trigger is set then skip this (waiting) part and start sampling immediatelly

                // take the first sample
                union {
                    osc1SignalSample last1SignalSample;
                    osc2SignalsSample last2SignalsSample;
                };

                if (noOfSignals == 1) { last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) 0}; } // gpio1 should always be valid PIN
                else                  { last2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) 0}; } // gpio1 should always be valid PIN

                // wait for trigger condition
                while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { 
                    // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] waiting for a trigger event, state = %i\n", ((oscSharedMemory *) sharedMemory)->oscReaderState);

                    // wait befor continuing to next sample and calculate delta offset for it
                    delayMicrosecondsUntil (&newSampleMicroseconds, samplingTime);
                    deltaTime = newSampleMicroseconds - lastSampleMicroseconds; 
                    lastSampleMicroseconds = newSampleMicroseconds;                        

                    // take the second sample
                    union {
                        osc1SignalSample new1SignalSample;
                        osc2SignalsSample new2SignalsSample;
                    };
                    if (noOfSignals == 1) { new2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) 0}; } // gpio1 should always be valid PIN
                    else                  { new2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) 0}; } // gpio1 should always be valid PIN

                    // Compare both samples to check if the trigger condition has occured, only gpio1 is used to trigger the sampling. Please note that it doesn't matter wether we compare last1SignalSample or last2SignalsSample since they share the same space and signal1 is always on the same place.
                    if ((positiveTrigger && last2SignalsSample.signal1 < positiveTriggerTreshold && new2SignalsSample.signal1 >= positiveTriggerTreshold) || (negativeTrigger && last2SignalsSample.signal1 > negativeTriggerTreshold && new2SignalsSample.signal1 <= negativeTriggerTreshold)) { 
                        // trigger condition has occured, insert both samples into read buffer
                        if (noOfSignals == 1) { readBuffer->samples1Signal [1] = last1SignalSample; readBuffer->samples1Signal [2] = new1SignalSample; } // timeOffset (from left of the screen) = 0, this is the first sample after triggered
                        else                  { readBuffer->samples2Signals [1] = last2SignalsSample; readBuffer->samples2Signals [2] = new2SignalsSample; } // timeOffset (from left of the screen) = 0, this is the first sample after triggered
                        screenTime = deltaTime;     // start measuring screen time from new sample on
                        readBuffer->sampleCount = 3;

                        // correct screenTime
                        screenTime = deltaTime;

                        // wait befor continuing to next sample and calculate delta offset for it
                        delayMicrosecondsUntil (&newSampleMicroseconds, samplingTime);
                        deltaTime = newSampleMicroseconds - lastSampleMicroseconds; 
                        lastSampleMicroseconds = newSampleMicroseconds;                            

                        break; // trigger event occured, stop waiting and proceed to sampling
                    } else {
                        // Just forget the first sample and continue waiting for trigger condition - copy just signal values and let the timing start from 0. Please note that it doesn't matter wether we move new1SignalSample or new2SignalsSample since they share the same space.
                        last2SignalsSample.signal1 = new2SignalsSample.signal1;
                        last2SignalsSample.signal2 = new2SignalsSample.signal2;
                    }
                } // while not triggered
            } // if in trigger mode

            // take (the rest of the) samples that fit on one screen
            while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { // while screenTime < screenWidthTime

                // if we already passed screenWidthMilliseconds then copy read buffer to send buffer so it can be sent to the javascript client
                if (screenTime >= screenWidthTime || (noOfSignals == 1 && readBuffer->sampleCount >= OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE) || (noOfSignals == 2 && readBuffer->sampleCount >= OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE)) { 
                    // DEBUG: Serial.printf ("[oscilloscope] end of packet sampling - full screen: %i >=? %i   samplingTime = %i    samples = %i\n", screenTime, screenWidthTime, samplingTime, readBuffer->sampleCount);
                    // copy read buffer to send buffer so that oscilloscope sender can send it to javascript client 

                    if (!sendBuffer->samplesAreReady) 
                        *sendBuffer = *readBuffer; // this also copies 'ready' flag from read buffer which is 'true' - tell oscSender to send the packet, this would refresh client screen
                    // else send buffer with previous frame is still waiting to be sent, do nothing now, skip this frame

                    // break out of the loop and than start taking new samples
                    break; // get out of while loop to start sampling from the left of the screen again
                }

                // take the next sample
                union {
                    osc1SignalSample new1SignalSample;
                    osc2SignalsSample new2SignalsSample;
                };

                if (noOfSignals == 1) { new1SignalSample = {(int16_t) digitalRead (gpio1), (int16_t) deltaTime}; 
                                        readBuffer->samples1Signal [readBuffer->sampleCount ++] = new1SignalSample;
                } else                { new2SignalsSample = {(int16_t) digitalRead (gpio1), (int16_t) digitalRead (gpio2), (int16_t) deltaTime}; 
                                        readBuffer->samples2Signals [readBuffer->sampleCount ++] = new2SignalsSample;
                }

                screenTime += deltaTime;

                // wait befor continuing to next sample and calculate delta offset for it
                delayMicrosecondsUntil (&newSampleMicroseconds, samplingTime);
                deltaTime = newSampleMicroseconds - lastSampleMicroseconds; 
                lastSampleMicroseconds = newSampleMicroseconds;

            } // while screenTime < screenWidthTime

            // wait before next screen refresh
            vTaskDelayUntil (&lastScreenRefreshTicks, pdMS_TO_TICKS (screenRefreshMilliseconds));

        } // while sampling

        // DEBUG: Serial.printf ("[oscilloscope][oscReader] stopping, state = %i\n", ((oscSharedMemory *) sharedMemory)->oscReaderState);

        // wait for the STOP signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 

        vTaskDelete (NULL);
    }

    // oscReader that takes analog samples (analogRead) on time interval specified in microseconds
    //  - it can only read 1 or 2 digital signals
    //  - it can only work in 'screen at a time' mode (which is not a drawback for the sampling is measured in microseconds)
    void oscReader_analog (void *sharedMemory) {
        // *not needed* bool doAnalogRead =                 !strcmp (((oscSharedMemory *) sharedMemory)->readType, "analog");
        // *not needed* bool unitIsMicroSeconds =           !strcmp (((oscSharedMemory *) sharedMemory)->samplingTimeUnit, "us");
        int samplingTime =                  ((oscSharedMemory *) sharedMemory)->samplingTime;
        bool positiveTrigger =              ((oscSharedMemory *) sharedMemory)->positiveTrigger;
        bool negativeTrigger =              ((oscSharedMemory *) sharedMemory)->negativeTrigger;
        unsigned char gpio1 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio1; // easier to check validity with unsigned char then with integer 
        unsigned char gpio2 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio2; // easier to check validity with unsigned char then with integer
        unsigned char noOfSignals = 1; if (gpio2 <= 39) noOfSignals = 2;  // monitor 1 or 2 signals
        adc1_channel_t adcchannel1 =        ((oscSharedMemory *) sharedMemory)->adcchannel1;
        adc1_channel_t adcchannel2 =        ((oscSharedMemory *) sharedMemory)->adcchannel2;
        int positiveTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->positiveTriggerTreshold;
        int negativeTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->negativeTriggerTreshold;
        unsigned long screenWidthTime =     ((oscSharedMemory *) sharedMemory)->screenWidthTime; 
        oscSamples *readBuffer =            &((oscSharedMemory *) sharedMemory)->readBuffer;
        oscSamples *sendBuffer =            &((oscSharedMemory *) sharedMemory)->sendBuffer;

        // Is samplingTime large enough to fill the whole screen? If not, make a correction.
        if (noOfSignals == 1) {
            if ((unsigned long) samplingTime * (OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE - 2) < screenWidthTime) {
                samplingTime = max ((int) (screenWidthTime / (OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE - 2)) + 1, 1); // + 1 just to be on the safe side due to integer calculation rounding
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog] 1 signal samplingTime was too short (regarding to buffer size) and is corrected to %i\n", samplingTime);
            }
        } else {
            if ((unsigned long) samplingTime * (OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE - 2) < screenWidthTime) {
                samplingTime = max ((int) (screenWidthTime / (OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE - 2)) + 1, 1); // + 1 just to be on the safe side due to integer calculation rounding
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog] 2 signals samplingTime was too short (regarding to buffer size) and is corrected to %i\n", samplingTime);
            }
        }
        // Is samplingTime is too long for 15 bits, make a correction. Max sample time can be 32767 (15 bits) but since in some case actual sample time can be much larger than required le's keep it below 5000.
        if (samplingTime > 5000) {
                samplingTime = 5000;
                // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog] samplingTime was too long (to fit in 15 bits in (almost?) all cases) and is corrected to %i\n", samplingTime);
        }  

        // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog] samplingTime = %i, screenWidthTime = %i\n", samplingTime, screenWidthTime);
        if (noOfSignals == 2 && screenWidthTime <= 200 || noOfSignals == 1 && screenWidthTime <= 100) {
            #ifdef __DMESG__
                dmesg ("[oscilloscope] the settings exceed oscilloscope capabilities.");
            #endif
            ((oscSharedMemory *) sharedMemory)->webSocket->sendString ("[oscilloscope] the settings exceed oscilloscope capabilities."); // send error to javascript client
            while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
            ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 
            vTaskDelete (NULL);
        }

        // Calculate screen refresh period. It sholud be arround 50 ms (sustainable screen refresh rate is arround 20 Hz) but it is better if it is a multiple value of screenWidthTime.
        unsigned long screenRefreshMilliseconds; // screen refresh period
        int noOfSamplesPerScreen = screenWidthTime / samplingTime; if (noOfSamplesPerScreen * samplingTime < screenWidthTime) noOfSamplesPerScreen ++;
        unsigned long correctedScreenWidthTime = noOfSamplesPerScreen * samplingTime;                         
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenWidthTime = %i   noOfSamplesPerScreen = %lu   correctedScreenWidthTime = %i\n", screenWidthTime, noOfSamplesPerScreen, correctedScreenWidthTime);
        screenRefreshMilliseconds = correctedScreenWidthTime >= 50000 ? correctedScreenWidthTime / 1000 : ((50500 / correctedScreenWidthTime) * correctedScreenWidthTime) / 1000;
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenRefreshMilliseconds = %lu ms (should be close to 50 ms) => screen refresh frequency = %f Hz (should be close to 20 Hz)\n", screenRefreshMilliseconds, 1000.0 / screenRefreshMilliseconds);

        // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog] samplingTime = %i   screenWidthTime: %i\n", samplingTime, screenWidthTime);

        // mark sendBuffer as already beeing sent, meaning it is free now
        sendBuffer->samplesAreReady = true;

        // thanks to gin66 (https://github.com/BojanJurca/Esp32_oscilloscope/issues/19 we can also read GPIOs that were configured for OUTPUT or PWM
        if (gpio1 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio1]);
        if (gpio2 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio2]);

        // wait for the START signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != START) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STARTED; 

        // --- do the sampling, samplingTime and screenWidthTime are in us ---

        // triggered or untriggered mode of operation
        bool triggeredMode = positiveTrigger || negativeTrigger;

        TickType_t lastScreenRefreshTicks = xTaskGetTickCount ();               // for timing screen refresh intervals            

        while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { // sampling from the left of the screen - while not getting STOP signal

            unsigned long screenTime = 0;                                       // in us - how far we have already got from the left of the screen (we'll compare this value with screenWidthTime)
            unsigned long deltaTime = 0;                                        // in us - delta from previous sample
            unsigned long lastSampleMicroseconds = micros ();                   // for sample timing                
            unsigned long newSampleMicroseconds = lastSampleMicroseconds;

            // Insert first dummy sample to read-buffer this tells javascript client to start drawing from the left of the screen. Please note that it also tells javascript client how many signals are in each sample
            if (noOfSignals == 1) readBuffer->samples1Signal [0] = {-2, -2}; // no real data sample can look like this
            else                  readBuffer->samples2Signals [0] = {-3, -3, -3}; // no real data sample can look like this
            readBuffer->sampleCount = 1;
            // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] first (dummy) sample inserted\n");

            if (triggeredMode) { // if no trigger is set then skip this (waiting) part and start sampling immediatelly

                // take the first sample
                union {
                    osc1SignalSample last1SignalSample;
                    osc2SignalsSample last2SignalsSample;
                };

                #ifdef INVERT_ADC1_GET_RAW
                    if (noOfSignals == 1) { last2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                    else                  { last2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (~adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                #else
                    if (noOfSignals == 1) { last2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                    else                  { last2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                #endif

                // wait for trigger condition
                while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { 
                    // DEBUG: Serial.printf ("[oscilloscope][oscReader_digital] waiting for a trigger event, state = %i\n", ((oscSharedMemory *) sharedMemory)->oscReaderState);

                    // wait befor continuing to next sample and calculate delta offset for it
                    delayMicrosecondsUntil (&newSampleMicroseconds, samplingTime);
                    deltaTime = newSampleMicroseconds - lastSampleMicroseconds; 
                    lastSampleMicroseconds = newSampleMicroseconds;                        

                    // take the second sample
                    union {
                        osc1SignalSample new1SignalSample;
                        osc2SignalsSample new2SignalsSample;
                    };

                    #ifdef INVERT_ADC1_GET_RAW
                        if (noOfSignals == 1) { new2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                        else                  { new2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (~adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                    #else
                        if (noOfSignals == 1) { new2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                        else                  { new2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) 0}; } // gpio1 should always be valid PIN
                    #endif

                    // Compare both samples to check if the trigger condition has occured, only gpio1 is used to trigger the sampling. Please note that it doesn't matter wether we compare last1SignalSample or last2SignalsSample since they share the same space and signal1 is always on the same place.
                    if ((positiveTrigger && last2SignalsSample.signal1 < positiveTriggerTreshold && new2SignalsSample.signal1 >= positiveTriggerTreshold) || (negativeTrigger && last2SignalsSample.signal1 > negativeTriggerTreshold && new2SignalsSample.signal1 <= negativeTriggerTreshold)) { 
                        // trigger condition has occured, insert both samples into read buffer
                        if (noOfSignals == 1) { readBuffer->samples1Signal [1] = last1SignalSample; readBuffer->samples1Signal [2] = new1SignalSample; } // timeOffset (from left of the screen) = 0, this is the first sample after triggered
                        else                  { readBuffer->samples2Signals [1] = last2SignalsSample; readBuffer->samples2Signals [2] = new2SignalsSample; } // timeOffset (from left of the screen) = 0, this is the first sample after triggered
                        screenTime = deltaTime;     // start measuring screen time from new sample on
                        readBuffer->sampleCount = 3;

                        // correct screenTime
                        screenTime = deltaTime;

                        // wait befor continuing to next sample and calculate delta offset for it
                        delayMicrosecondsUntil (&newSampleMicroseconds, samplingTime);
                        deltaTime = newSampleMicroseconds - lastSampleMicroseconds; 
                        lastSampleMicroseconds = newSampleMicroseconds;                            

                        break; // trigger event occured, stop waiting and proceed to sampling
                    } else {
                        // Just forget the first sample and continue waiting for trigger condition - copy just signal values and let the timing start from 0. Please note that it doesn't matter wether we move new1SignalSample or new2SignalsSample since they share the same space.
                        last2SignalsSample.signal1 = new2SignalsSample.signal1;
                        last2SignalsSample.signal2 = new2SignalsSample.signal2;
                    }
                } // while not triggered
            } // if in trigger mode

            // take (the rest of the) samples that fit on one screen
            while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { // while screenTime < screenWidthTime

                // if we already passed screenWidthMilliseconds then copy read buffer to send buffer so it can be sent to the javascript client
                if (screenTime >= screenWidthTime || (noOfSignals == 1 && readBuffer->sampleCount >= OSCILLOSCOPE_1SIGNAL_BUFFER_SIZE) || (noOfSignals == 2 && readBuffer->sampleCount >= OSCILLOSCOPE_2SIGNALS_BUFFER_SIZE)) { 
                    // DEBUG: Serial.printf ("[oscilloscope] end of packet sampling - full screen: %i >=? %i   samplingTime = %i    samples = %i\n", screenTime, screenWidthTime, samplingTime, readBuffer->sampleCount);
                    // copy read buffer to send buffer so that oscilloscope sender can send it to javascript client 

                    if (!sendBuffer->samplesAreReady) 
                        *sendBuffer = *readBuffer; // this also copies 'ready' flag from read buffer which is 'true' - tell oscSender to send the packet, this would refresh client screen
                    // else send buffer with previous frame is still waiting to be sent, do nothing now, skip this frame

                    // break out of the loop and than start taking new samples
                    break; // get out of while loop to start sampling from the left of the screen again
                }

                // take the next sample
                union {
                    osc1SignalSample new1SignalSample;
                    osc2SignalsSample new2SignalsSample;
                };

                #ifdef INVERT_ADC1_GET_RAW
                    if (noOfSignals == 1) { new1SignalSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) deltaTime}; 
                                            readBuffer->samples1Signal [readBuffer->sampleCount ++] = new1SignalSample;
                    } else                { new2SignalsSample = {(int16_t) (~adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (~adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) deltaTime}; 
                                            readBuffer->samples2Signals [readBuffer->sampleCount ++] = new2SignalsSample;
                    }
                #else
                    if (noOfSignals == 1) { new1SignalSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) deltaTime}; 
                                            readBuffer->samples1Signal [readBuffer->sampleCount ++] = new1SignalSample;
                    } else                { new2SignalsSample = {(int16_t) (adc1_get_raw (adcchannel1) & 0xFFF), (int16_t) (adc1_get_raw (adcchannel2) & 0xFFF), (int16_t) deltaTime}; 
                                            readBuffer->samples2Signals [readBuffer->sampleCount ++] = new2SignalsSample;
                    }
                #endif

                screenTime += deltaTime;

                // wait befor continuing to next sample and calculate delta offset for it
                delayMicrosecondsUntil (&newSampleMicroseconds, samplingTime);
                deltaTime = newSampleMicroseconds - lastSampleMicroseconds; 
                lastSampleMicroseconds = newSampleMicroseconds;

            } // while screenTime < screenWidthTime

            // wait before next screen refresh
            vTaskDelayUntil (&lastScreenRefreshTicks, pdMS_TO_TICKS (screenRefreshMilliseconds));

        } // while sampling

        // DEBUG: Serial.printf ("[oscilloscope][oscReader] stopping, state = %i\n", ((oscSharedMemory *) sharedMemory)->oscReaderState);

        // wait for the STOP signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 

        vTaskDelete (NULL);
    }

    void oscReader_analog_1_signal_continuous (void *sharedMemory) {
        // *not needed* bool doAnalogRead =                 !strcmp (((oscSharedMemory *) sharedMemory)->readType, "analog");
        // *not needed* bool unitIsMicroSeconds =           !strcmp (((oscSharedMemory *) sharedMemory)->samplingTimeUnit, "us");
        int samplingTime =                  ((oscSharedMemory *) sharedMemory)->samplingTime;
        bool positiveTrigger =              ((oscSharedMemory *) sharedMemory)->positiveTrigger;
        bool negativeTrigger =              ((oscSharedMemory *) sharedMemory)->negativeTrigger;
        unsigned char gpio1 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio1; // easier to check validity with unsigned char then with integer 
        // * not needed * unsigned char gpio2 =               (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio2; // easier to check validity with unsigned char then with integer
        // * not needed * unsigned char noOfSignals = 1; if (gpio2 <= 39) noOfSignals = 2;  // monitor 1 or 2 signals
        adc1_channel_t adcchannel1 =        ((oscSharedMemory *) sharedMemory)->adcchannel1;
        // * not needed * adc1_channel_t adcchannel2 =        ((oscSharedMemory *) sharedMemory)->adcchannel2;
        int positiveTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->positiveTriggerTreshold;
        int negativeTriggerTreshold =       ((oscSharedMemory *) sharedMemory)->negativeTriggerTreshold;
        unsigned long screenWidthTime =     ((oscSharedMemory *) sharedMemory)->screenWidthTime; 
        oscSamples *readBuffer =            &((oscSharedMemory *) sharedMemory)->readBuffer;
        oscSamples *sendBuffer =            &((oscSharedMemory *) sharedMemory)->sendBuffer;

        // calculate correct sampling time so that it will prefectly aligh with sampleRate (regarding integer calculation rounding) and that the sample buffer is large enough 
        unsigned long sampleRate = 1000000 / samplingTime; // samplingTime is in us
        int noOfSamplesToTakeFirstTime = sampleRate * screenWidthTime / 1000000; // screenWidthTime is in us
        while (samplingTime != 1000000 / sampleRate // integer clculation rounding missmatch
           || (unsigned long) samplingTime * (OSCILLOSCOPE_CONTINUOUS_BUFFER_SIZE - 2) < screenWidthTime // samples do not fill the screen
           || noOfSamplesToTakeFirstTime > (OSCILLOSCOPE_CONTINUOUS_BUFFER_SIZE - 2 - 1 - 1 - 8) // samples do not fit in the buffer ... - 1 due to i2s_read bug (we may do 2 readings so - 1 again) ... please read ahead, - 8 because i2s_read can not read less than 8 samples 
           || samplingTime < 4 // dummy values -2 and -3 are already taken for different types of buffer
           || samplingTime < 7) // max ESP32 sampling rate = 150 kHz (sampling time >= 6.6 us)
        {
            samplingTime ++;
            sampleRate = 1000000 / samplingTime;
            noOfSamplesToTakeFirstTime = sampleRate * screenWidthTime / 1000000 + 1; // + 1 sample more than distance between them
        }
        // there is a bug in i2s_read: it odd number of samples are to be read the last one is always 0, so make sure we have even number of samples
        if (noOfSamplesToTakeFirstTime % 2 != 0) noOfSamplesToTakeFirstTime ++;
        if (noOfSamplesToTakeFirstTime < 8) noOfSamplesToTakeFirstTime = 8;

        // Is samplingTime is too long for 15 bits, make a correction. Max sample time can be 32767 (15 bits) but since in some case actual sample time can be much larger than required le's keep it below 5000.
        if (samplingTime > 32767) {
            samplingTime = 32767;
            // DEBUG: Serial.printf ("[oscilloscope][oscReader_oscReader_analog_1_signal_continuous] sampling with these parameters will not be precise\n");
            #ifdef __DMESG__
                dmesg ("[oscilloscope][oscReader_oscReader_analog_1_signal_continuous] sampling with these parameters will not be precise");
            #endif
        }

        // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog_1_signal_continuous] samplingTime: %lu sampleRate: %lu noOfSamplesToTake: %lu screenWidthTime: %lu ... positiveTriggerTreshold = %i, negativeTriggerTreshold = %i\n", samplingTime, sampleRate, noOfSamplesToTakeFirstTime, screenWidthTime, positiveTriggerTreshold, negativeTriggerTreshold);

        // Calculate screen refresh period. It sholud be arround 50 ms (sustainable screen refresh rate is arround 20 Hz) but it is better if it is a multiple value of screenWidthTime.
        unsigned long screenRefreshMilliseconds; // screen refresh period
        int noOfSamplesPerScreen = screenWidthTime / samplingTime; if (noOfSamplesPerScreen * samplingTime < screenWidthTime) noOfSamplesPerScreen ++;
        unsigned long correctedScreenWidthTime = noOfSamplesPerScreen * samplingTime;                         
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenWidthTime = %i   noOfSamplesPerScreen = %lu   correctedScreenWidthTime = %i\n", screenWidthTime, noOfSamplesPerScreen, correctedScreenWidthTime);
        screenRefreshMilliseconds = correctedScreenWidthTime >= 50000 ? correctedScreenWidthTime / 1000 : ((50500 / correctedScreenWidthTime) * correctedScreenWidthTime) / 1000;
        // DEBUG: Serial.printf ("[oscilloscope][oscReader] screenRefreshMilliseconds = %lu ms (should be close to 50 ms) => screen refresh frequency = %f Hz (should be close to 20 Hz)\n", screenRefreshMilliseconds, 1000.0 / screenRefreshMilliseconds);

        // mark sendBuffer as already beeing sent, meaning it is free now
        sendBuffer->samplesAreReady = true;

        // thanks to gin66 (https://github.com/BojanJurca/Esp32_oscilloscope/issues/19 we can also read GPIOs that were configured for OUTPUT or PWM
        if (gpio1 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio1]);
        // * not needed * if (gpio2 <= 39) PIN_INPUT_ENABLE (GPIO_PIN_MUX_REG [gpio2]);

        // wait for the START signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != START) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STARTED; 

        // --- do the sampling, samplingTime and screenWidthTime are in us ---

        // triggered or untriggered mode of operation
        bool triggeredMode = positiveTrigger || negativeTrigger;
        int noOfSamplesTaken;

        TickType_t lastScreenRefreshTicks = xTaskGetTickCount ();               // for timing screen refresh intervals            

        while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) { // sampling from the left of the screen - while not getting STOP signal

            // Insert first dummy sample to read-buffer this tells javascript client to start drawing from the left of the screen. Please note that it also tells javascript client how many signals are in each sample
            readBuffer->samplesContinuousSignal [0].signal1 = -samplingTime; // no real data sample can look like this
            // readBuffer->sampleCount = 1;
            // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog_1_signal] first (dummy) sample inserted\n");

            // take (the rest of the) samples that fit on one screen: https://www.instructables.com/The-Best-Way-for-Sampling-Audio-With-ESP32

            // setupI2S
            esp_err_t err;
            i2s_config_t i2s_config = { 
                .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
                .sample_rate = (uint32_t) 1000000 / samplingTime, // = samplingFrequency (samplingTime is in us),
                .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // could only get it to work with 32bits
                .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // <- mono signal - stereo signal -> I2S_CHANNEL_FMT_RIGHT_LEFT, // although the SEL config should be left, it seems to transmit on right
                .communication_format = i2s_comm_format_t (I2S_COMM_FORMAT_STAND_I2S), //// I2S_COMM_FORMAT_STAND_I2S, // I2S_COMM_FORMAT_I2S_MSB, - deprecated
                .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interrupt level 1
                .dma_buf_count = 4, // number of buffers
                .dma_buf_len = noOfSamplesToTakeFirstTime, // samples per buffer
                .use_apll = true // false//,
                // .tx_desc_auto_clear = false,
                // .fixed_  mclk = 1
            };

            err = i2s_driver_install (I2S_NUM_0, &i2s_config,  0, NULL);  //step 2
            if (err != ESP_OK) {
                Serial.printf ("Failed installing driver: %d\n", err);
                #ifdef __DMESG__
                    dmesg ("[oscilloscope][oscReader_oscReader_analog_1_signal_continuous] failed to install the driver: ", err);
                #endif

                ((oscSharedMemory *) sharedMemory)->webSocket->sendString ("[oscilloscope] failed to install the i2s driver."); // send error to javascript client
                // wait for the STOP signal
                while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
                ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 
                vTaskDelete (NULL);
            }

            // doesn't help:
            // The raw ADC data is written in DMA in inverted form. This add aninversion: https://www.esp32.com/viewtopic.php?t=12324
            // SET_PERI_REG_MASK(SYSCON_SARADC_CTRL2_REG, SYSCON_SARADC_SAR1_INV);            

            err = i2s_set_adc_mode (ADC_UNIT_1, adcchannel1);
            if (err != ESP_OK) {
                Serial.printf ("Failed setting up adc mode: %d\n", err);
                #ifdef __DMESG__
                    dmesg ("[oscilloscope][oscReader_oscReader_analog_1_signal_continuous] failed setting up adc mode: ", err);
                #endif
                i2s_driver_uninstall (I2S_NUM_0);

                ((oscSharedMemory *) sharedMemory)->webSocket->sendString ("[oscilloscope] failed setting up i2s adc mode."); // send error to javascript client
                // wait for the STOP signal
                while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
                ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 
                vTaskDelete (NULL);
            }

            while (((oscSharedMemory *) sharedMemory)->oscReaderState == STARTED) {

                // read the whole buffer
                size_t bytesRead = 0;
                err = i2s_read (I2S_NUM_0, 
                                (void *) &readBuffer->samplesContinuousSignal [1], // skip the first (dummy) sample
                                noOfSamplesToTakeFirstTime << 1, // in bytes
                                &bytesRead,
                                portMAX_DELAY); // no timeout
                if (err != ESP_OK) {
                    Serial.printf ("Failed reading samples: %d\n", err);
                    #ifdef __DMESG__
                        dmesg ("[oscilloscope][oscReader_oscReader_analog_1_signal_continuous] failed reading samples: ", err);
                    #endif
                    i2s_driver_uninstall (I2S_NUM_0);

                    ((oscSharedMemory *) sharedMemory)->webSocket->sendString ("[oscilloscope] failed setting up i2s adc mode."); // send error to javascript client
                    // wait for the STOP signal
                    while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
                    ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 
                    vTaskDelete (NULL);
                }
                // For some strange reason the sample come swapped two-by two. Unswap them and filter out only 12 bits that actually hold the value
                noOfSamplesTaken = bytesRead >> 1; // samples are 16 bit integers 
                // for (int i = 1; i <= noOfSamplesTaken; i++) readBuffer->samplesContinuousSignal [i].signal1 &= 0x0FFF;
                #ifdef INVERT_I2S_READ
                    for (int i = 1; i < noOfSamplesTaken; i += 2) {
                        int16_t tmp = readBuffer->samplesContinuousSignal [i].signal1;
                        readBuffer->samplesContinuousSignal [i].signal1 = ~readBuffer->samplesContinuousSignal [i + 1].signal1 & 0xFFF;
                        readBuffer->samplesContinuousSignal [i + 1].signal1 = ~tmp & 0xFFF;
                    }
                #else
                    for (int i = 1; i < noOfSamplesTaken; i += 2) {
                        int16_t tmp = readBuffer->samplesContinuousSignal [i].signal1;
                        readBuffer->samplesContinuousSignal [i].signal1 = readBuffer->samplesContinuousSignal [i + 1].signal1 & 0xFFF;
                        readBuffer->samplesContinuousSignal [i + 1].signal1 = tmp & 0xFFF;
                    }
                #endif

                // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog_1_signal_continuous] noOfSamplesTaken = %i\n", noOfSamplesTaken);
                // DEBUG: for (int i = 0; i <= noOfSamplesTaken; i++) Serial.printf (" %i ", readBuffer->samplesContinuousSignal [i].signal1); Serial.printf ("\n");

                if (!triggeredMode) break; // if not in triggered modt then we already have what we need

                // if in triggered mode try to find trigger condition
                for (int i = 1; i < noOfSamplesTaken; i++) {
                    if ( (positiveTrigger && readBuffer->samplesContinuousSignal [i].signal1 < positiveTriggerTreshold && readBuffer->samplesContinuousSignal [i + 1].signal1 >= positiveTriggerTreshold) || (negativeTrigger && readBuffer->samplesContinuousSignal [i].signal1 > negativeTriggerTreshold && readBuffer->samplesContinuousSignal [i + 1].signal1 <= negativeTriggerTreshold) ) { 
                        // trigger condition found at i, copy the rest of the buffer to its beginning and do another i2s_read for the samples that are missing
                        if (i > 1) {
                            int noOfSamplesToTakeSecondTime = (i - 1);
                            // there is a bug in i2s_read: it odd number of samples are to be read the last one is always 0, so make sure we have even number of samples
                            if (noOfSamplesToTakeSecondTime % 2 != 0) noOfSamplesToTakeSecondTime ++;
                            if (noOfSamplesToTakeSecondTime < 8) noOfSamplesToTakeSecondTime = 8;
                            int fromInd = noOfSamplesTaken - (i - 1) + 1;
                            // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog_1_signal_continuous] trigger condition found at: %i, need to read additional %i samples at buffer position %i\n", i, i - 1,  fromInd);
                            memcpy ((void*) &readBuffer->samplesContinuousSignal [1], (void*) &readBuffer->samplesContinuousSignal [i], (noOfSamplesTaken - (i - 1)) << 1);
                            err = i2s_read (I2S_NUM_0, 
                                            (void*) &readBuffer->samplesContinuousSignal [fromInd],
                                            noOfSamplesToTakeSecondTime << 1, // in bytes
                                            &bytesRead,
                                            portMAX_DELAY); // no timeout
                            if (err != ESP_OK) {
                                Serial.printf ("Failed reading samples: %d\n", err);
                                #ifdef __DMESG__
                                    dmesg ("[oscilloscope][oscReader_oscReader_analog_1_signal_continuous] failed reading samples: ", err);
                                #endif
                                i2s_driver_uninstall (I2S_NUM_0);

                                ((oscSharedMemory *) sharedMemory)->webSocket->sendString ("[oscilloscope] failed setting up i2s adc mode."); // send error to javascript client
                                // wait for the STOP signal
                                while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
                                ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 
                                vTaskDelete (NULL);
                            }
                            noOfSamplesTaken = noOfSamplesTaken - (i - 1) + (bytesRead >> 1); // - deleted samples + newly read samples (normally we would end up with the same number)
                            // for (int j = fromInd; j <= noOfSamplesTaken; j++) readBuffer->samplesContinuousSignal [j].signal1 &= 0x0FFF;
                            // For some strange reason the sample come swapped two-by two. Unswap them and filter out only 12 bits that actually hold the value
                            #ifdef INVERT_I2S_READ
                                for (int j = fromInd; j < noOfSamplesTaken; j += 2) {
                                    int16_t tmp = readBuffer->samplesContinuousSignal [j].signal1;
                                    readBuffer->samplesContinuousSignal [j].signal1 = ~readBuffer->samplesContinuousSignal [j + 1].signal1 & 0xFFF;
                                    readBuffer->samplesContinuousSignal [j + 1].signal1 = ~tmp & 0x0FFF;
                                }
                            #else
                                for (int j = fromInd; j < noOfSamplesTaken; j += 2) {
                                    int16_t tmp = readBuffer->samplesContinuousSignal [j].signal1;
                                    readBuffer->samplesContinuousSignal [j].signal1 = readBuffer->samplesContinuousSignal [j + 1].signal1 & 0xFFF;
                                    readBuffer->samplesContinuousSignal [j + 1].signal1 = tmp & 0x0FFF;
                                }
                            #endif
                        }

                        goto passSamplesToOscSender;
                    }
                }
                // trigger condition not found, continue reading
            } // while (true)

        passSamplesToOscSender:

            // pass readBuffer to oscSender
            readBuffer->sampleCount = noOfSamplesTaken + 1; // + 1 dummy sample
            // DEBUG: Serial.printf ("[oscilloscope][oscReader_analog_1_signal_continuous] noOfSamplesTaken =  %i\n", noOfSamplesTaken);
            if (!sendBuffer->samplesAreReady) 
                *sendBuffer = *readBuffer;

            // uninstall the driver
            i2s_driver_uninstall (I2S_NUM_0);

            // wait before next screen refresh
            vTaskDelayUntil (&lastScreenRefreshTicks, pdMS_TO_TICKS (screenRefreshMilliseconds));

        } // while sampling

        // DEBUG: Serial.printf ("[oscilloscope][oscReader] stopping, state = %i\n", ((oscSharedMemory *) sharedMemory)->oscReaderState);

        // wait for the STOP signal
        while (((oscSharedMemory *) sharedMemory)->oscReaderState != STOP) delay (1);
        ((oscSharedMemory *) sharedMemory)->oscReaderState = STOPPED; 

        vTaskDelete (NULL);
    }

    // oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender oscSender 

    void oscSender (void *sharedMemory) {
      unsigned char gpio1 =                   (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio1; // easier to check validity with unsigned char then with integer 
      unsigned char gpio2 =                   (unsigned char) ((oscSharedMemory *) sharedMemory)->gpio2; // easier to check validity with unsigned char then with integer
      unsigned char noOfSignals = 1; if (gpio2 <= 39) noOfSignals = 2;  // monitor 1 or 2 signals
      oscSamples *sendBuffer =                &((oscSharedMemory *) sharedMemory)->sendBuffer;
      sendBuffer->samplesAreReady = false;     
      bool clientIsBigEndian =                ((oscSharedMemory *) sharedMemory)->clientIsBigEndian;
      WebSocket *webSocket =                  ((oscSharedMemory *) sharedMemory)->webSocket; 

      unsigned long lastMillis = millis ();
      while (true) { 
        delay (1);
        // send samples to javascript client if they are ready
        if (sendBuffer->samplesAreReady && sendBuffer->sampleCount) {
          // DEBUG: Serial.printf ("[oscilloscope] screen refresh time (shold be close to 50): %lu ms\n", millis () - lastMillis); lastMillis = millis ();

          // copy buffer with samples within critical section
          oscSamples sendSamples = *sendBuffer;
          sendBuffer->samplesAreReady = false; // oscRader will set this flag when buffer is the next time ready for sending
          // swap bytes if javascript client is big endian
          int sendBytes; // calculate the number of bytes in the buffer

          // find out the type of buffer used
          if (noOfSignals == 1) 
              if (sendSamples.samplesContinuousSignal [0].signal1 < -3) 
                  sendBytes = sendSamples.sampleCount * sizeof (oscContinuousSample);  // 1 continuous signal (continuos signal does not starts with -1, -2 or -3 dummy value)
              else
                  sendBytes = sendSamples.sampleCount * sizeof (osc1SignalSample); // 1 signal with deltaTime
          else                  
              sendBytes = sendSamples.sampleCount * sizeof (osc2SignalsSample); // 2 signals with deltaTime
          int sendWords = sendBytes >> 1;                                 // number of 16 bit words = number of bytes / 2

          if (clientIsBigEndian) {
            uint16_t *w = (uint16_t *) &sendSamples;
            for (size_t i = 0; i < sendWords; i ++) w [i] = htons (w [i]);
          }
          // DEBUG: int16_t *wp = (int16_t *) &sendSamples; for (int i = 0; i < sendBytes / 2; i++) Serial.printf (" %i ", wp [i]); Serial.println ();
          if (!webSocket->sendBinary ((byte *) &sendSamples,  sendBytes)) return;
          // DEBUG: Serial.printf ("[oscilloscope] sent   samples: %i   bytes: %i\n", sendSamples.sampleCount, sendBytes);
        }

        // read (text) stop command form javscrip client if it arrives - according to oscilloscope protocol the string could only be 'stop' - so there is no need checking it
        if (webSocket->available () != WebSocket::NOT_AVAILABLE) return; // this also covers ERROR and TIME_OUT
      }
    }

    // main oscilloscope function - it reads request from javascript client then starts two threads: oscilloscope reader (that reads samples ans packs them into buffer) and oscilloscope sender (that sends buffer to javascript client)

    void runOscilloscope (WebSocket *webSocket) {

      // set up oscilloscope shared memory that will be shared among all 3 oscilloscope threads
      oscSharedMemory sharedMemory = {};                           // get some memory that will be shared among all oscilloscope threads and initialize it with zerros
      sharedMemory.webSocket = webSocket;                          // put webSocket rference into shared memory
      sharedMemory.readBuffer.samplesAreReady = true;              // this value will be copied into sendBuffer later where this flag will be checked

      // oscilloscope protocol starts with binary endian identification from the client
      uint16_t endianIdentification = 0;
      if (webSocket->readBinary ((byte *) &endianIdentification, sizeof (endianIdentification)) == sizeof (endianIdentification))
        sharedMemory.clientIsBigEndian = (endianIdentification == 0xBBAA); // cient has sent 0xAABB
      if (!(endianIdentification == 0xAABB || endianIdentification == 0xBBAA)) {
        #ifdef __DMESG__
          dmesg ("[oscilloscope] communication does not follow oscilloscope protocol. Expected endian identification.");
        #endif
        webSocket->sendString ("[oscilloscope] communication does not follow oscilloscope protocol. Expected endian identification."); // send error also to javascript client
        return;
      }

      // oscilloscope protocol continues with (text) start command in the following forms:
      // start digital sampling on GPIO 36 every 250 ms screen width = 10000 ms
      // start analog sampling on GPIO 22, 23 every 100 ms screen width = 400 ms set positive slope trigger to 512 set negative slope trigger to 0
      string s = webSocket->readString (); 
      // DEBUG: Serial.printf ("[oscilloscope] command: %s\n", s);

      if (s == "") {
        #ifdef __DMESG__
          dmesg ("[oscilloscope] communication does not follow oscilloscope protocol. Expected start oscilloscope parameters.");
        #endif
        webSocket->sendString ("[oscilloscope] communication does not follow oscilloscope protocol. Expected start oscilloscope parameters."); // send error also to javascript client
        return;
      }

      // try to parse what we have got from client
      char posNeg1 [9] = "";
      char posNeg2 [9] = "";
      int treshold1;
      int treshold2;
      char *cmdPart1 = (char *) s;
      char *cmdPart2 = strstr (cmdPart1, " every"); 
      char *cmdPart3 = NULL;
      if (cmdPart2) {
        *(cmdPart2++) = 0;
        cmdPart3 = strstr (cmdPart2, " set"); 
        if (cmdPart3) 
          *(cmdPart3++) = 0;
      }
      // parse 1st part
      sharedMemory.gpio1 = sharedMemory.gpio2 = 255; // invalid GPIO
      if (sscanf (cmdPart1, "start %7s sampling on GPIO %2i, %2i", sharedMemory.readType, &sharedMemory.gpio1, &sharedMemory.gpio2) < 2) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] oscilloscope protocol syntax error.");
        #endif
        webSocket->sendString ("[oscilloscope] oscilloscope protocol syntax error."); // send error also to javascript client
        return;
      }
      // use adc1_get_raw instead of analogRead
      if (!strcmp (sharedMemory.readType, "analog")) {
        switch (sharedMemory.gpio1) {
          // ADC1
          case 36: sharedMemory.adcchannel1 = ADC1_CHANNEL_0; break;
          case 37: sharedMemory.adcchannel1 = ADC1_CHANNEL_1; break;
          case 38: sharedMemory.adcchannel1 = ADC1_CHANNEL_2; break;
          case 39: sharedMemory.adcchannel1 = ADC1_CHANNEL_3; break;
          case 32: sharedMemory.adcchannel1 = ADC1_CHANNEL_4; break;
          case 33: sharedMemory.adcchannel1 = ADC1_CHANNEL_5; break;
          case 34: sharedMemory.adcchannel1 = ADC1_CHANNEL_6; break;
          case 35: sharedMemory.adcchannel1 = ADC1_CHANNEL_7; break;
          // ADC2 (GPIOs 4, 0, 2, 15, 13, 12, 14, 27, 25, 26), reading blocks when used together with WiFi 
          // other GPIOs do not have ADC
          default:  webSocket->sendString (string ("[oscilloscope] can't analogRead GPIO ") + string (sharedMemory.gpio1) + (char *) "."); // send error also to javascript client
                    return;  
        }
        switch (sharedMemory.gpio2) {
          // ADC1
          case 36: sharedMemory.adcchannel2 = ADC1_CHANNEL_0; break;
          case 37: sharedMemory.adcchannel2 = ADC1_CHANNEL_1; break;
          case 38: sharedMemory.adcchannel2 = ADC1_CHANNEL_2; break;
          case 39: sharedMemory.adcchannel2 = ADC1_CHANNEL_3; break;
          case 32: sharedMemory.adcchannel2 = ADC1_CHANNEL_4; break;
          case 33: sharedMemory.adcchannel2 = ADC1_CHANNEL_5; break;
          case 34: sharedMemory.adcchannel2 = ADC1_CHANNEL_6; break;
          case 35: sharedMemory.adcchannel2 = ADC1_CHANNEL_7; break;
          // not used
          case 255: break;
          // ADC2 (GPIOs 4, 0, 2, 15, 13, 12, 14, 27, 25, 26), reading blocks when used together with WiFi 
          // other GPIOs do not have ADC
          default:  webSocket->sendString (string ("[oscilloscope] can't analogRead GPIO ") + string (sharedMemory.gpio2) + (char *) "."); // send error also to javascript client
                    return;  
        }        
      }

      // parse 2nd part
      if (!cmdPart2) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] oscilloscope protocol syntax error.");
        #endif
        webSocket->sendString ("[oscilloscope] oscilloscope protocol syntax error."); // send error also to javascript client
        return;        
      }
      if (sscanf (cmdPart2, "every %i %2s screen width = %lu %2s", &sharedMemory.samplingTime, sharedMemory.samplingTimeUnit, &sharedMemory.screenWidthTime, sharedMemory.screenWidthTimeUnit) != 4) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] oscilloscope protocol syntax error.");
        #endif
        webSocket->sendString ("[oscilloscope] oscilloscope protocol syntax error."); // send error also to javascript client
        return;    
      }

      // parse 3rd part
      if (cmdPart3) { 
        switch (sscanf (cmdPart3, "set %8s slope trigger to %i set %8s slope trigger to %i", posNeg1, &treshold1, posNeg2, &treshold2)) {
          case 0: // no trigger
                  break;
          case 4: // two triggers
                  if (!strcmp (posNeg2, "positive")) {
                    sharedMemory.positiveTrigger = true;
                    sharedMemory.positiveTriggerTreshold = treshold2;
                  }
                  if (!strcmp (posNeg2, "negative")) {
                    sharedMemory.negativeTrigger = true;
                    sharedMemory.negativeTriggerTreshold = treshold2;
                  }    
                  // don't break, continue to the next case
          case 2: // one trigger
                  if (!strcmp (posNeg1, "positive")) {
                    sharedMemory.positiveTrigger = true;
                    sharedMemory.positiveTriggerTreshold = treshold1;
                  }
                  if (!strcmp (posNeg1, "negative")) {
                    sharedMemory.negativeTrigger = true;
                    sharedMemory.negativeTriggerTreshold = treshold1;
                  }
                  break;
          default:
                  #ifdef __DMESG__
                      dmesg ("[oscilloscope] oscilloscope protocol syntax error.");
                  #endif
                  webSocket->sendString ("[oscilloscope] oscilloscope protocol syntax error."); // send error also to javascript client
                  return;    
        }
      }

      // DEBUG: Serial.printf ("[oscilloscope] parsing command: samplingTime = %i %s, screenWidth = %lu %s\n", sharedMemory.samplingTime, sharedMemory.samplingTimeUnit, sharedMemory.screenWidthTime, sharedMemory.screenWidthTimeUnit);

      // check the values and calculate derived values
      if (!(!strcmp (sharedMemory.readType, "analog") || !strcmp (sharedMemory.readType, "digital"))) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] wrong readType. Read type can only be analog or digital.");
        #endif
        webSocket->sendString ("[oscilloscope] wrong readType. Read type can only be analog or digital."); // send error also to javascript client
        return;    
      }
      if (sharedMemory.gpio1 < 0 || sharedMemory.gpio2 < 0) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] invalid GPIO.");
        #endif
        webSocket->sendString ("[oscilloscope] invalid GPIO."); // send error also to javascript client
        return;      
      }
      if (!(sharedMemory.samplingTime >= 1 && sharedMemory.samplingTime <= 25000)) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] invalid sampling time. Sampling time must be between 1 and 25000.");
        #endif
        webSocket->sendString ("[oscilloscope] invalid sampling time. Sampling time must be between 1 and 25000."); // send error also to javascript client
        return;      
      }
      if (strcmp (sharedMemory.samplingTimeUnit, "ms") && strcmp (sharedMemory.samplingTimeUnit, "us")) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] wrong samplingTimeUnit. Sampling time unit can only be ms or us.");
        #endif
        webSocket->sendString ("[oscilloscope] wrong samplingTimeUnit. Sampling time unit can only be ms or us."); // send error also to javascript client
        return;    
      }

      if (strcmp (sharedMemory.screenWidthTimeUnit, sharedMemory.samplingTimeUnit)) {
        #ifdef __DMESG__
            dmesg ("[oscilloscope] screenWidthTimeUnit must be the same as samplingTimeUnit.");
        #endif        
        webSocket->sendString ("[oscilloscope] screenWidthTimeUnit must be the same as samplingTimeUnit."); // send error also to javascript client
        return;    
      }

      // DEBUG: Serial.printf ("[oscilloscope] parsing4 command: samplingTime = %i %s, screenWidth = %i %s\n", sharedMemory.samplingTime, sharedMemory.samplingTimeUnit, sharedMemory.screenWidthTime, sharedMemory.screenWidthTimeUnit);

      if (sharedMemory.positiveTrigger) {
        if (sharedMemory.positiveTriggerTreshold > 0 && sharedMemory.positiveTriggerTreshold <= (strcmp (sharedMemory.readType, "analog") ? 1 : 4095)) {
          ;// Serial.printf ("[oscilloscope] positive slope trigger treshold = %i\n", sharedMemory.positiveTriggerTreshold);
        } else {
          #ifdef __DMESG__
              dmesg ("[oscilloscope] invalid positive slope trigger treshold (according to other settings).");
          #endif
          webSocket->sendString ("[oscilloscope] invalid positive slope trigger treshold (according to other settings)."); // send error also to javascript client
          return;      
        }
      }
      if (sharedMemory.negativeTrigger) {
        if (sharedMemory.negativeTriggerTreshold >= 0 && sharedMemory.negativeTriggerTreshold < (strcmp (sharedMemory.readType, "analog") ? 1 : 4095)) {
          ;//Serial.printf ("[oscilloscope] negative slope trigger treshold = %i\n", sharedMemory.negativeTriggerTreshold);
        } else {
          #ifdef __DMESG__
              dmesg ("[oscilloscope] invalid negative slope trigger treshold (according to other settings).");
          #endif
          webSocket->sendString ("[oscilloscope] invalid negative slope trigger treshold (according to other settings)."); // send error also to javascript client
          return;      
        }
      }

      // DEBUG: Serial.printf ("[oscilloscope] parsing5 command: samplingTime = %i %s, screenWidth = %i %s\n", sharedMemory.samplingTime, sharedMemory.samplingTimeUnit, sharedMemory.screenWidthTime, sharedMemory.screenWidthTimeUnit);

      // choose the corect oscReader
      void (*oscReader) (void *sharedMemory);
      if (strcmp (sharedMemory.readType, "analog")) oscReader = oscReader_digital; // us sampling interval, 1-2 signals, digital reader
      else {
          if (sharedMemory.gpio2 <= 39) // 2 signalas
              oscReader = oscReader_analog; // us sampling interval, 1-2 signals, analog reader
          else // 1 signal
              oscReader = oscReader_analog_1_signal_continuous; // us sampling interval, 1-2 signals, analog reader
      }
      if (!strcmp (sharedMemory.samplingTimeUnit, "ms")) oscReader = oscReader_millis; // ms sampling intervl, 1-2 signals, digital-analog reader with 'sample at a time' option

      sharedMemory.oscReaderState = INITIAL;

      #ifdef OSCILLOSCOPE_READER_CORE
          BaseType_t taskCreated = xTaskCreatePinnedToCore (oscReader, "oscReader", 4 * 1024, (void *) &sharedMemory, OSCILLOSCOPE_READER_PRIORITY, NULL, OSCILLOSCOPE_READER_CORE);
      #else
          BaseType_t taskCreated = xTaskCreate (oscReader, "oscReader", 4 * 1024, (void *) &sharedMemory, OSCILLOSCOPE_READER_PRIORITY, NULL);
      #endif
      if (pdPASS != taskCreated) {
            #ifdef __DMESG__
                dmesg ("[oscilloscope] could not start oscReader");
            #endif
            webSocket->sendString ("[oscilloscope] could not start oscReader."); // send error also to javascript client
      } else {

                // send oscReader START signal and wait until STARTED
                sharedMemory.oscReaderState = START; 
                while (sharedMemory.oscReaderState == START) delay (1); 

        // start oscilloscope sender in this thread

        oscSender ((void *) &sharedMemory); 
        // stop reader - we can not simply vTaskDelete (oscReaderHandle) since this could happen in the middle of analogRead which would leave its internal semaphore locked

                // send oscReader STOP signal
                sharedMemory.oscReaderState = STOP; 

                // wait until oscReader STOPPED
                while (sharedMemory.oscReaderState != STOPPED) delay (1); 
      }

      return;
    }
kubark42 commented 10 months ago

Cool! I'm trying to find an ESP32 to try this with, I only have ESP32-S3 right now. I had originally modified your code to get it to work on those newer chips, but the high-speed ADC isn't supported on them. (Why there is so much craziness to Espressif's product line naming convention is a headscratcher.)

BojanJurca commented 7 months ago

Solved.

kubark42 commented 7 months ago

<3

On Wed, Nov 15, 2023 at 2:09 PM Bojan Jurca @.***> wrote:

Closed #21 https://github.com/BojanJurca/Esp32_oscilloscope/issues/21 as completed.

— Reply to this email directly, view it on GitHub https://github.com/BojanJurca/Esp32_oscilloscope/issues/21#event-10971161394, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAIQ72KIQCMEH3FWGWWM7J3YEUHODAVCNFSM6AAAAAA3Z6DMHSVHI2DSMVQWIX3LMV45UABCJFZXG5LFIV3GK3TUJZXXI2LGNFRWC5DJN5XDWMJQHE3TCMJWGEZTSNA . You are receiving this because you authored the thread.Message ID: @.*** com>