Plotting serial data via pyserial at high frequency, live and logged

I am trying to develop an arduino based sensor for respiratory rate in small animals. We get decent measurements of respiration via a force sensitive resistor (coin-sized) mounted on the back of the anesthetized animal. I managed to log the data via pyserial in Python with timestamps, but I would also like to live-plot the data for on-site monitoring of respiratory rate. I don't have much experience with this, but I managed to set up a script that gives a live plot - but it only plots 0 values, or higher values but after a significant delay (like 20s). The csv logging is also broken in this script (only 0 values, or significantly delayed higher values). Ideally, we would have a live plot with a refresh rate of ~100hz or more.

Any pointers on how I can achieve this with Python? Or if Python is too slow for this, any other way?

Arduino code:

#include <Streaming.h>

int fsrPin = 0;     // the FSR and 10K pulldown are connected to a0
int fsrReading;     // the analog reading from the FSR resistor divider
 
void setup(void) {
  Serial.begin(115200);   
}

void loop(void) {
  fsrReading = analogRead(fsrPin);  
 
  Serial.print(fsrReading);     // print the raw analog reading
  Serial.println();
}

Python code:

import serial
import time
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import csv

def generate_output_filename():
    now = datetime.now()
    filename = now.strftime("output_%Y%m%d_%H%M%S.csv")
    return filename

# Function to update the plot with new data
def update_plot(x_data, y_data, line, ax):
    line.set_xdata(x_data)
    line.set_ydata(y_data)
    
    if len(x_data) > 0:
        # Set x-axis to only show the last 10 seconds
        max_time = max(x_data)  # Latest timestamp
        min_time = max_time - timedelta(seconds=10)
        ax.set_xlim(min_time, max_time)
        ax.set_ylim(0,1000)
    
    ax.relim()  # Recalculate limits
    ax.autoscale_view(True, True, True)  # Rescale the y-axis
    plt.draw()
    plt.pause(0.001)  # A short pause to allow the plot to update

def readserial(comport, baudrate, timestamp=False):
    # Setup for live plotting
    plt.ion()  # Start interactive mode
    fig, ax = plt.subplots()
    x_data, y_data = [], []
    line, = ax.plot(x_data, y_data, 'r-')  # Red line for the live data
    plt.xlabel('Time')
    plt.ylabel('Data')
    plt.title('Live Data Plot')

    ser = serial.Serial(comport, baudrate, timeout=None)
    time.sleep(2)   # give the arduino time to start.
    ser.flushInput()

    output_csv = generate_output_filename()
    with open(output_csv, 'w', newline='') as csvfile:
        csv_writer = csv.writer(csvfile)
        csv_writer.writerow(['Timestamp', 'Data'])

        try:
            while True:
                data = ser.readline().decode().strip() #strip newline characters.
                if data and timestamp:
                    now = datetime.now()
                    formatted_timestamp = now.strftime('%H:%M:%S') + '.{:04d}'.format(int(now.microsecond / 1000))
                    csv_writer.writerow([formatted_timestamp, data])
                    print(f'{formatted_timestamp} > {data}')  # Monitor in terminal.

                    # Update plot data
                    x_data.append(now)   # Use datetime for x-axis
                    y_data.append(float(data))  # Convert the data to float for y-axis
                    update_plot(x_data, y_data, line, ax)

        except KeyboardInterrupt:
            print("Serial reading stopped by user (Ctrl-c in terminal)")

    plt.ioff()  # Turn off interactive mode.
    plt.show()  # Keep the window open even after the data stops coming in.

if __name__ == '__main__':
    readserial('COM9', 115200, timestamp=True)

Python code without plotting, just logging to csv (this works in real-time).

import serial
import time
from datetime import datetime
import csv

def generate_output_filename():
    now = datetime.now()
    # Create a datetime string suitable for a filename, e.g., "output_20230405_123456.csv"
    filename = now.strftime("output_%Y%m%d_%H%M%S.csv")
    return filename

def readserial(comport, baudrate, timestamp=False):

    ser = serial.Serial(comport, baudrate, timeout=None)         # 1/timeout is the frequency at which the port is read
    time.sleep(2)  # give the arduino time to start.
    ser.flushInput()
    
    output_csv = generate_output_filename()  # Generate filename with current datetime
    with open(output_csv, 'w', newline='') as csvfile:
        csv_writer = csv.writer(csvfile)
        csv_writer.writerow(['Timestamp', 'Data'])  # Write the header row
        try:
            while True:
                data = ser.readline().decode().strip() #'utf-8', errors='ignore'
                if data and timestamp:
                    now = datetime.now()
                    formatted_timestamp = now.strftime('%H:%M:%S') + '.{:04d}'.format(int(now.microsecond / 1000))
                    csv_writer.writerow([formatted_timestamp, data])
                    print(f'{formatted_timestamp} > {data}')   # Optional: monitor in terminal.
        
        except KeyboardInterrupt:
            print("Serial reading stopped by user. (Ctrl-c in terminal)")


if __name__ == '__main__':

    readserial('COM9', 115200, timestamp=True)```

The MegunoLink plotting program may give you the speed you need.
It's a regular old school Windows program. There's a free trial version.

Alternatively just use an oscilloscope to look at the real-time analog signal from the sensor.

1 Like

Maybe you could try with LOGGBOK. It's currently free and you could log and monitor data in the millisecond range.

DISCLAIMER: I'm the developer of the program.

1 Like

Hi @sebajost, this looks great, thanks! I managed to install it and I modified my arduino code based on examples from your loggbok github, but I can't seem to get the monitor going. Any additional pointers?

This is how far I got:

And this is the code, which is probably wrong..

#include <Arduino.h>
#include <BlaeckSerial.h>

#define ExampleVersion "0.1"
int fsrPin = 0;     // the FSR and 10K pulldown are connected to a0
int fsrReading;     // the analog reading from the FSR resistor divider

BlaeckSerial BlaeckSerial;
void setup(void) {
  Serial.begin(115200);   
  BlaeckSerial.begin(
    &Serial,   //Serial reference
    1         //Maxmimal signal count used;
  );
  BlaeckSerial.DeviceName = "Force Sensitive Resistor";
  BlaeckSerial.DeviceHWVersion = "Arduino Mega 328 Rev3";
  BlaeckSerial.DeviceFWVersion = ExampleVersion;

  BlaeckSerial.addSignal("Force", &fsrReading);
}

void loop(void) {
  fsrReading = analogRead(fsrPin);  
  BlaeckSerial.tick();
  //Serial.print(fsrReading);     // print the raw analog reading
  //Serial.println();
}```

Hi sverreg,

just change the Monitored Signal to Force.

1 Like

Code looks good! :slight_smile:

1 Like

That was easy! Works perfectly now. Easy to install (no admin required, a blessing on a university desktop), easy to run (with 0 .ino experience), UI looks great, and somehow it is free. Thank you so much!

Edit: Pic of /1ms recording:

1 Like

Hi @sverreg! Looks nice. I'm happy I could help. Thanks for your kind words. Maybe you could post this as a review on Microsoft store? I would very much appreciate it.

2 Likes

Of course, done!

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.