Arduino UNO is slowing down after sending some data

Hello Everybody,
I need help with my project I am using a laser communication to achieve sending data (text, images, sound) with turning each into text first and sending text as 1's and 0's via alternating the laser state, on the other side there is an IR sensor that detects it and is able to do the process backwards.

Sender Code:

const int laserPin = 4;  // Pin connected to the laser
short bitDuration = 700;

void setup() {
  // Initialize the laser pin as an output
  pinMode(laserPin, OUTPUT);
  digitalWrite(laserPin, HIGH);
  // Initialize the serial communication
  Serial.begin(2000000);
}

void loop() {
  // Check if any data is available to read from the serial port
  if (Serial.available() > 0) {
    // Read the incoming byte
    char incomingByte = Serial.read();

    sendByteAsBits(incomingByte);
  }
}

void sendByteAsBits(char byte) {

  // Send start bit (LOW)
  digitalWrite(laserPin, LOW);
  delayMicroseconds(bitDuration*2);
  
  // Send each bit of the byte
  for (int i = 7; i >= 0; i--) {
    bool bitValue = bitRead(byte, i);
    digitalWrite(laserPin, bitValue ? HIGH : LOW);
    Serial.print(bitValue);
    delayMicroseconds(bitDuration);
  }

  // Send stop bit (HIGH)
  digitalWrite(laserPin, HIGH);
  delayMicroseconds(bitDuration*2);
}

Receiver Code:

int irpin = 7;
bool receiving = false;
float bitDuration = 700;
unsigned char recived_byte = 0;

void setup() {
  Serial.begin(2000000);
  pinMode(irpin, INPUT);
}

void loop() {
  if (receiving) {
    recived_byte = 0;  // Reset the received byte
    // Read each bit of the byte
    for (int i = 0; i < 8; i++) {
      recived_byte = (recived_byte << 1) | !digitalRead(irpin);  // Read the bit and shift into position
      // Serial.print(!digitalRead(irpin));
      delayMicroseconds(bitDuration);  // Wait for the next bit
    }
    receiving = false;  // Reset the receiving flag

    if (recived_byte != 0) {
      char receivedChar = (char)recived_byte;  // Convert to character
      Serial.print(receivedChar);  // Print the received character
    }
  } else if (!digitalRead(irpin) == LOW) {
    // Detect start bit (LOW signal)
    receiving = true;  // Set the receiving flag

    delayMicroseconds(bitDuration * 3);  // Wait to the middle of the first data bit
  }
}


Fortunately the Data is being sent correctly and I am able to retrieve it the correct way when it comes to serial monitor inputted data,
When it comes to me wanting to send multiple types of data, I have used Python to make this possible and to make a GUI for the sender and receiver and handle encoding of file and so on like so:


via these code
sender:

import tkinter as tk
from tkinter import filedialog
import serial
import time
import threading
from datetime import datetime
import base64
import os
import zlib  # Import zlib for compression

class SenderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Chat APP")

        self.send_button = tk.Button(root, text="Send Text", command=self.send_text)
        self.send_button.pack(pady=10)
        
        self.send_button = tk.Button(root, text="Send Image", command=self.send_image)
        self.send_button.pack(pady=10)
        
        self.send_button = tk.Button(root, text="Send Sound", command=self.send_sound)
        self.send_button.pack(pady=10)

        self.progress_label = tk.Label(root, text="Progress: 0%")
        self.progress_label.pack(pady=10)

        self.chat_label = tk.Label(root, text="Chat")
        self.chat_label.pack(pady=10)
        
        self.chat_display = tk.Text(root, height=15, width=50)
        self.chat_display.pack(pady=10)
        
        self.chat_entry = tk.Entry(root, width=40)
        self.chat_entry.pack(side=tk.LEFT, padx=10)
        
        self.chat_send_button = tk.Button(root, text="Send", command=self.send_chat)
        self.chat_send_button.pack(side=tk.RIGHT, padx=10)
        
    
    def split_text(self, text, chunk_size=512):
        return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)]

    def send_text(self):
        file_path = filedialog.askopenfilename()
        if not file_path:
            return

        # Clear the chat display
        self.chat_display.delete(1.0, tk.END)

        with open(file_path, 'r') as file:
            text = file.read()
            
        compressed_text = zlib.compress(text.encode('utf-8'))  # Compress the text
        encoded_text = base64.b64encode(compressed_text).decode('utf-8')  # Encode to base64

        self.total_size = len(encoded_text)
        self.sent_size = 0
        chunks = self.split_text(encoded_text)
        
        threading.Thread(target=self.send_chunks, args=(chunks,b'\x03')).start()

    def send_image(self):
        image_path = filedialog.askopenfilename()
        # Check if the image file exists
        if not os.path.isfile(image_path):
            raise FileNotFoundError(f"The image file {image_path} does not exist.")
        
        # Read the image file in binary mode
        with open(image_path, "rb") as image_file:
            image_data = image_file.read()
        
        # Encode the image data to base64
        compressed_image = zlib.compress(image_data)  # Compress the image
        encoded_image_data = base64.b64encode(compressed_image).decode('utf-8')
        
        # Get the directory and file name without extension
        file_dir, file_name = os.path.split(image_path)
        file_name_without_ext = os.path.splitext(file_name)[0]
        
        # Create the text file path
        text_file_path = os.path.join(file_dir, f"{file_name_without_ext}.txt")
        
        # Write the encoded image data to the text file
        with open(text_file_path, "w") as text_file:
            text_file.write(encoded_image_data)
    
        file_path = text_file_path
        if not file_path:
            return

        # Clear the chat display
        self.chat_display.delete(1.0, tk.END)

        with open(text_file_path, 'r') as file:
            text = file.read()
            
        self.total_size = len(text)
        self.sent_size = 0
        chunks = self.split_text(text)
        
        threading.Thread(target=self.send_chunks, args=(chunks,b'\x06')).start()


    def send_sound(self):

        file_path = filedialog.askopenfilename(
            title="Select an MP3 file",
            filetypes=[("MP3 files", "*.mp3")]
        )
        if not file_path:
            print("No file selected.")
            return

        # Read the MP3 file
        with open(file_path, 'rb') as mp3_file:
            mp3_data = mp3_file.read()
            
            # Encode the MP3 data to UTF-8
        compressed_sound = zlib.compress(mp3_data)  # Compress the image
        base64_data = base64.b64encode(compressed_sound).decode('utf-8')

        # Create a new filename for the encoded text file
        base_name = os.path.basename(file_path)
        name, _ = os.path.splitext(base_name)
        utf8_file_path = os.path.join(os.path.dirname(file_path), f"{name}.txt")

        # Save the encoded data to a text file
        with open(utf8_file_path, 'w', encoding='utf-8') as utf8_file:
            utf8_file.write(base64_data)

        # Clear the chat display
        self.chat_display.delete(1.0, tk.END)

        with open(utf8_file_path, 'r') as file:
            text = file.read()

        self.total_size = len(text)
        self.sent_size = 0
        chunks = self.split_text(text)
            
        threading.Thread(target=self.send_chunks, args=(chunks,b'\x07')).start()


    def send_chunks(self, chunks, end_frame):
        with serial.Serial('COM3', 2000000, timeout=1) as ser:
            time.sleep(2)  # Wait for the serial connection to initialize
            ser.flushInput()  # Clear Arduino buffer

            # Send unique start frame for the first chunk
            ser.write(b'\x01')  # Unique start of transmission for the first chunk

            for chunk in chunks:
                ser.write(b'\x02')  # Standard start of transmission for each chunk
                self.send_chunk(ser, chunk)
                # ser.flushInput()  # Clear Arduino buffer
                ser.reset_output_buffer()
                ser.reset_input_buffer()


            ser.write(end_frame)  # Unique end of transmission for the last chunk
            # ser.flushInput()  # Clear Arduino buffer
            ser.reset_output_buffer()
            ser.reset_input_buffer()

    def send_chunk(self, ser, chunk):
        for char in chunk:
            ser.write(char.encode('utf-8'))  # Send the character to Arduino
            self.chat_display.insert(tk.END, char)
            self.chat_display.see(tk.END)
            self.sent_size += 1
            time.sleep(0.007)  # Give Arduino time to process each character
            self.update_progress()
            
        self.root.update()

    def send_chat(self):
        message = self.chat_entry.get()
        if message:
            self.chat_entry.delete(0, tk.END)
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            display_message = f"[{timestamp}] Sender: {message}\n"
            self.chat_display.insert(tk.END, display_message)
            self.chat_display.see(tk.END)
            threading.Thread(target=self.send_chat_message, args=(message,)).start()

    def send_chat_message(self, message):
        with serial.Serial('COM3', 9600, timeout=1) as ser:
            time.sleep(2)  # Wait for the serial connection to initialize
            ser.write(b'\x04')  # Unique start frame for chat
            for char in message:
                ser.write(char.encode('utf-8'))
                time.sleep(0.02)  # Give Arduino time to process each character
            ser.write(b'\x05')  # Unique end frame for chat

    def update_progress(self):
        progress = (self.sent_size / self.total_size) * 100
        self.progress_label.config(text=f"Progress: {progress:.2f}%")

root = tk.Tk()
app = SenderApp(root)
root.mainloop()

receiver:

import tkinter as tk
import serial
import threading
import os
import time
from datetime import datetime
import base64
import os
import zlib  # Import zlib for compression

class ReceiverApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Text File and Chat Receiver")

        self.speed_label = tk.Label(root, text="Receiving Speed: 0 bits/second")
        self.speed_label.pack(pady=10)
        
        self.time_label = tk.Label(root, text="Elapsed Time: 0 seconds")
        self.time_label.pack(pady=10)
        
        self.chat_display = tk.Text(root, height=20, width=50)
        self.chat_display.pack(pady=20)
        
        self.received_data = []
        self.chunk_files = []
        self.start_char_first_chunk = '\x01'  # Unique start of transmission for the first chunk
        self.start_char_chunk = '\x02'  # Start of transmission for each chunk
        self.end_char_last_chunk = '\x03'   # Unique end of transmission for the last chunk
        self.start_char_chat = '\x04'  # Unique start frame for chat
        self.end_char_chat = '\x05'  # Unique end frame for chat
        self.end_image = '\x06'  # Unique end frame for image
        self.end_sound = '\x07'  # Unique end frame for sound
        self.receiving = False
        self.receiving_chat = False
        self.start_time = None
        self.total_bits_received = 0
        
        # Start the receiving thread
        threading.Thread(target=self.receive_file).start()

    def get_unique_filename(self, base_name, extension=".txt"):
        """
        Generate a unique file name by appending a number if the file exists.
        """
        counter = 1
        file_name = f"{base_name}{extension}"
        while os.path.isfile(file_name):
            file_name = f"{base_name}_{counter}{extension}"
            counter += 1
        return file_name

    def receive_file(self):
        base_file_name = "received_chunk"
        
        with serial.Serial('COM5', 2000000, timeout=1) as ser:
            while True:
                if ser.in_waiting > 0:
                    incoming_byte = ser.read().decode('utf-8' , errors='ignore')
                    
                    if incoming_byte == self.start_char_first_chunk:
                        # Clear everything before in memory
                        self.chunk_files = []
                        self.received_data = []
                        self.chat_display.delete(1.0, tk.END)  # Clear chat display
                        self.start_time = time.time()  # Start timing
                        self.total_bits_received = 0  # Reset bit counter

                    if incoming_byte == self.start_char_first_chunk or incoming_byte == self.start_char_chunk:
                        if self.received_data:  # Save previous chunk if exists
                            chunk_file_name = self.get_unique_filename(base_file_name, extension=".chunk")
                            with open(chunk_file_name, 'w') as file:
                                file.write(''.join(self.received_data))
                            self.chunk_files.append(chunk_file_name)
                        ser.flushInput()  # Clear Arduino buffer for the next chunk
                        ser.reset_output_buffer()
                        ser.reset_input_buffer()
                        self.receiving = True
                        self.received_data = []  # Reset received data
                        self.chat_display.delete(1.0, tk.END)  # Clear chat display
                    elif incoming_byte == self.end_char_last_chunk:
                        self.receiving = False
                        # Calculate and display receiving speed and elapsed time
                        elapsed_time = time.time() - self.start_time
                        bit_rate = self.total_bits_received / elapsed_time
                        self.speed_label.config(text=f"Receiving Speed: {bit_rate:.2f} bits/second")
                        self.time_label.config(text=f"Elapsed Time: {elapsed_time:.2f} seconds")
                        # Save the last chunk data to a unique file
                        chunk_file_name = self.get_unique_filename(base_file_name, extension=".chunk")
                        with open(chunk_file_name, 'w') as file:
                            file.write(''.join(self.received_data))
                        self.chunk_files.append(chunk_file_name)
                        ser.flushInput()  # Clear Arduino buffer for the next chunk
                        ser.reset_output_buffer()
                        ser.reset_input_buffer()
                        # Combine all chunk files into one file
                        combined_file_name = self.get_unique_filename("received_text")
                        with open(combined_file_name, 'w') as combined_file:
                            for chunk_file in self.chunk_files:
                                with open(chunk_file, 'r') as file:
                                    combined_file.write(file.read())
                        # Delete all chunk files after combining
                        for chunk_file in self.chunk_files:
                            os.remove(chunk_file)
                        self.decompress_and_save_file(combined_file_name, 'txt')
                    elif incoming_byte == self.end_image:
                        self.receiving = False
                        # Calculate and display receiving speed and elapsed time
                        elapsed_time = time.time() - self.start_time
                        bit_rate = self.total_bits_received / elapsed_time
                        self.speed_label.config(text=f"Receiving Speed: {bit_rate:.2f} bits/second")
                        self.time_label.config(text=f"Elapsed Time: {elapsed_time:.2f} seconds")
                        # Save the last chunk data to a unique file
                        chunk_file_name = self.get_unique_filename(base_file_name, extension=".chunk")
                        with open(chunk_file_name, 'w') as file:
                            file.write(''.join(self.received_data))
                        self.chunk_files.append(chunk_file_name)
                        ser.flushInput()  # Clear Arduino buffer for the next chunk
                        # Combine all chunk files into one file
                        combined_file_name = self.get_unique_filename("received_text")
                        with open(combined_file_name, 'w') as combined_file:
                            for chunk_file in self.chunk_files:
                                with open(chunk_file, 'r') as file:
                                    combined_file.write(file.read())
                        # Delete all chunk files after combining
                        for chunk_file in self.chunk_files:
                            os.remove(chunk_file)

                        #decompression
                        self.decompress_and_save_file(combined_file_name, "png")
                            
                    elif incoming_byte == self.end_sound:
                        self.receiving = False
                        # Calculate and display receiving speed and elapsed time
                        elapsed_time = time.time() - self.start_time
                        bit_rate = self.total_bits_received / elapsed_time
                        self.speed_label.config(text=f"Receiving Speed: {bit_rate:.2f} bits/second")
                        self.time_label.config(text=f"Elapsed Time: {elapsed_time:.2f} seconds")
                        # Save the last chunk data to a unique file
                        chunk_file_name = self.get_unique_filename(base_file_name, extension=".chunk")
                        with open(chunk_file_name, 'w') as file:
                            file.write(''.join(self.received_data))
                        self.chunk_files.append(chunk_file_name)
                        ser.flushInput()  # Clear Arduino buffer for the next chunk
                        # Combine all chunk files into one file
                        combined_file_name = self.get_unique_filename("received_text")
                        with open(combined_file_name, 'w') as combined_file:
                            for chunk_file in self.chunk_files:
                                with open(chunk_file, 'r') as file:
                                    combined_file.write(file.read())
                        # Delete all chunk files after combining
                        for chunk_file in self.chunk_files:
                            os.remove(chunk_file)
                            
                        file_path = combined_file_name
                        # Check if a file was selected
                        if not file_path:
                            print("No file selected.")
                            return
                        
                        self.decompress_and_save_file(combined_file_name, 'mp3')

                    elif incoming_byte == self.start_char_chat:
                        self.receiving_chat = True
                        self.chat_data = []
                    elif incoming_byte == self.end_char_chat:
                        self.receiving_chat = False
                        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                        chat_message = ''.join(self.chat_data)
                        display_message = f"[{timestamp}] Receiver: {chat_message}\n"
                        self.chat_display.insert(tk.END, display_message)
                        self.chat_display.see(tk.END)
                    elif self.receiving:
                        self.received_data.append(incoming_byte)
                        self.chat_display.insert(tk.END, incoming_byte)
                        self.chat_display.see(tk.END)
                        self.root.update()
                        self.total_bits_received += 8  # Increment bit counter by 8 bits (1 byte)
                    elif self.receiving_chat:
                        self.chat_data.append(incoming_byte)

    def decompress_and_save_file(self, file_path, file_type):
        with open(file_path, 'r') as text_file:
            encoded_data = text_file.read()

        decoded_data = base64.b64decode(encoded_data)
        decompressed_data = zlib.decompress(decoded_data)

        file_dir, file_name = os.path.split(file_path)
        file_name_without_ext = os.path.splitext(file_name)[0]

        output_file_path = os.path.join(file_dir, f"{self.get_unique_filename(file_name_without_ext)}.{file_type}")

        with open(output_file_path, 'wb') as output_file:
            output_file.write(decompressed_data)
        
        return output_file_path

root = tk.Tk()
app = ReceiverApp(root)
root.mainloop()

and fortunately I was able to achieve a correct sending and receiving process.
the problem is as follows:

1- when I remove the

            time.sleep(0.007)

from the sender between charachters the sending is not done correctly although it is done correctly in the Arduino serial monitor
and that's the fastest I was able to get (around 1300 bits/second).

2- the second problem and the most frustrating one is when I try to send a small file let's say 2000 characters it is fast and keeps the same speed, when I increase the file size to maybe 10000 characters it works fine for a few seconds and then it start to slow down a ton,

approaches I tried to solve:
1- to cut them into chunks.
2- flush input and reset input and output buffers between chunks as shown in the code (that prevented the receiving to stop entirely but the slowing down is still happening).

please help me I am sending a 15 kb file in around 10 minutes. I want to increase the speed as fast as possible.

maybe bluetooth?

When data is flowing, you are sending a huge number of characters through the Serial port - 8 characters for every character you receive. Even at 2,000,000 BAUD, you will almost certainly fill the transmit buffer, which will cause Serial.print to block until there is an empty spot in the buffer ro put the new character in.

That doesn't sound very efficient.

That's why I am resetting the buffers after each chunk of charachters, is there any other solution to this ?

I think he meant 8 birs / charachter

It's an Optical communication project.

The only "solution" is send less data through Serial. Figure out what your peak input data rate is, and calculate your peak Serial.print data rate is (i.e. - 8X the input rate, since you receive a byte, then translate it into 8 characters going to Serial). The Serial buffer is only 64 characters. So, if you are receiving input characters at full speed, your are at risk of filling the transmit buffer after receving only a bit more than 8 characters.

Why do you need to print the received data as ASCII binary? Simply removing the Serial.print from sendByteAsBits will almost certainly elminate your problem entirely.

No, I think he meant this function.

For every character it receives, it sends one full character for each bit. This function takes in 8 bits and sends out 64.