# pyduino_bridge.py - PyDuino Bridge: Library for transparent bidirectional
# communication between Python and Arduino. Python File.
# PyDuino Bride uses pyserial library. You can install it with: `pip install pyserial`
###############################################################################################################
# Author: Daniel Saromo. Adapted from Robin2 code.
###############################################################################################################
# Description:
#
# The key functions are:
#    write(str) which sends the given string to the Arduino. The string may
#                       contain characters with any of the values 0 to 255
#
#    read()  which returns an array.
#                         The first element contains the number of bytes that the Arduino said it included in
#                             message. This can be used to check that the full message was received.
#                         The second element contains the message as a string
#
#
# The overall process followed by the demo program is as follows
#   open the serial connection to the Arduino - which causes the Arduino to reset
#   wait for a message from the Arduino to give it time to reset
#   loop through a series of test messages
#      send a message and display it on the PC screen
#      wait for a reply and display it on the PC
#
# to facilitate debugging the Arduino code this program interprets any message from the Arduino
#    with the message length set to 0 as a debug message which is displayed on the PC screen
#
# the message to be sent to the Arduino starts with < and ends with > (startMarker and endMarker
# default values)
#    the message content comprises a string, an integer and a float
#    the numbers are sent as their ascii equivalents
#    for example <LED1,200,0.2>
#    this means set the flash interval for LED1 to 200 millisecs
#      and move the servo to 20% of its range
#
# receiving a message from the Arduino involves
#    waiting until the startMarker is detected
#    saving all subsequent bytes until the end marker is detected
#
# NOTES
#       this program does not include any timeouts to deal with delays in communication
#
#       for simplicity the program does NOT search for the comm port - the user must modify the
#         code to include the correct reference.
#         search for the lines
#               serPort = "/dev/ttyS80"
#               baudRate = 9600
#               ser = serial.Serial(serPort, baudRate)
#
# The code is as minimalist as possible, so that you can keep your
# existing python and arduino and use it with the library.
# Adapted from Robin2 code: https://forum.arduino.cc/index.php?topic=225329.msg1810764#msg1810764
# Robin2 modification for Python3: https://forum.arduino.cc/index.php?topic=566897.0
#
# Configuration considerations:
#
# By default, the maximum number of characters allowed in the messages between Arduino and Python is 40.
# If you want to change this buffer size, you have to put the same value in both the Python (change the value `buffSize`
# when using the `begin` method) and the Arduino code (change the line `#define buffSize 40` in the `pyduino_bridge.h` and
# `pyduino_bridge.cpp` files).
#
# Also, verify that the same `numIntValues_FromPy` and `numFloatsValues_FromPy` values are configured in the Python code
# you are using, and also in the Arduino library files (before uploading, change the lines `#define numIntValues_FromPy 1`
# and `#define numFloatValues_FromPy 1` in the `pyduino_bridge.h` and `pyduino_bridge.cpp`). In the Arduino library files,
# this number is set by the `#define` command, since the value representing the number of elements in the array must be a
# constant expression, because arrays are blocks of static memory whose size must be determined at the compilation time,
# before the program runs (for more information, see http://www.cplusplus.com/doc/tutorial/arrays/).
#
# Timeline:
#
# 23 Apr 2020 - DanielSaromo: Created Bridge_py (this Python code) and Bridge_ino (in Arduino code)
# classes to allow high-level use.
###############################################################################################################

__version__ = '1.0.7'

import serial # PyDuino requires the serial library. You can install it with `pip install serial`.
import time

class Bridge_py:
    """This class allows transparent bidirectional communication between Python and Arduino."""
    def __init__(self):
        self.intsRecvd = []
        self.floatsRecvd = []
        self.sleepTime = 5.0

    def begin(self, serPort, baudRate, numIntValues_FromPy, numFloatValues_FromPy,\
        openingMsg = "Arduino is ready", startMarker=ord("<"), endMarker=ord(">"), buffSize=40,
        verbosity=1):
        print("Please verify in the header of the Arduino code that it is expecting to receive",numIntValues_FromPy,"integers and",numFloatValues_FromPy,"floats.")
        print("Verify that you are sending from Python that number of elements mentioned above.")
        """Starts the Python-Arduino serial bridge for serial data transmission."""
        self.ser = serial.Serial(serPort, baudRate)
        print ("PyDuino Bridge opened in port " + serPort + "! Baudrate: " + str(baudRate)+".")
        self.openingMsg = openingMsg

        self.startMarker = startMarker # 60: is the ASCII code of <
        self.endMarker = endMarker   # 62: is the ASCII code of >
        self.buffSize = buffSize
        self.numIntValues_FromPy = numIntValues_FromPy
        self.numFloatValues_FromPy = numFloatValues_FromPy
        self.verbosity = verbosity

        self.waitForArduino()

    def waitForArduino(self):
        """Waits until Arduino sends `openingMsg` (default is: 'Arduino Ready')."""

        # this allows time for Arduino reset. it also ensures that any bytes left
        # over from a previous message are discarded

        msg = ""
        while msg.find(self.openingMsg) == -1:

            while self.ser.inWaiting() == 0:
                pass

            msg = self.read()

            print ("Received from arduino:",msg) # python3 requires parenthesis
            print ()

    def read(self):
        """Receives data from arduino."""

        ck = ""
        x = "z" # any value that is not an end or startMarker
        byteCount = -1 # to allow for the fact that the last increment will be one too many

        # wait for the start character
        while  ord(x) != self.startMarker:
            x = self.ser.read()

        # save data until the end marker is found
        while ord(x) != self.endMarker:
            if ord(x) != self.startMarker:
                ck = ck + x.decode("utf-8") # change for Python3
                byteCount += 1
            x = self.ser.read()

        return(ck)

    def write(self, sendStr):
        """Sends a string to arduino."""
        if not type(sendStr) is str:
            raise TypeError("The message must be a string.")

        assert len(sendStr)<= self.buffSize, "The maximum buffer size (message length) for the string to send to arduino is "+str(self.buffSize)
        self.ser.write(sendStr.encode('utf-8')) # change for Python3

    def writeAndRead_HeaderAndTwoLists(self, header, listIntsFromPython, listFloatsFromPython):
        """Sends to Arduino a list of `self.numIntValues_FromPy` integers and a list of `self.numFloatValues_FromPy` floats.
        Returns from the reply from Arduino: a string header, a list of ints, a list of floats and the `millis` value."""
        assert len(listIntsFromPython)==self.numIntValues_FromPy, "You are expected to send `self.numIntValues_FromPy` integers."
        assert len(listFloatsFromPython)==self.numFloatValues_FromPy, "You are expected to send `self.numIntValues_FromPy` integers."
        
        strListInts = []
        for e in listIntsFromPython:
            if type(e) is int:
                strListInts.append(str(e))
            else:
                raise TypeError("The element "+str(e)+" must be an integer!")

        strListFloats=[]
        for e in listFloatsFromPython:
            if type(e) is float:
                strListFloats.append(str(e))
            else:
                raise TypeError("The element "+str(e)+" must be a float!")

        toSendStr = ",".join([chr(self.startMarker)+str(header)] + strListInts + strListFloats)
        toSendStr += chr(self.endMarker)

        waitingForReply = False
        receivedData = []

        while len(receivedData)==0:

            if waitingForReply == False:
                self.write(toSendStr)
                if(self.verbosity==1): print ("Sent: " + toSendStr)
                waitingForReply = True

            if waitingForReply == True:

                while self.ser.inWaiting() == 0:
                    pass

                dataRecvd = self.read()
                if(self.verbosity==1): print ("Reply received: " + dataRecvd)
                receivedData.append(dataRecvd)
                waitingForReply = False

                if(self.verbosity==1): print ("===========")

        #Until here, `receivedData` is a list with one element: the string received.

        receivedData = receivedData[0]

        numInts = receivedData.count("Int")
        numFloats = receivedData.count("Float")

        receivedData = receivedData.split(" ")
        receivedData.reverse()
        header = receivedData.pop()

        assert header!="Int", "The header must not be \"Int\"!"
        assert header!="Float", "The header must not be \"Float\"!"

        listIntsFromArduino = []
        for _ in range(numInts):
            receivedData.pop()
            listIntsFromArduino.append(int(receivedData.pop()))
        
        listFloatsFromArduino = []
        for _ in range(numFloats):
            receivedData.pop()
            listFloatsFromArduino.append(float(receivedData.pop()))
        
        return header, listIntsFromArduino, listFloatsFromArduino, float(receivedData[0])

    def sleep(self, nap):
        """Waits `nap` seconds. It just uses the `time.sleep` method."""
        time.sleep(nap)

    def setSleepTime(self, sleepTime):
        """Sets the sleep time (in seconds) after each sending and response pair."""
        self.sleepTime = sleepTime

    def setVerbosity(self, newVerbosity):
        """If set to 1, shows in the Python terminal the messages send and received between Arduino and Python.
        If set to 0, hides those messages. The default value is set to 1."""
        self.verbosity = newVerbosity
        print("PyDuino Bridge: Python terminal verbosity set to", self.verbosity,".")

    def writeAndRead_Strings(self, td):#td: transmitted data
        """Sends a list of strings to Arduino and returns the list of string replies."""
        numLoops = len(td)
        waitingForReply = False
        n = 0
        receivedData = []

        while n < numLoops:
            teststr = td[n]

            if waitingForReply == False:
                self.write(teststr)
                if(self.verbosity==1): print ("Sent (str # " + str(n) + "): " + teststr)
                waitingForReply = True

            if waitingForReply == True:

                while self.ser.inWaiting() == 0:
                    pass

                dataRecvd = self.read()
                if(self.verbosity==1): print ("Reply received: " + dataRecvd)
                receivedData.append(dataRecvd)
                n += 1
                waitingForReply = False

                if(self.verbosity==1): print ("===========")

            time.sleep(self.sleepTime)

        return receivedData

    def close(self):
        """Closes the serial bridge between Python and Arduino."""
        self.ser.close()
        print("PyDuino Bridge closed!")
