Send data from Teensy to Python

frohr

Well-known member
Hi all,

I try make app which send data from Teensy 4.0 to Kivy Python app. Now I send some info data usin serial.print and its ok. I send 30000 values using serial.write and it works for a few readings and then is some error.

I need help with 3 points:
- How to send data correct and as fast as possible?
- How send 30000 or 120000 or 240000 of values?
- How read data in loop without data loss?

Teensy code:

Rich (BB code):
#include <Arduino.h>

const int numSamples = 30000;  // Number of samples to generate and send
float samples[numSamples];

void generateSineWave() {
  float amplitude = random(50, 301) / 100.0;  // Random amplitude between 0.5g and 3g
  float frequency = 50.0; // 50Hz
  float samplingRate = 30000.0; // 30k SPS
  for (int i = 0; i < numSamples; i++) {
    samples = amplitude * sin(2 * PI * frequency * (i / samplingRate));
  }
}

void setup() {
  Serial.begin(115200); 
  randomSeed(analogRead(0)); 
  while (!Serial);
}

void loop() {
  if (Serial.available() > 0) {
    char receivedChar = Serial.read();

    if (receivedChar == 't') {
      Serial.println("@");  // Send Analyzer ready message
    } else if (receivedChar == 'C') {
      delay(10); 
      if (Serial.available() > 0) {
        char command = Serial.read();
        if (command == '1') {
          Serial.println("Correction1=1.123456");  // Send Correction1 value
        } else if (command == '2') {
          Serial.println("Correction2=2.12345");  // Send Correction2 value
        }
      }
    } else if (receivedChar == 'r') {
      generateSineWave();
      Serial.println("D");  // Data start transfer
      sendData();
      Serial.println("E");  // Data sending end
    }
  }
}

void sendData() {
  int bytesToSend = numSamples * sizeof(float);
  Serial.write((uint8_t*)samples, bytesToSend);

  // Wait for the receiver to acknowledge receipt of the chunk
  while (!Serial.available());
  char ack = Serial.read();
  if (ack != 'A') {
    // Handle error maybe
    Serial.println("Error in data transmission.");
  }
}



Python Kivy code:
Rich (BB code):
import kivy
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.textinput import TextInput
from kivy.clock import Clock
from kivy.garden.graph import Graph, MeshLinePlot
import serial
import threading
import struct

class TeensyApp(App):
    def build(self):
        self.serial_port = serial.Serial('COM5', 115200, timeout=1)
        self.serial_port.flush()

        layout = BoxLayout(orientation='vertical')

        self.response_text = TextInput(size_hint=(1, 0.4), readonly=True)
        layout.add_widget(self.response_text)

        button_layout = BoxLayout(size_hint=(1, 0.2))

        self.t_button = Button(text='Send t')
        self.t_button.bind(on_press=self.send_t)
        button_layout.add_widget(self.t_button)

        self.c1_button = Button(text='Send C1')
        self.c1_button.bind(on_press=self.send_c1)
        button_layout.add_widget(self.c1_button)

        self.c2_button = Button(text='Send C2')
        self.c2_button.bind(on_press=self.send_c2)
        button_layout.add_widget(self.c2_button)

        self.r_button = Button(text='Send r')
        self.r_button.bind(on_press=self.send_r)
        button_layout.add_widget(self.r_button)

        layout.add_widget(button_layout)

        self.graph = Graph(
            xlabel='Time', ylabel='Amplitude',
            x_ticks_minor=5, x_ticks_major=25,
            y_ticks_minor=0.5, y_ticks_major=1,
            y_grid_label=True, x_grid_label=True, padding=5,
            x_grid=True, y_grid=True, xmin=0, xmax=30000, ymin=-2, ymax=2
        )
        self.plot = MeshLinePlot(color=[1, 0, 0, 1])
        self.graph.add_plot(self.plot)
        layout.add_widget(self.graph)

        # Start a thread to read data from the serial port
        self.data_buffer = bytearray()
        self.lock = threading.Lock()
        self.receiving_data = False
        self.data_length = 120000  # 30,000 floats * 4 bytes per float
        self.current_state = "IDLE"
        threading.Thread(target=self.read_from_port, daemon=True).start()

        return layout

    def send_t(self, instance):
        with self.lock:
            self.serial_port.write(b't')
            self.current_state = "WAIT_FOR_RESPONSE"
            Clock.schedule_once(lambda dt: self.update_textbox('Sent t command'), 0)

    def send_c1(self, instance):
        with self.lock:
            self.serial_port.write(b'C1')
            self.current_state = "WAIT_FOR_RESPONSE"
            Clock.schedule_once(lambda dt: self.update_textbox('Sent C1 command'), 0)

    def send_c2(self, instance):
        with self.lock:
            self.serial_port.write(b'C2')
            self.current_state = "WAIT_FOR_RESPONSE"
            Clock.schedule_once(lambda dt: self.update_textbox('Sent C2 command'), 0)

    def send_r(self, instance):
        with self.lock:
            self.serial_port.write(b'r')
            self.current_state = "WAIT_FOR_D"
            Clock.schedule_once(lambda dt: self.update_textbox('Sent r command'), 0)

    def read_from_port(self):
        while True:
            try:
                with self.lock:
                    if self.serial_port.in_waiting > 0:
                        if self.current_state == "WAIT_FOR_RESPONSE":
                            line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
                            Clock.schedule_once(lambda dt: self.update_textbox(f'Received response: {line}'), 0)
                            self.current_state = "IDLE"
                        elif self.current_state == "WAIT_FOR_D":
                            line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
                            if line == 'D':
                                Clock.schedule_once(lambda dt: self.update_textbox('Data transfer started (D)'), 0)
                                self.current_state = "RECEIVE_DATA"
                                self.data_buffer = bytearray()
                            else:
                                Clock.schedule_once(lambda dt: self.update_textbox(f'Received line: {line}'), 0)
                        elif self.current_state == "RECEIVE_DATA":
                            if len(self.data_buffer) < self.data_length:
                                self.data_buffer.extend(self.serial_port.read(self.serial_port.in_waiting))
                                if len(self.data_buffer) >= self.data_length:
                                    self.serial_port.write(b'A')  # Acknowledge receipt of data
                                    self.current_state = "WAIT_FOR_E"
                            else:
                                self.current_state = "WAIT_FOR_E"
                        elif self.current_state == "WAIT_FOR_E":
                            line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
                            if line == 'E':
                                Clock.schedule_once(lambda dt: self.update_textbox('Data transfer ended (E)'), 0)
                                self.process_data()
                                self.current_state = "IDLE"
            except Exception as e:
                Clock.schedule_once(lambda dt: self.update_textbox(f'Error: {e}'), 0)

    def update_textbox(self, line):
        self.response_text.text += f"{line}\n"

    def process_data(self):
        total_floats = len(self.data_buffer) // 4
        if total_floats == 30000:
            unpacked_data = struct.unpack('f' * total_floats, self.data_buffer)
            # Plotting the received 30,000 samples
            self.plot.points = [(i, unpacked_data) for i in range(len(unpacked_data))]
            Clock.schedule_once(lambda dt: self.update_textbox(f'Received {len(unpacked_data)} samples'), 0)
        else:
            Clock.schedule_once(lambda dt: self.update_textbox(f'Error: Received {total_floats} floats, expected 30000'), 0)

if __name__ == '__main__':
    TeensyApp().run()



Thanks for any advice!
Michal
 
const int numSamples = 30000; // Number of samples to generate and send
float samples[numSamples];

void sendData() {
int bytesToSend = numSamples * sizeof(float);
Serial.write((uint8_t*)samples, bytesToSend);

...
}
[/CODE]
Don't try to send 30000 float values in a single Serial.write(). Start with 10 to see if the handshaking works, then try to go up to 100 and then maybe you can even get to 1000. That would be 4000 bytes and it might work, but you also might have a limit below 1000 float values per Serial.write().
 
600k per second yes. Over the usb connection to the pc as COM port.
Are you referring to your function named communicationProcess() in firmware.ino? If I understand that code correctly, it sends just one variable of size 1 to 8 bytes per call to Serial.write(). Also, do you mean 600K bytes/sec or 600K values/sec?
 
I regularly send about 4000 values, in either binary using write or formatted using print.

You can see my code for one example of this at:
https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library

There is a somewhat newer version at this next link, though at 2000 values,
https://github.com/drmcnelson/S11639-01-Linear-CCD-PCB-and-Code
The above is with python multitasking on the receiving end. There is a C++ app using Windex (for windows), I can try to bring that up to snuff for release if you need it.

There are a few points to be aware of:

The secret of success is largely on the host side.

You have two choices, either
(a) there has to be a buffer large enough to receive the data in the device driver, or
(b) your host program has to be able to retrieve the data from the driver at a rate that exceeds the rate at which the driver receives it.

Further regarding (b), any pause in retrieving the data from the driver has to be short and balanced by a yet faster transfer rate from the driver to your program.

Handshaking is not actually a (c), it will reduce how much you might need to buffer at once, but it is still (a) or (b), plus more complexity. What happens often by adding handshaking, is you think you solved it, but eventually it fails for having failed to correctly implement the underlying (a) or (b) even at that reduced buffer size.

Succesful implementation of (b) almost always is based on multi-threading. You need one thread that has a loop that just reads from the serial port and sends the data to an appropriate thread for processing. For example using queues and good architecture (think about what goes where) your queues will absorb the lapses in performance and eventually the system catches up.

To get true parallel processing with Python in Linux, you need to use the multitasking api (assuming you also have a true multi core cpu or multi cpu system). I do not know what the story is with Python threads in Windows. I know there are some limitations in using the multi-tasking api in windows that do not appear in Liunux.

Parallel processing is a topic in computer engineering. Using queues helps but there are still some things to understand. I recall there was a tutorial on posix threads that seemed to cover some of the basic ideas.

And finally, my experience is that all of this, whether you are using (a) or (b), is more challenging in Windows than in Linux. Windows seems to throw away a lot of performance and real time response is one of the places where it shows up.

I added CRC code to the latest versions of my own code, I am not sure if it is in the repos yet. But anyway, it was to implement a re-send to deal with windows. The linux system never generates a re-send event.
 
I updated my code and test it on Windows and seems quite ok. Or you see there any mistakes? I send 120000 values from Teensy, it takes about 1.4s. I generate sine wave with some noise and add 5us between every sample to emulate 30k sampling rate of ads1256.

I have to test it also on Android mobile because its very important for me (change to Kivy and compile using Buildozer).

Code:
#include <Arduino.h>

const int sampleRate = 30000; // 30 kHz
const int numSamples = 120000; // 30,000 samples
const float frequency = 50.0; // 50 Hz
const float amplitude = 1.0;  // 1g for sine wave
const int noiseMinFreq = 100; // 100 Hz
const int noiseMaxFreq = 200; // 200 Hz
const float noiseMinG = 0.1; // 0.1g minimum noise amplitude
const float noiseMaxG = 1.0; // 1g maximum noise amplitude

float data[numSamples]; // Data buffer

void setup() {
  Serial.begin(115200); // Initialize serial communication
}

void loop() {
  if (Serial.available()) {
    char command = Serial.read();
    if (command == 'r') {
      generateSineWaveWithNoise();
      Serial.write('d'); // Send confirmation
      sendDataInChunks();
      Serial.write('E'); // Send end confirmation
    }
  }
}

void generateSineWaveWithNoise() {
  for (int i = 0; i < numSamples; i++) {
    float t = (float)i / sampleRate;
    float sineValue = amplitude * sin(2 * PI * frequency * t);
    float noiseFrequency = random(noiseMinFreq, noiseMaxFreq);
    float noiseG = noiseMinG + (noiseMaxG - noiseMinG) * ((float)random() / (float)RAND_MAX);
    float noiseValue = noiseG * sin(2 * PI * noiseFrequency * t + ((float)random() / (float)RAND_MAX) * 2 * PI - PI); // Adjust noise amplitude and phase
    data[i] = sineValue + noiseValue;
    delayMicroseconds(5); // Simulate sampling rate of ADC
  }
}

void sendDataInChunks() {
  const unsigned int chunkSize = 500; // Adjust chunk size as needed
  unsigned int bytesSent = 0; // Use unsigned int for bytesSent
  while (bytesSent < numSamples * sizeof(float)) {
    unsigned int bytesToSend = min(chunkSize, (unsigned int)(numSamples * sizeof(float) - bytesSent));
    Serial.write((byte*)data + bytesSent, bytesToSend);
    bytesSent += bytesToSend;
  }
}



Python code:

Code:
import serial
import numpy as np
import time
import threading
from tkinter import *
from tkinter.scrolledtext import ScrolledText
from scipy.signal import detrend
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

# Ensure the correct backend for Matplotlib
matplotlib.use("TkAgg")

def calculate_rms(data):
    return np.sqrt(np.mean(np.square(data)))

def read_data(ser, num_samples):
    data = bytearray()
    while len(data) < num_samples * 4:
        chunk = ser.read(num_samples * 4 - len(data))
        if not chunk:
            break
        data.extend(chunk)
    if len(data) != num_samples * 4:
        log_message(f"Error: Expected {num_samples * 4} bytes, but received {len(data)} bytes")
        return np.array([])
    return np.frombuffer(data, dtype=np.float32)

def send_command_and_receive_data():
    global is_reading
    tot_err = 0
    while is_reading:
        try:
            tot_time = 0
            start_time = time.time()
            ser.write(b'r')
            log_message("Sent command: r")
            ser.flush()  # Ensure all data is sent before continuing
            time.sleep(0.1)  # Wait a short period to ensure Teensy is ready

            response = ser.read(1)
            if response == b'd':
                log_message("Received confirmation: d")
                data = read_data(ser, num_samples)
                if len(data) == num_samples:
                    log_message(f"Received data length: {len(data)}")
                    rms_value = calculate_rms(data)
                    log_message(f"RMS of received data: {rms_value:.4f}")
                    if data.size > 0:
                        update_plot(data)
                else:
                    log_message("Error: Incomplete data received")
                    tot_err += 1
                end_signal = ser.read(1)
                if end_signal == b'E':
                    log_message("Received end signal: E")
            else:
                log_message("Error: Did not receive expected confirmation")
                tot_err += 1
            
            end_time = time.time()
            tot_time = end_time - start_time
            log_message(f"Total time: {tot_time}")
            log_message(f"Total error: {tot_err}")

        except serial.SerialException as e:
            log_message(f"Serial exception: {e}")
            is_reading = False
            close_serial()
        except Exception as e:
            log_message(f"Exception: {e}")
            is_reading = False
            close_serial()
    is_reading = False

def close_serial():
    global ser
    if ser and ser.is_open:
        ser.close()

def reconnect_serial():
    global ser
    try:
        ser = serial.Serial('COM5', 115200, timeout=1)
        log_message("Reconnected successfully")
    except serial.SerialException as e:
        log_message(f"Failed to reconnect: {e}")

def on_send_command():
    global is_reading, read_thread
    if is_reading:
        is_reading = False
        send_button.config(text="Start Reading")
        log_message("Stopped reading")
        if read_thread and read_thread.is_alive():
            read_thread.join(timeout=1)  # Wait for the thread to exit, with timeout
    else:
        try:
            reconnect_serial()
            if ser.is_open:
                is_reading = True
                send_button.config(text="Stop Reading")
                log_message("Started reading")
                read_thread = threading.Thread(target=send_command_and_receive_data)
                read_thread.start()
        except Exception as e:
            log_message(f"Exception while starting: {e}")

def update_plot(data):
    ax.clear()
    ax.plot(data)
    ax.set_title("Teensy Data")
    ax.set_xlabel("Sample")
    ax.set_ylabel("Amplitude")
    canvas.draw()

def log_message(message):
    log_text.insert(END, message + "\n")
    log_text.see(END)  # Scroll to the end


ser = None
try:
    ser = serial.Serial('COM5', 115200, timeout=1)
except serial.SerialException as e:
    print(f"Failed to connect to the device: {e}")

num_samples = 120000
is_reading = False
read_thread = None

# Setup GUI
root = Tk()
root.title("Teensy Data Receiver")

log_text = ScrolledText(root, wrap=WORD, width=50, height=20)
log_text.pack(padx=10, pady=10)

send_button = Button(root, text="Start Reading", command=on_send_command)
send_button.pack(pady=5)

# Setup Plot
fig, ax = plt.subplots()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)

root.mainloop()

# Close the serial connection on exit
if ser:
    ser.close()
 
Two comments, one is a correction to my previous.

If you can serialize your exchanges, then that is a third approach. Bottom line, it is about getting the data from he driver before the internal buffer in the driver runs out of room.

Re the code, again, take a look at the multiprocessing API in python, it is almost "plug compatible" with threads at a superficial level, but sharing data and queues is a little different, because it is implemented as an actual parallel process. Threads share the interpreter, as i recall, so they are not actually free of each other. There are comments about threads being okay for i/o bound situations, but it is easy to imagine how it might still effect latencies, and that is a big part of the what you need to avoid those buffer underruns.

And finally a suggestion:

When you test, you may want to test for a long time, or at lest if your failure tolerance is low.

A story: I recall building a sonar buoy system that fed data to an RF tape deck, all running on a TI DSP board in a VME crate. It was engineered to keep the tape drive moving to avoid repositioning. It ran great except that once in 24 hours if would glitch, it turned out to be in an fpga that moved the data between two busses running at different clocks.

So, anyway, the point is that these buffer versus latency scenarios sometimes need to be tested pretty exhaustivley, and especially when one end of the transfer also hosts a user environment, (and even more so when it is windows which has a long running reputation for miserably poor latencies).
 
Are you referring to your function named communicationProcess() in firmware.ino? If I understand that code correctly, it sends just one variable of size 1 to 8 bytes per call to Serial.write(). Also, do you mean 600K bytes/sec or 600K values/sec?
Yes. Correct. Over 600k 32 bit values per second. Teensy automatically put the write command data into a buffer and makes larger usb packets. Usbpcap can capture the packets to check.

The pc side is actually the harder one, you have to keep up with the datastream, and I have not found a way to make the driverbuffer at the pc side bigger.
 
Back
Top