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);
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.
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));
}
}
// 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();
}
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 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.
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.
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);
}
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.
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.