"""
Author: Cooper
Date: 29/08/2018

Desc: This is the module that handles all things HANO-1.  It is able to operate in two modes, as a simple HANO-1 encoder
or talk directly to a controller if configured to do so.  Nothing really complicated here apart from dealing with the
various commands that HANO-1 supports.

Connection is via serial port (ttyS1) so this can be used for more general purposes.

Several modifications have been made to HANO-1F to better support Onion communication these include:

#   USB Payloads and Onion Terminal Mode
When the DG3 powers up it will start listening for a ‘poll’ -  that is, a single ‘o’ character sent as HANO-1 at 115200 baud.
The reply to this will be “DG3” sent as HANO-1.
The DG3 will then reboot and configure itself.    – if the DG3 is already configured, it will not reboot.

Xfer.zip File loading:
The DG3 will send the  HANO-1 “Dnnnn”  command to prepare for file load, and continue in the normal way.

Command: o
Responses:
+-------+--------------------------------------+
| Reply |               Meaning                |
+-------+--------------------------------------+
| D     | USB Payload Available                |
| oK!   | Enter Terminal mode (FE HELD)        |
| oK?   | Enter Terminal mode (via lockscreen) |
| <NAK> | None of the above                    |
+-------+--------------------------------------+

#   Terminal Mode Commands
In Terminal mode the Onion can drive the display and read key presses.
Note: The DG3 will still to respond to existing HANO-1 commands like receiving eric.bin, setting destination etc.

+---------+---------------------+
| Command |       Meaning       |
+---------+---------------------+
| oTO     | Enter Terminal Mode |
| oTC     | End Terminal Mode   |
| oDL     | Display Text        |
| oDC     | Clear Display       |
+---------+---------------------+

oDL – Display Line  - oDL [line][align][text]
    e.g.    oDL0Conions!   displays “onions!” on line 0 (top) Centre alignment.
    Lines are 0, 1 2 and Align is ‘L’, ‘R’, ‘C.

oTO -  it should then wait for the DG3 so send a query character (within a second) before issuing display commands.
    CLARIFY!!

#   Terminal Mode Key Presses
This is the mapping for the keys on the EG3 and DG3, with the DG3s keypad, only the first 6 entries in the left column
are relevant.

All key press messages are prefixed with oK

+--------------+-------+--+-----+-------+
|     Key      | Value |  | Key | Value |
+--------------+-------+--+-----+-------+
| Up           | +     |  | D   | D     |
| Down         | -     |  | R   | R     |
| Left         | <     |  | I   | I     |
| Right        | >     |  | X   | X     |
| Ent/FE       | 0x0D  |  | Y   | Y     |
| Ent/FE(Held) | 0X11  |  | 0-9 | 0-9   |
| F            | F     |  |     |       |
+--------------+-------+--+-----+-------+

For some reason the EG3 Onion returns "C" when ENT is held.

The following has been depreciated after version DG3 1.37 (I think it only worked in that version!)

+-----------+-------+
|    Key    | Value |
+-----------+-------+
| Up        | 0x01  |
| Down      | 0x02  |
| Left      | 0x0C  |
| FE        | 0x0A  |
| FE (Held) | 0x??  |   Doesnt appear to work at the moment
+-----------+-------+

# Digital input polling
Command: e
    For Console's digital input status (not the one wired to the onion)


# Dest/Route code polling
It is also possible to tell whether the current dest/route code is set locally or remotely
Command: z? or r?

If manual/local:  zM0001
If auto/remote: zA0001

#   Other Notes


"""
import copy
import re
import threading
import unicodedata
import serial
import time
import os
import logging

from hanip.debug import print_text

class HANO1(object):
    def __init__(self, comport, baud, serEnable, dataDir):
        self.crc16Table = [
            0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
            0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
            0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
            0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
            0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
            0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
            0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
            0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
            0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
            0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
            0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
            0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
            0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
            0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
            0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
            0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
            0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
            0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
            0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
            0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
            0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
            0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
            0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
            0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
            0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
            0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
            0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
            0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
            0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
            0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
            0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
            0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
        ]

        self.dataDir = dataDir

        self.comport = comport
        self.baud = baud
        self.serEnable = serEnable

        self.tlock = threading.Lock()

        self.busy = False  # Whilst the class is busy transferring a database, other requests cannot be handled-
        self.terminal_active = False

        if serEnable:
            print("Initialising serial port")
            self.initSerial()

        if "oniondebug_hano" in os.listdir("/tmp"):
            self.debug = True
        else:
            self.debug = False

    """
    ###################################################################################################################
    Serial Handlers
    """

    def initSerial(self):
        """
        Initialises the serial port for HANO-1 communications
        :return:
        """
        self.ser = serial.Serial(self.comport, self.baud, 8, "N", stopbits=1, timeout=0.5)
        #Do not decrease timeout as it breaks things!
        #Timeout was upped to 0.5 to deal with delays in console replies, originally 0.2

    def reinitSerial(self):
        """
        Reinitialises the serial port for HANO-1 data transfers, because replies may take longer to arrive, the timeout
        has been extended to take that into account.

        It is set to a value of 1 second and should not be changed!
        :return:
        """
        self.closeSerial()
        self.ser = serial.Serial(self.comport, self.baud, 8, "N", stopbits=1, timeout=1)

    def closeSerial(self):
        """
        Closes the serial port
        :return:
        """
        self.ser.close()

    def transmitMessage(self, message, encode, numofbytes):
        """
        The original command, but now checks the busy flag first, this way I don't have to change this entire code to check

        """
        if not self.busy:
            reply = self.actual_transmit_message(message, encode, numofbytes)
            return reply
        else:
            return ""

    def actual_transmit_message(self, message, encode, numofbytes):
        """
        Due to how big this class has gotten, it would be have been too much work to fix RM4608

        Transmits a message AND attempts to get a reply if expected.

        If the message needs putting into a HANO-1 frame, it is, but if
        not it's transmitted as passed in. In both cases, any message is
        encoded to the latin-1 character set, because consoles are fixed
        to that (i.e. no Unicode supported).

        Args:
            message (str): The message to be sent.
            encode (bool): Flag saying whether the message should be HANO-1 encoded.
            numofbytes (int): Number of bytes expected in reply.

        Returns:
            str: Any reply received. If any reply is expected but not received,
                an empty string.
        :return:
        """
        if encode:
            encodedMsg = self.encodeHANO1(message)
        else:
            encodedMsg = message

        try:
            if self.debug:
                print("Hano_Tx: ", print_text.PrintText.to_ascii(encodedMsg))
            self.ser.write(encodedMsg.encode("latin-1"))
        except serial.SerialException as e:
                print(e)
                return ""

        if numofbytes > 0:
            time.sleep(0.05)
            try:
                reply = self.readSerial(numofbytes)
                reply_length = len(reply)
            except serial.SerialException as e:
                print(e)
                return ""
            except TypeError as e:
                print(e)
                return ""

            if self.debug:
                print("Hano_Rx: ", print_text.PrintText.to_ascii(reply), "len:", reply_length)
            if reply_length < 7:
                if self.debug:
                    print("\tRejected reply: Too short")
                return ""

            return reply

    def readSerial(self, numofbytes):
        """
        This method is discriminatory, will read until it sees an 0x03 byte or the amount of bytes to read and return
        the read contents of whichever comes first
        :param numofbytes:
        :return:
        """
        reply = self.ser.read_until(b"\x03", numofbytes)
        return reply.decode("latin-1")

    def read_serial_signdata(self, numofbytes, decode_utf=False):
        """
        This will read a given number of bytes and returns it, it will return less than this if a timeout occurs
        :param numofbytes:
        :return:
        """
        reply = self.ser.read(numofbytes)
        if decode_utf:
            try:
                return reply.decode("utf-8")
            except UnicodeDecodeError:
                return reply.decode("latin-1")
        else:
            return reply.decode("latin-1")

    """
    ###################################################################################################################
    HANO1 Encoding
    """

    def genCRC16(self,data):
        tmpCRC = 0x0000

        for b in data:
            index = ((tmpCRC ^ b) & 0xFF)
            tmpCRC = (tmpCRC >> 8) ^ self.crc16Table[index]

        return tmpCRC

    def encodeHANO1(self, input):
        """
        HANO-1 Protocol starts with STX, followed by length, the data, checksum and ETX
        [stx][len1][len2][data][cs1][cs2][etx]
        length includes everything after [len2]
        checksum is everything between [stx] and [cs1] exclusive
        checksum is the 8-bit XOR
        """

        checksum = 0

        length = len(input) + 3
        LEN1 = ((length & 0xF0) >> 4) | 0x30
        LEN2 = (length & 0x0F) | 0x30
        checksum ^= LEN1
        checksum ^= LEN2

        for x in input:
            checksum ^= ord(x)

        CS1 = ((checksum & 0xF0) >> 4) | 0x30
        CS2 = (checksum & 0x0F) | 0x30

        message = "\x02" + chr(LEN1) + chr(LEN2) + input + chr(CS1) + chr(CS2) + "\x03"

        return message

    def strip_hano_fluff(self, msg):
        """
        Takes out the HANO-1 headers and other stuff that isnt useful to other parts of the application.
        """
        return msg[3:-3]

    """
    ###################################################################################################################
    File sending, formerly Database Sending
    """
    def check_space_available(self, decompressedSize, compressedSize=None):
        """
        This checks that there is enough space on the console to load a file.

        F (filespace) message F loadsizehex,decompressedsizehex where decompressedsize is absent for an uncompressed file.
        eg "1C04,2AA0" tells us the file is 1C04 bytes and decompresses to 2AA0 bytes.
        "F2D56" means an uncompressed file of 2D56 bytes.

        The request replies O if it wll fit and N if not.
        :return: Whether the file will fit, 1 for True, 0 for False
        """

        if compressedSize != None:
            msg = "F%X,%X" % (compressedSize, decompressedSize)
        else:
            msg = "F%X" % decompressedSize

        reply = self.transmitMessage(msg, True, 7)

        if reply[3] == "O":
            return True
        else:
            return False

    def send_file(self, filepath, filetype, blocktype="B"):
        """
        This is the new entry point for sending files to the console, the old way of only dealing with ERIC.BINs has
        been superseded.  The path of the file needs to be provided for this, as well as the file type so that we know
        which prefix to use during the SOT.

        """
        if os.path.isfile(filepath) is False:
            return 1

        sot_prefix_dict = {
            "destination_list": "D",
            "console_config": "DINI",
            "console_firmware": "DFW"
        }

        try:
            sot_prefix = sot_prefix_dict[filetype]
        except KeyError:
            logging.warning("HANO1: Invalid filetype for transfer")
            return 1

        self.busy = True
        self.reinitSerial()

        _blocksize = 1024 if blocktype == "B" else 240
        file_chunks, blockcount = self.split_file(filepath, _blocksize)

        if self.sendSOT(sot_prefix, blockcount):
            return 1

        logging.info("HANO1: Transmitting %s blocks, block type %s" % (blockcount, _blocksize))
        for blocknum, block in enumerate(file_chunks):
            logging.info("Block %04d of %04d" % (blocknum+1, blockcount))
            if blocktype == "B":
                block_msg = self.pack_b_block(blocknum, block)
            else:
                block_msg = self.pack_c_block(blocknum, block)

            if not self.sendBlock(block_msg):
                return 1

        logging.info("Getting checksum")
        reply = self.actual_transmit_message("PC", True, 10)
        # self.printAscii(reply)

        #Send Oui back to console
        reply = self.actual_transmit_message("O", True, 10)

        self.busy = False

        self.closeSerial()
        self.initSerial()

        return 0


    def split_file(self, filepath, blocksize):
        """
        This splits the file up into blocks so that it can be sent over HANO1.
        """
        _file = open(filepath, "rb")
        filecontents = _file.read()

        fileblocks = [filecontents[i:i + blocksize] for i in range(0, len(filecontents), blocksize)]
        blockcount = len(fileblocks)

        return fileblocks, blockcount

    def sendSOT(self, sot_prefix, blockcount):
        """
        Sends start of transfer, this tells the console that a new database is ready to be
        sent.  This will attempt to start the process ten times before quitting.
        """
        SOT_Retries = 10

        logging.info("HANO1: Sending SOT")
        sotmsg = ("%s%04d" % (sot_prefix, blockcount))
        for x in range(0, SOT_Retries):
            reply = self.actual_transmit_message(sotmsg, True, 7)
            try:
                if reply[3] == "O":
                    return 0
            except IndexError:
                continue

        logging.warning("HANO1: Cannot initiate transfer")
        return 1

    def pack_b_block(self, blocknumber, block):
        blockmsg = "B%04d%04d" % (blocknumber + 1, len(block))
        blockmsg = self.encodeHANO1(blockmsg)
        blockcrc = self.genCRC16(block)
        crcBytes = chr(blockcrc & 0xFF) + chr(blockcrc >> 8)

        blockmsg = blockmsg + "%s%s" % (block.decode("latin-1"), crcBytes)

        return blockmsg

    def pack_c_block(self, blocknumber, block):
        blockmsg = "C%04d%s" % (blocknumber + 1, block.decode("latin-1"))
        blockmsg = self.encodeHANO1(blockmsg)

        return blockmsg

    def sendBlock(self, block):
        """
        Transmits the block allowing up to 5 retries if there is a problem transmitting the block
        """
        block_retries = 5

        for x in range(0, block_retries):
            reply = self.actual_transmit_message(block, False, 7)
            try:
                if reply[3] == "\x06":
                    return 1
                else:
                    time.sleep(1)
            except IndexError as e:
                logging.warning("HANO1: Retrying block")
                logging.warning(e)
                time.sleep(0.5)

        return 0


    """
    ###################################################################################################################
    Database Receiving 
    """
    def receive_payload(self, dmsg):
        self.busy = True
        self.actual_transmit_message("O", True, 0)
        payloadsize = int(dmsg[4:8])
        print("Total blocks to receive: %s" % payloadsize)

        checksum = 0
        temp = ""
        blockcount = 0
        maxRetries = 5
        retrycount = 0

        while blockcount < payloadsize:
            for retries in range(maxRetries):
                print()
                #DO NOT CHANGE THIS!!!
                block = self.read_serial_signdata(1041)

                if len(block) < 1:
                    print("Nothing received...")
                    retrycount += 1
                    if retrycount > maxRetries:
                        return 1
                    continue

                parsedBlock = self.parseBlock(block)

                if parsedBlock == 1:
                    print("EH!?")
                    self.actual_transmit_message("\x15", True, 0)
                    time.sleep(0.2)
                    continue

                blockNumber = parsedBlock[0]
                blockData = parsedBlock[1]
                status = parsedBlock[2]

                print("Block %s of %s" % (blockNumber, payloadsize))

                if status:
                    print("\tIssue with block, send NAK")
                    self.actual_transmit_message("\x15", True, 0)
                    retrycount += 1

                    if retrycount > maxRetries:
                        return 1
                else:
                    print("\tBlock OK, send ACK")
                    for x in blockData:
                        checksum ^= ord(x)

                    temp += blockData
                    self.actual_transmit_message("\x06", True, 0)
                    blockcount += 1
                    retrycount = 0
                    break

            time.sleep(0.1)

        print("All blocks received, generating checksum")
        # Checksum request:
        reply = self.readSerial(8)
        print(reply)

        CS1 = ((checksum & 0xF0) >> 4) | 0x30
        CS2 = (checksum & 0x0F) | 0x30

        print("\tPayload checksum: " + chr(CS1) + chr(CS2))
        self.actual_transmit_message("CK" + chr(CS1) + chr(CS2), True, 0)

        reply = self.readSerial(20)
        print(reply)

        print("Transfer complete")
        print("Writing to file...")
        payloadpath = os.path.join("/tmp", "xfer.zip")
        payloadfile = open(payloadpath, "wb")
        payloadfile.write(temp.encode("latin-1"))
        payloadfile.close()

        self.busy = False
        print("File saved")
        return 0

    def parseBlock(self, block):
        try:
            blockType = block[3]

            if blockType == "B":
                blockNumber = block[4:8]
                blockSize = block[8:12]
                blockData = block[15:-2]
                blockCRC = block[-2:]
            elif blockType == "C":
                blockNumber = block[4:8]
                blockSize = None
                blockData = block[8:-2]
                blockCRC = None
            else:
                print("Invalid block: %s" % blockType)
                return 1
        except IndexError:
            print("Invalid block")
            return 1

        print("BlockType %s" % blockType)
        print("BlockNum %s" % blockNumber)
        print("BlockSize %s" % blockSize)

        if blockSize == None or int(blockSize) == len(blockData):
            status = 0
        else:
            print(blockSize, len(blockData), "Error: rcvd size mismatch")
            status = 1

        return blockNumber, blockData, status

    """
    ###################################################################################################################
    Console Commands - Dest/Route/Info/Test
    """
    def getDestCode(self):
        """ Sends 3 queries, one to start or stop Sign Test, one to obtain the
        Route Code and one to obtain the Destination Code.

        Returns:
            (tuple): Destination, Route and Sign Test Mode as a Boolean
        """
        testMode = self.transmitMessage("t", True, 15)
        routeCode = self.transmitMessage("r", True, 15)
        destCode = self.transmitMessage("z", True, 15)

        if "t1" in testMode:
            test_mode = True
        else:
            test_mode = False

        routeCode = routeCode[4:-3].replace(" ", "0")
        destCode = destCode[4:-3]

        return destCode, routeCode, test_mode

    def get_info_code(self):
        """
        gets information from the console Where 99 is a numeric information
        code with 1 to 2 digits 0123456789.
        Return
            info_code:
                information code for the console
        """
        info_code = self.transmitMessage("i", True, 15)
        info_code = info_code[4:6]

        return info_code

    def get_auto_man_status(self):
        """
        Obtains whether a destination code was set manually (via keypad) or remote, it is up to whatever calls this
        routine to decide if the code is manual or auto
        zM0001 for Manual
        zA0001 for Remote (auto)

        I think for now it is only necessary to obtain the method in which the dest code is set, if this changes in
        future then we can include it.  Although it would be odd for one to be set one way and the other another but at
        this point nothing surprises me...
        """
        destCode = self.transmitMessage("z?", True, 16)[4:-3]

        try:
            mode = destCode[0]
        except IndexError:
            mode = "M"
        return mode

    def get_code_validity(self, code):
        """
        Waiting for this to be implemented on the console (CR0183)
        Will ask the console whether a given destination code is valid or not

        “znnn…? – database check on destination code  (just added question mark to end of set dest code)
        Works in RouteBrowse mode if nnnn.. is 8 digits and made up of  zero-padded 4-digit RN + zero-padded 4-digit DN so it forms a database key.
        Replies:
        ‘0’ destination not found
        ‘1’ destination found’
        You would have to give this request enough time to search the database – which might be a few seconds if large.

        """
        if len(code) != 8:
            return None

        msg = "z%s?" % code
        reply = self.transmitMessage(msg, True, 7)

        if len(reply) > 0:
            if "1" in reply[3]:
                return True
            else:
                return False
        else:
            return False

    def setDestCode(self, dest):
        """ Transmits a message to set the Destination code.

        Args:
            dest (str): [description]
        """
        self.transmitMessage("z%s" % dest, True, 10)

    def setRouteCode(self, route):
        """ Transmits a message to set the Routination code.

        Args:
            route (str): [description]
        """
        self.transmitMessage("r%s" % route, True, 10)

    def setInformationCode(self, infocode):
        """
        Transmits a message to the console to update info code
        Args:
            infocode(str): infocode
        """
        self.transmitMessage("i%s" % infocode, True, 10)

    def setTestMode(self, test):
        """
        Transmits a message to the console to set it in testing mode
        """
        print("HANO1: Setting Test Mode %s" %str(test))

        if test:
            test_mode = self.transmitMessage("t%s" % 1, True, 15)
        if not test:
            test_mode = self.transmitMessage("t%s" % 2, True, 15)

        return test_mode

    """
    ###################################################################################################################
    Console Commands - IO
    """
    def obtain_keypress(self):
        """
        gets information from the console Where 99 is a numeric information
        code with 1 to 2 digits 0123456789.
        Return
            info_code:
                information code for the console
        """
        keypresses = self.transmitMessage("o", True, 15)

        if keypresses[3:5] == "oK":
            keypresses = keypresses[5:-3]
        else:
            keypresses = None

        return keypresses

    def sendKeyPress(self, key):
        """Sends a key press to the console"""
        print("Sending console keypress %s" %key)

        self.transmitMessage("oK%s" % key, True, 15)

    def get_digital_inputs(self):
        """ Special command 'e' which obtains the state of the Console's Digital
        Inputs.

        Returns:
            str: An n-digit string where '1' indicates that the input is ACTIVE.
        """
        reply = self.transmitMessage("e", True, 20)
        return reply[4:-3]

    def showOnConsole(self, message, line, duration):
        """
        Shows text on the console via the little 'q' command.
        Duration is binary encoded and is in deci-seconds, e.g. 0x0A is 1 second

        This is similar to the one for the Terminal mode, but they are not interchangeable
        :param message: Message to show on screen
        :param line: Line number, 0, 1 or 2
        :param duration: How long to display the message for
        """
        duration = duration * 10

        if duration > 255:
            duration = 255

        msg = "q%s%s%s" % (line, chr(duration), message)

        try:
            self.transmitMessage(msg, True, 10)
        except:
            print("Hano1: Error showing msg on console")

    def resetConsoleDisplay(self):
        self.transmitMessage("q01", True, 10)

    """
    ###################################################################################################################
    Console Commands - Parameters/Reboot/Factory Reset
    """
    def get_parameter_value(self, parameter):
        """
        This requests the value of a given console parameter

        command: co<parameter>
        """

        msg = "co%s" % parameter
        reply = self.transmitMessage(msg, True, 250)[3:-3]

        if reply == "???":
            return None
        else:
            return reply

    def set_parameter_value(self, new_parameters):
        """
        This sets various console parameters, parameters can be set individually, or multiple(ly).

        Command: co<parameter>=<value> or co<parameter>=<value>,<parameter>=<value>,<parameter>=<value>...

        Note:  Console will reboot upon receiving this command.

        :param new_parameters: A list containing the parameters to be set and their respective values e.g.
                                ["BR=1","T0=30","P2=GTMH-1"]

        """
        command_string = ""

        for command in new_parameters:
            command_string += command + ","

        command_string = command_string.rstrip(",")     #Strip off the last commma

        self.transmitMessage(command_string, True, 0)

    def rebootConsole(self):
        """
        Reboots the console immediately
        """
        self.transmitMessage("!", True, 0)

    def factory_reset_console(self):
        """
        Reboots the console and resets all the parameters back to factory settings, this will delete the installed
        database as well
        :return:
        """
        self.transmitMessage("!F", True, 0)

    """
    ###################################################################################################################
    Console Commands - Ancillary
    """
    def getSoftwareVersion(self):
        """ Obtains the software version.
        Issues the 'S' command, which returns string "Sxxx*x*xx*xx".
        This is re-phrased into the form "MMM m.dd.nn" where MMM is a model
        name (3 chars) and m is Major version, dd is Minor version and nn is
        Patch version.

        Returns:
            (str): rephrased string as described above.
        """
        reply = self.transmitMessage("S", True, 80)

        if len(reply) > 10:
            vers = reply[4:-3]
            model = vers.split("*")[0]
            software_version = model + " " + vers.replace("*", ".")[len(model)+1:]

            return software_version
        else:
            return 1

    def obtain_data_version(self):
        """
        Obtains the current database data version
        """
        c = '\x03'
        et = '04'
        nrsp = " "

        reply = self.transmitMessage("I", True, 64)

        cln_reply = copy.deepcopy(reply)#copy.deepcopy(reply[3:(c_ind - 2)])
        cln_reply = "".join(ch for ch in cln_reply if unicodedata.category(ch)[0]!="C")
        cln_reply = cln_reply[2:-2]
        cln_reply = re.sub(r"[^a-zA-Z0-9 ]", "", cln_reply)

        ret = cln_reply if reply[1:3] != et else None

        return ret

    def get_console_status(self):
        """
        “cc”   - retrieve current cons state
        Replies:
        ‘0’ - IDLE
        ‘1’ – Displaying a destination
        ‘2’ -  Bad Destination
        ‘w’ – Wait – busy updating last destination
        ‘?’ – WTF!  (no database)
        """
        reply = self.transmitMessage("cc", True, 64)
        status = self.strip_hano_fluff(reply)

        return status

    def poll_console(self):
        """ Polls console by sending a single "o" command.

        Attempts to get an indication of certain aspects of the console's status
        by examining the reply to this single character command.
        Replies expected:
            If 'D' is first char of message body, a Database might be available. If
            the expected payload is present, the return value will be 0.
            If a NAK reply (0x15) or no/incomplete reply is received, the return value will be 1.
            If the console is in terminal mode, the return value will be 2

        Returns:
            int: Indication of poll result as described above.
        """
        keypresses = None
        reply = self.transmitMessage("o", True, 20)

        if len(reply) < 4:
            return_code = 1

        elif reply[3] == "\x15":
            self.closeSerial()
            self.initSerial()
            return_code = 1
        else:
            if reply[3] == "D":
                self.reinitSerial()
                print("HANO1: Payload available")
                if self.receive_payload(reply) == 0:
                    return_code = 0
                else:
                    return_code = 1

                self.closeSerial()
                self.initSerial()
            elif "oK!" in reply:
                return_code = 2
            elif "oK?" in reply:
                return_code = 4
            elif "oK" in reply:
                # keypress
                return_code = 3
                keypresses = reply[5:-3]
            else:
                return_code = 1

        return return_code, keypresses

    """
    ###################################################################################################################
    Sign Handling Commands
    """
    def sendSignStatus(self, statusString):
        """ Sends a summary of the current Sign Status to the console as
        a string. Modelled on the way the console displays Sign Status.

        The string contains a place for up to 16 signs. They are implicitly
        numbered by their HCP Address, with each address occupying a position
        in the string calculated as (address -1).
        If a sign is not fitted, the string will contain a '.' at that position.
        Otherwise it will contain a single digit summarising its status, as it appears
        in the HCP Status Query Response.
        If fewer than 16 signs are fitted, the string will be truncated to
        contain only those which are fitted.

        Args:
            statusString (str): String as described above.
        """
        msg = "oS%s" % statusString
        self.transmitMessage(msg, True, 0)

    def obtain_fitted_signs(self):
        """
        This obtains an ASCII byte array depicting which signs are fitted, where the index of the byte gives the HCP
        address of the sign.

        Command: oZc

        Where:
            ‘-’ = Not Fitted
            Number = Sign Mapping

        Example reply (Not encoded in HANO1):
        1,2,3,1,-,-,-,-,-,-,-,-,-,-,-<0x00>

        Note:  This is not the same as the fitted table on the top of an eric.bin
        """
        msg = "oZc"
        reply = self.transmitMessage(msg, True, 32)

        return reply

    def obtain_sign_resolution(self, switch_address):
        """
        This obtains the resolution of a sign at a given switch_address

        Command: ad<sign_address>

        Example response body “ad1120x24”, meaning the dimension of sign at address 1 are 120 wide by 24 high.

        :param hcp_address: The HCP address of the sign where the resolution is required
        :return reply:
        """
        msg = "ad%s" % switch_address
        reply = self.transmitMessage(msg, True, 20)

        return reply

    def obtain_sign_firmware(self, switch_address):
        """
        This obtains the firmware of the sign at a given hcp_address

        Command: av<sign_address>

        Example response body “av0OLED 1.30.03”, meaning the firmware is OLED 1.30.03
        """
        msg = "av%s" % switch_address
        reply = self.transmitMessage(msg, True, 50)

        return reply

    def obtain_sign_content(self, hcp_address):
        """
        This obtains the sign content, where the address given is the HCP address of the sign.
        The data returned is NOT a HANO-1 reply, but a HCP message for that sign.

        The consoles own page timings are not taken into account, whenever data is requested, the console will then ready
        the next page for that given sign only.

        Command: oZ<sign_address>
        """
        msg = "oZ%s" % hcp_address
        self.transmitMessage(msg, True, 0)
        
        reply = ""
        while 1:
            part_reply = self.read_serial_signdata(1000, decode_utf=True)
            if len(part_reply) == 0:
                break
            else:
                reply += part_reply
        # reply = self.read_serial_signdata(5000, decode_utf=True)

        return reply

    def obtain_sign_statuses(self):
        """
        This obtains the sign status that the console has obtained via RS485.  The console will return a string
        of sign status':  <0x02>0<a4444....6=<0x03>

        The index of the value represents a given sign, and the value itself is the sign status.
        :return:
        """

        msg = "a"
        reply = self.transmitMessage(msg, True, 15)
        reply = self.strip_hano_fluff(reply)

        status_list = []

        for index, status in enumerate(reply):
            if index == 0:
                continue
            else:
                status_list.append(status)

        return status_list

    """
    ###################################################################################################################
    Terminal Mode
    """
    def start_terminal_mode(self):
        reply = self.transmitMessage("oTO", True, 20)
        print("Terminal Mode Opened")
        self.terminal_active = True

    def end_terminal_mode(self):
        self.transmitMessage("oTC", True, 0)
        self.terminal_active = False
        print("Terminal Mode Closed")

    def show_on_terminal(self, line, align, text):
        msg = "oDL%s%s%s" % (line, align, text)
        self.transmitMessage(msg, True, 0)
        time.sleep(0.1)

    def clear_terminal(self):
        self.transmitMessage("oDC", True, 0)

    def wait_for_keypress(self, timeout=0):
        """
        Instructs the module to wait for human interaction, there is an option for timeout.
        The timeout for serial port reads is 0.1 seconds
        :param timeout: Time in seconds to wait for input, if 0 this will wait forever
        :return:
        """
        wait_time = time.time()

        while 1:
            reply = self.readSerial(20)

            if len(reply) > 0:
                key_val = self.check_keypress(reply)
                if key_val == 0:
                    print("Invalid keypress received")
                else:
                    print("Valid keypress received")
                    return key_val

            if timeout == 0:
                time.sleep(0.1)
                continue
            else:
                if (time.time() - wait_time) > timeout:
                    return None

    def check_keypress(self, value):
        try:
            key_val = value[5]
            self.printAscii(key_val)
        except IndexError:
            return 0

        nav_keys = ["<", ">", "+", "-", "\x0D", "\x11", "F", "C"]
        nav_keys_legacy = ["\x01", "\x02", "\x0A", "\x0C"]
        dest_keys = ["D", "R", "I", "X", "Y"]
        num_keys = [str(x) for x in range(0, 10)]
        valid_keys = nav_keys + nav_keys_legacy + dest_keys + num_keys

        if key_val in valid_keys:
            return key_val
        else:
            return 0


    """
    ###################################################################################################################
    Debug
    """
    def printAscii(self, stuff):
        print_text.PrintText.print_ascii(stuff)


if __name__ == "__main__":
    import sys

    logging.basicConfig(level=logging.DEBUG)

    if sys.platform == "linux":
        comport = "/dev/ttyS1"
    else:
        comport = "COM9"

    # Change baud rate as needed:
    hano1 = HANO1(comport, 115200, True, "..")

    # Initial test code
    if 0:
        while 1:
            hano1.poll_console()
            hano1.getDestCode()
            hano1.get_info_code()
            hano1.get_auto_man_status()
            hano1.obtain_fitted_signs()

            time.sleep(1)

    # General any old HANO-1 message tests
    if 1:
        while 1:
            entry = input(">: ")
            reply = hano1.transmitMessage(entry, True, 5000)
            hano1.printAscii(reply)
