How to draw real-time data in a specific canvas area?

Excuse me, I am testing multiple physiological parameters and hope to draw real-time waveforms in a specific canvas area, taking ECG as an example, how should I do it?


I have three channels of data. The data received by the serial port is unpacked and the waveform drawing data is cached in list<>(). Due to different sampling rates, the data size of each channel is inconsistent. However, when drawing the graph, I use multi-threading to update it every 20ms. For this reason, I want to know, is there any way to draw the data in a certain canvas area? Is there any way to dynamically update the data drawing?

mExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            if (mDrawWaveFlag) {
                drawEcgWave();
                drawRespWave();
                drawGSRWave();                  
            }
        }
    },0, 20, TimeUnit.MILLISECONDS);

Thanks!

In order to plot data you need x and y coordinates. Set the y coordinate to be different for each channel’s data. For example, you might put GSR at height - 50, RESP at height - 150, and ECG at height - 300 or something like that. Nice project.

Plotting data in realtime with Processing can be a little tricky in my experience. I presume that you are using SerialEvent() to get data from your input device. The data is sent as a string, and I set up a global to hold incoming data after converting the string to a float. In draw() this float value is then plotted at its default refresh rate (60 frame/sec). It’s not ideal and I remember struggling with this. I have an ECG project in the public domain if you would like to see one way of doing this:

I would recommend starting with something simple and then increase the complexity after you have it working the way you want it. For example I used the following sawtooth pattern generated by Arduino to get things working initially. It will also help you see if you are missing any data points; I was unable to draw directly from Serial Events() and had to use the global to store data before drawing. Separate threads might also work, but they can be tricky to juggle and use. You might want to try drawing the sawtooth pattern at different locations on the screen just to develop control over where it is drawn.

Arduino code:

byte outputValue = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
    outputValue += 4;
    Serial.println(outputValue);
 }

Note that there are no frame markers except for the line feed which println() adds. The line feeds could be used to parse the output ‘soup’ depending on how you are sending your data.

Here’s a Processing demo that you can use for testing:

import processing.serial.*;

float x, y, y1;
float inByte = 0;

Serial myPort;

void setup() {
  size(800, 900);
  background(255);
  printArray(Serial.list());
  // Enter correct number for your system
  myPort = new Serial(this, Serial.list()[1], 9600);
  strokeWeight(2);
}

void draw () {
  // Change last number to control screen position
  y1 = height - inByte - 550;
  line(x - 1, y, x, y1);
  y = y1;
  if (x >= width) {
    x = 0; // at edge of screen -> back to beginning
    background(255);
  } else {
    x++; // increment horizontal position:
  }
}

void serialEvent( Serial myPort) {
  //'\n' is the end delimiter indicating the end of a complete packet
  String inStr = myPort.readStringUntil('\n');
  //make sure our data isn't empty before continuing
  if (inStr != null) {
    //trim whitespace and formatting characters (like line feed, carriage return)
    inStr = trim(inStr);
    inByte = float(inStr);
    println(int(inByte));
  }
}

Hello @cola,

One way is with PGraphics to create an offscreen buffer and then display it as desired.

Example:

PGraphics d0, d1;

FloatList data0, data1;

void setup()
  {
  size(600, 500);
  d0 = createGraphics(600, 100);
  d1 = createGraphics(600, 100);
  data0 = new FloatList();
  data1 = new FloatList();
  
  for(int i=0; i<width; i++)
    {
    data0.append(20*sin(i*TAU/width));
    }

  for(int i=0; i<width; i++)
    {
    data1.append(10*sin(i*2*(TAU/width)));
    }
    
  d0.beginDraw();
  d0.background(255, 255, 0);
  d0.stroke(0);
  d0.strokeWeight(2);
  for(int i=0; i<data0.size() ; i++)
    {
    d0.point(i, data0.get(i)+d0.height/2);
    }
  d0.endDraw();

  d1.beginDraw();
  d1.background(128, 255, 0);
  d1.stroke(0);
  d1.strokeWeight(2);
  for(int i=0; i<data1.size() ; i++)
    {
    d1.point(i, data1.get(i)+d1.height/2);
    }
  d1.endDraw();

  }
  
void draw()
  {
  background(128); 
  image(d0, 0, 100);
  
  image(d1, mouseX, mouseY);
  }

:)

1 Like

Will that technique handle real-time data display or is it a static display?

Hello @cola,

Another example with simulated data in a thread:

// Draw data generated in a thread in realtime.
// Data is buffered and drawn with each frame.
//
// Author: GLV
// Date:   2025-01-22

PGraphics d0;
FloatList data0;

int i;

void setup()
  {
  size(600, 500);
  
  d0 = createGraphics(400, 100); 
  data0 = new FloatList();
  
  frameRate(60);           // default

  thread("requestData");   // start thread to generate data
  }

//*****************************************************************  

void draw()
  {
  background(0);
  update();
  image(d0, mouseX, mouseY);
  }

// This could be serialEvent() and only intended to simulate incoming data.
// There are other ways to control timing of a thread. :)

void requestData() 
  {
  while(true)
    {
    delay(3);
        
    float th = i*TAU/d0.width;
    data0.append(40*sin(th*3)*cos(th));
    
    if (data0.size() > d0.width)
    data0.remove(0);

   //update(); // Still thinking about best place for this, Here or draw()?    

    i = (i+1)%d0.width;  // 0 to 399 to avoid drift from floating point math
    println(i);      
    }    
  }  
    
void update()
  {
  d0.beginDraw();
  d0.background(255, 255, 0);
  d0.stroke(0);
  d0.strokeWeight(2);
  for(int i=0; i<data0.size() ; i++)
    {
    d0.point(i, data0.get(i)+d0.height/2);
    }
  d0.endDraw();  
  }

Note:
Data may not be synchronized in my example.

Have fun!

:)

Thank you for your answer, it’s helpful. I also used the same logic processing as you in the early stage, I used the Gplot method, which receives byte data through the serial port, and can be used for real-time mapping when collecting alone, but after adding equipment, due to the difference in sampling rate, this method is difficult to initialize.

What you are trying to do is not easy in my opinion. It’s one thing to plot an ecg, but you are adding two more data streams. Respirations shouldn’t be too bad, it’s just a number. I don’t know what GSR is. Are you collecting all this data with a single Arduino? Are you sending all three pieces of data in a single string? Are you are doing this with Bluetooth Classic and a mobile device or on a PC with serial?

I think PGraphics() is more in line with what I want than line(), but what does it mean that the data won’t sync?

I customized the transmission protocol so that each sensor transmits data independently, as long as the transmission interval does not conflict. At present, the serial port is used for transmission, and the data is uploaded to the PC side to unpack, and the following is my transmission protocol.

My thought would be to use a single string, line feed terminated, with comma separated data values. Then you parse it in SerialEvent() back to its array structure. How are you catching four separate data streams? Are you using java code or Processing functions?

Addendum:
I think you just need to send one string with three separate array elements (comma separated) from Arduino and let SerialEvent() parse it back into three separate values: eg, data[0], data[1], and data[2]. Split it first at the line feed and then at the commas which will automatically give you a data array with three elements. You would send it with Serial.println( outData[0] + “,” + outData[1] +“,” + outData[2]) which will automatically add the line feed at the end.

I use processing function. I’m doing this to distinguish between different sensor data, and the data I’m sending is actually a single string, but it’s just that the packet is depacked as the data is received.

The following works on my system to unpack a three integer array and reconstitute it:

Processing code:

import processing.serial.*;

Serial myPort;    // The serial port
String inStr;  // Input string from serial port
int lf = 10;      // ASCII linefeed

void setup() {
  surface.setVisible(false);
  printArray(Serial.list());
  myPort = new Serial(this, Serial.list()[1], 9600);
  myPort.bufferUntil(lf);
}

void draw() {
}

void serialEvent(Serial myPort) {
  inStr = myPort.readString();
  if (inStr != null) {
    inStr = trim(inStr);
    String[] data = split(inStr, ',');
    printArray(data);
    println("array length = ", data.length);
    if (data.length == 3) {
      println (" v0 " + data[0] + " v1 " + data[1] + " v2 " + data[2]);
    }
  }
}

Arduino code:

int num[3];

void setup() {
  Serial.begin(9600);
}

void loop() {
  num[0] = random(255);
  num[1] = random(30, 90);
  num[2] = random(6,15);
  Serial.print(num[0]);
  Serial.print(",");  // Comma separated values
  Serial.print(num[1]);
  Serial.print(",");
  Serial.println(num[2]); // Has a line feed.
  //Wait for a bit to keep serial data from saturating
  delay(10);
}

When the drawing width is reached, I want it to update the drawing from left to right again, but not clear the previous data at once. How can I modify it? Because I tried either the scrolling mode like yours, or clearing all and redrawing.

I will try it !Thanks :handshake:

Hello @cola,

Keep in mind what Serial.println() is sending from Arduino:
https://docs.arduino.cc/language-reference/en/functions/communication/serial/println/

It is much cleaner to do this on Arduino side as the terminating character:

Serial.print('\n'); // Sends only an ASCII line feed

And one less character to send and deal with on receiving end.

You may have to trim() those extra characters later before converting a String to a float or otherwise.
Sometimes the conversion method will trim whitespace for you but that is not always the case.

Keep this in mind for future!

References:

:)

Hello @svan,

It is technically still a String array in your code.

I will leave the conversion to an integer or float for @cola to sort out.

:)

True. Serial data is sent as a string.

The following is a more complete example which converts the string array to a float array and then plots the data for all three values:

Processing code:

import processing.serial.*;

float x, y0, y1, y2;
float d0, d1, d2;
float[] inByte = new float[3];
int lf = 10;      // ASCII linefeed

Serial myPort;

void setup() {
  size(800, 900);
  background(255);
  printArray(Serial.list());
  // Enter correct number for your system
  myPort = new Serial(this, Serial.list()[1], 9600);
  myPort.bufferUntil(lf);
  strokeWeight(2);
}

void draw () {
  // Change last number to control screen position
  d0 = height - inByte[0] - 550;
  line(x - 1, y0, x, d0);
  y0 = d0;

  d1 = height - inByte[1] - 300;
  line(x - 1, y1, x, d1);
  y1 = d1;

  d2 = height - inByte[2] - 100;
  line(x - 1, y2, x, d2);
  y2 = d2;

  if (x >= width) {
    x = 0; // at edge of screen -> back to beginning
    background(255);
  } else {
    x++; // increment horizontal position:
  }
}

void serialEvent(Serial myPort) {
  String inStr = myPort.readString();
  if (inStr != null) {
    inStr = trim(inStr);
    String[] data = split(inStr, ',');
    printArray(data);
    println("array length = ", data.length);
    if (data.length == 3) { // converts string array to float array
      inByte[0] = float(data[0]);
      inByte[1] = float(data[1]);
      inByte[2] = float(data[2]);
      println (" d0 " + data[0] + " d1 " + data[1] + " d2 " + data[2]);
    }
  }
}

Arduino code:

int num[3];

void setup() {
  Serial.begin(9600);
}

void loop() {
  num[0] = random(255);
  num[1] = random(30, 90);
  num[2] = random(6,15);
  Serial.print(num[0]);
  Serial.print(",");  // Comma separated values
  Serial.print(num[1]);
  Serial.print(",");
  Serial.println(num[2]); // Has a line feed.
  //Wait for a bit to keep serial data from saturating
  delay(10);
}

Output:

Serial.println() according to your reference adds both a carriage return and a line feed. In the Processing demo that I posted the carriage return seems not to cause a problem and was not separately ‘dealt with’. I tried the Arduino code both ways and the output on the Processing side was the same. However, that may not to be the case when working in Python where I’ve had to .strip both characters at one time to get clean data. I’ve always liked Serial.println() because it’s less code on the Arduino side. But if I ever see a problem in Processing’s SerialEvent() after adding a carriage return I could be convinced to change.

Addendum:
In order to get rid of a line feed added with ‘Serial.print(’\n’)’ the user will likely have to use trim(inStr), which also removes carriage returns so I can’t see any advantage to this extra line of code on the Arduino side vs. using Serial.println(data). In addition, there seems to be no penalty for leaving a line feed or carriage return with the data by skipping the trim() as shown in the posted example (which doesn’t use it). However, there are other hidden characters sometimes added to the string and trim() will remove these as well, so it’s probably a good idea to use trim() as a matter of safety. I will edit the example to add trim(inStr) for this reason.

We can completely drop the ','& '\n' separator characters and just use the buffer() method:

1 Like

That’s in setup(); does that count? With the technique shown the commas are necessary to subdivide the string into a data array. I’m not sure how you would know where to split it without some sort of marker. The string is separated first at the line feed (I guess by the line of code above), then split again at the commas.