
# Script for reading data from microphones (via I2S bus), storing them into CSV file and sending the file to the server
# v1, 20.4.2024, VUT FEKT Brno, Vaculík Samuel

# --------------------------------------------------
#                     LIBRARIES
# --------------------------------------------------

import os
import sys
import time
import pyaudio
import threading
import csv
import gzip
import shutil
import requests
from requests.auth import HTTPBasicAuth
from requests.exceptions import HTTPError, ConnectionError, Timeout, RequestException
from queue import Queue
from dotenv import load_dotenv
import pandas as pd
import numpy as np


# --------------------------------------------------
#                  AUDIO DEFINITIONS
# --------------------------------------------------

FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 44100
CHUNK = CHANNELS * 16384

p = pyaudio.PyAudio()

# Audio object
audio_stream = p.open(format=FORMAT,
                      channels=CHANNELS,
                      rate=RATE,
                      input=True,
                      input_device_index=1,
                      frames_per_buffer=CHUNK)


# --------------------------------------------------
#                    DEFINITIONS
# --------------------------------------------------

# Object for storing data
data_queue = Queue()

# Create CSV file name
file_name = sys.argv[1]      # Get the name from the main.py script
csv_file = f'/home/pi/BP_Vaculik/Logs/Microphones/Original/mics_{file_name}.csv'
compressed_csv_file = f'/home/pi/BP_Vaculik/Logs/Microphones/Compressed/mics_{file_name}.gz'

# Server url addresses (Only test server is used at this moment)
url_post = 'https://httpbin.org/post'
url_auth = 'https://httpbin.org/basic-auth/user/passwd'
SERVER_SENDING_PERIOD = 5

# Get login data to the server from the '.env' file
dotenv_path = '/home/pi/BP_Vaculik/.env'
load_dotenv(dotenv_path)
server_username = os.getenv('SERVER_USERNAME')
server_password = os.getenv('SERVER_PASSWORD')

# Try to authenticate to the server address. The maximum number of attempts is 10.
for try_number in range(1, 11):
    try:
        response_auth = requests.get(url_auth, auth=HTTPBasicAuth(server_username, server_password))
        response_auth.raise_for_status()

        if response_auth.status_code == 200:
            print('Authentication was successful.')
            break

    except Exception as e:
        print(f'{try_number}. Error during authentication: {e}')
        if try_number < 10:
            time.sleep(1)
        else:
            print('Can not authenticate to the server! The program will be terminated.')
            exit(1)


# --------------------------------------------------
#                     FUNCTIONS
# --------------------------------------------------

# Function for sending the compressed CSV file to the server
def send_file():
    print("Sending to server...")

    # Sending data to server (Test server is used at this moment)
    try:
        with open(compressed_csv_file, 'rb') as file:
            # Prepare compressed file for sending to the server (key/name, actual file, format)
            file_for_send = {'file': (compressed_csv_file.split('/')[-1], file, 'application/gzip')}

            try:
                # Actual sending to the server
                response = requests.post(url_post, files=file_for_send, timeout=5)
                response.raise_for_status()

                print("The compressed file has been successfully sent to the server. (microphones.py)")

            except HTTPError as e:
                print(f'HTTP error when sending a request: {e}')
            except ConnectionError as e:
                print(f'Connection error: {e}')
            except Timeout as e:
                print(f'Request timeout has expired: {e}')
            except RequestException as e:
                print(f'Error when sending a request: {e}')
            except Exception as e:
                print(f'Unexpected error: {e}')

    except FileNotFoundError as e:
        print(f'The file {compressed_csv_file} does not exist: {e}')
    except TypeError as e:
        print(f'Something is wrong with the file {compressed_csv_file}: {e}')
    except OSError as e:
        print(f'Operating system error: {e}')


# Function for compressing the CSV file
def compress_file():
    try:
        with open(csv_file, 'rb') as file_original:
            try:
                with gzip.open(compressed_csv_file, 'wb') as file_compressed:
                    # Copying data between files, data is also compressed at the same time
                    shutil.copyfileobj(file_original, file_compressed)

            except TypeError as e:
                print(f'Something is wrong with the file {compressed_csv_file}: {e}')
                pass
            except PermissionError:
                print(f"You do not have permission to write into the file {compressed_csv_file}!")
                pass
            except IsADirectoryError:
                print(f"The specified path ti the file is a directory! The path is: {compressed_csv_file}")
                pass
            except OSError as e:
                print(f"Operating system error: {e}")
                pass

    except FileNotFoundError as e:
        print(f'File {csv_file} does not exist: {e}')
    except TypeError as e:
        print(f'Something is wrong with the file {csv_file}: {e}')
        pass
    except PermissionError:
        print(f"You do not have permission to read the file {csv_file}!")
        pass
    except IsADirectoryError:
        print(f"The specified path to the file is a directory! The path is: {csv_file}")
        pass
    except OSError as e:
        print(f"Operating system error: {e}")
        pass


# Function to transfer data from queue to the CSV file
def process_data_in_queue():
    timestamp = int((time.time() - start_time) * 1000)

    try:
        with open(csv_file, 'w', newline='') as csvfile:
            writer = csv.writer(csvfile)                                     # Create object for writing data
            writer.writerow(["Timestamp"])                                   # Write first three rows into the CSV file
            writer.writerow([timestamp])
            writer.writerow(["Audio_data"])

            while not data_queue.empty():                                    # Emptying the queue into the CSV file
                data = data_queue.get()
                df = pd.DataFrame(data, columns=['Samples'])
                df.to_csv(csv_file, index=False)

    except TypeError as e:
        print(f'Something is wrong with the file {csv_file}: {e}')
        pass
    except ValueError:
        print(f"The file contains keys that are not in the 'fieldnames' list!")
        pass
    except PermissionError:
        print(f"You do not have permission to write into the file {csv_file}!")
        pass
    except IsADirectoryError:
        print(f"The specified path to the file is a directory! Path: {csv_file}")
        pass
    except OSError as e:
        print(f"Operating system error: {e}")
        pass


# --------------------------------------------------
#                THREAD FUNCTION
# --------------------------------------------------

def periodic_sending_to_the_server():
    while True:
        time.sleep(SERVER_SENDING_PERIOD)
        process_data_in_queue()
        compress_file()
        send_file()


# --------------------------------------------------
#                     MAIN
# --------------------------------------------------

def main():
    try:
        while True:
            # Read one CHUNK and insert it into the queue
            chunk_data = audio_stream.read(CHUNK, exception_on_overflow=False)
            audio_data = np.frombuffer(chunk_data, dtype=np.int16)
            data_queue.put(audio_data)

    except KeyboardInterrupt:
        audio_stream.stop_stream()
        audio_stream.close()
        p.terminate()


# --------------------------------------------------
#                     STARTUP
# --------------------------------------------------

if __name__ == '__main__':
    # Store program start time for timestamps
    start_time = time.time()

    # Sets up an independent loop for sending data to the server
    threading.Thread(target=periodic_sending_to_the_server, daemon=True).start()

    # Actual start of the script
    main()
