Exemple #1
0
class Application(cmd.Cmd):
    def __init__(self, transport=None, stdout=None):
        # cmd Initialization and configuration
        cmd.Cmd.__init__(self, stdout=stdout)
        self.intro = 'pytelemetry terminal started.' \
                 + ' (type help for a list of commands.)'
        self.prompt = ':> '
        self.file = None

        # pytelemetry setup
        if not transport:
            self.transport = transports.SerialTransport()
        else:
            self.transport = transport
        self.telemetry = Pytelemetry(self.transport)

        self.topics = Topics()
        self.plots = []
        self.plotsLock = Lock()
        self.runner = Runner(self.transport, self.telemetry, self.plots,
                             self.plotsLock, self.topics)

        self.telemetry.subscribe(None, self.topics.process)

        self.types_lookup = {
            '--s': 'string',
            '--u8': 'uint8',
            '--u16': 'uint16',
            '--u32': 'uint32',
            '--i8': 'int8',
            '--i16': 'int16',
            '--i32': 'int32',
            '--f32': 'float32'
        }
        logger.info("Module path : %s" %
                    os.path.dirname(os.path.realpath(__file__)))
        try:
            logger.info("Module version : %s" % __version__)
        except:
            logger.warning("Module version : not found.")

    def emptyline(self):
        pass  # Override default behavior to repeat last command if empty input

    @docopt_cmd
    def do_serial(self, arg):
        """
List serial ports or connect to one of them.

Usage: serial ((-l | --list) | <port> [options])

Options:
-b X, --bauds X         Connection speed in bauds  [default: 9600]
        """
        if arg['--list'] or arg['-l']:
            self.stdout.write("Available COM ports:\n")
            for port, desc, hid in list_ports.comports():
                self.stdout.write("%s \t %s\n" % (port, desc))
            return

        try:
            self.runner.disconnect()
            logger.warn("User requested connect without desconnecting first.")
        except (IOError, AttributeError) as e:
            logger.warn(
                "Already disconnected. Continuing happily. E : {0}".format(e))
            pass

        self.topics.clear()
        logger.info("Cleared all topics for new session.")

        self.transport.resetStats(averaging_window=10)
        self.runner.resetStats()
        self.telemetry.resetStats()
        logger.info("Cleared all stats for new session.")

        try:
            b = int(arg['--bauds'])
            self.runner.connect(arg['<port>'], b)
        except IOError as e:
            self.stdout.write(
                "Failed to connect to {0} at {1} (bauds).\n".format(
                    arg['<port>'], b))

            logger.warn(
                "Failed to connect to {0} at {1} (bauds). E : \n".format(
                    arg['<port>'], b, e))
        else:
            s = "Connected to {0} at {1} (bauds).\n".format(arg['<port>'], b)
            self.stdout.write(s)
            logger.info(s)

    @docopt_cmd
    def do_print(self, arg):
        """
Prints X last received samples from <topic>.

Usage: print <topic> [options]

Options:
-a, --all        Display all received samples under <topic>
-l X, --limit X  Display X last received samples under <topic> [default: 1]

        """
        topic = arg['<topic>']
        if not self.topics.exists(topic):
            s = "Topic '{0}' unknown. Type 'ls' to list all available topics.\n".format(
                topic)
            self.stdout.write(s)
            logger.warn(s)
            return

        try:
            if arg['--all']:
                amount = 0  # 0 is understood as 'return all samples' by self.topics.samples()
            else:
                amount = int(arg['--limit'])
        except:
            s = "Could not cast --limit = '{0}' to integer. Using 1.\n".format(
                arg['--limit'])
            self.stdout.write(s)
            logger.warn(s)
            amount = 1

        s = self.topics.samples(topic, amount)

        if s is not None:
            for i in s:
                self.stdout.write("{0}\n".format(i))
        else:
            logger.error(
                "Could not retrieve {0} sample(s) under topic '{1}'.\n".format(
                    amount, topic))

    @docopt_cmd
    def do_ls(self, arg):
        """
Prints available topics. Topics are basically labels under which data is available (for display, plot, etc).
Data can come from remote source (a connected embedded device) or the command-line interface itself (reception speed, etc).

Without flags, prints a list of remote topics.

Usage: ls [options]

Options:
-c, --cli       Prints all CLI topics. Use this to display topics for monitoring reception speed, errors amount, etc.
        """
        if arg['--cli']:
            for topic in self.topics.ls(source='cli'):
                self.stdout.write("%s\n" % topic)
            return

        for topic in self.topics.ls(source='remote'):
            self.stdout.write("%s\n" % topic)

    @docopt_cmd
    def do_plot(self, arg):
        """
Plots <topic> in a graph window.

Usage: plot <topic>
        """

        topic = arg['<topic>']

        if not self.topics.exists(topic):
            s = "Topic '{0}' unknown. Type 'ls' to list all available topics.\n".format(
                topic)
            self.stdout.write(s)
            logger.warn(s)
            return

        if self.topics.intransfer(topic):
            s = "Topic '{0}' already plotting.\n".format(topic)
            self.stdout.write(s)
            logger.warn(s)
            return

        has_indexes = self.topics.has_indexed_data(arg['<topic>'])

        if has_indexes:
            plotType = PlotType.indexed
            transferType = "indexed"
        else:
            plotType = PlotType.linear
            transferType = "linear"

        p = Superplot(topic, plotType)
        q, ctrl = p.start()

        # Protect self.plots from modifications from the runner thread
        self.plotsLock.acquire()

        self.plots.append({
            'topic': topic,
            'plot': p,  # Plot handler
            'queue': q,  # Data queue
            'ctrl': ctrl  # Plot control pipe
        })

        self.plotsLock.release()

        self.topics.transfer(topic, q, transfer_type=transferType)

        s = "Plotting '{0}' in mode [{1}].\n".format(topic, transferType)
        logger.info(s)
        self.stdout.write(s)

    @docopt_cmd
    def do_pub(self, arg):
        """
Publishes a (value | string) on <topic>.

Usage: pub (--u8 | --u16 | --u32 | --i8 | --i16 | --i32 | --f32 | --s) <topic> <value>
        """

        if arg['--f32']:
            arg['<value>'] = float(arg['<value>'])
        elif not arg['--s']:
            try:
                arg['<value>'] = int(arg['<value>'])
            except:
                # User most likely entered a float with an integer flag
                inter = float(arg['<value>'])
                rounded = int(inter)

                if isclose(inter, rounded):
                    arg['<value>'] = rounded
                else:
                    s = "Aborted : Wrote decimal number ({0}) with integer flag.".format(
                        arg['<value>'])
                    self.stdout.write(s + "\n")
                    logger.warning(s)
                    return

        subset = {
            k: arg[k]
            for k in ("--u8", "--u16", "--u32", "--i8", "--i16", "--i32",
                      "--f32", "--s")
        }

        valtype = None
        for i, k in subset.items():
            if k:
                valtype = self.types_lookup[i]

        if not valtype:
            logger.error("Payload type [{0}] unkown.".format(arg))
            return

        try:
            self.telemetry.publish(arg['<topic>'], arg['<value>'], valtype)
        except SerialTimeoutException as e:
            self.stdout.write("Pub failed. Connection most likely terminated.")
            logger.error(
                "Pub failed. Connection most likely terminated. exception : %s"
                % e)
            return
        except AttributeError as e:
            self.stdout.write(
                "Pub failed because you are not connected to any device. Connect first using `serial` command."
            )
            logger.warning(
                "Trying to publish while not connected. exception : %s" % e)
            return

        s = "Published on topic '{0}' : {1} [{2}]".format(
            arg['<topic>'], arg['<value>'], valtype)
        self.stdout.write(s + "\n")
        logger.info(s)

    @docopt_cmd
    def do_count(self, arg):
        """
Prints a count of received samples for each topic.

Usage: count
        """
        for topic in self.topics.ls():
            self.stdout.write("{0} : {1}\n".format(topic,
                                                   self.topics.count(topic)))

    @docopt_cmd
    def do_disconnect(self, arg):
        """
Disconnects from any open connection.

Usage: disconnect
        """
        try:
            self.runner.disconnect()
            self.stdout.write("Disconnected.\n")
            logger.info("Disconnected.")

            measures = self.transport.stats()

            for key, item in measures.items():
                logger.info("Raw IO : %s : %s" % (key, item))

            measures = self.runner.stats()

            for key, item in measures.items():
                logger.info("IO speeds : %s : %s" % (key, item))

            measures = self.telemetry.stats()

            for key, item in measures['framing'].items():
                logger.info("Framing : %s : %s" % (key, item))

            for key, item in measures['protocol'].items():
                logger.info("Protocol : %s : %s" % (key, item))

            logger.info("Logged session statistics.")

        except:
            logger.warn("Already disconnected. Continuing happily.")

    @docopt_cmd
    def do_info(self, arg):
        """
Prints out cli.py full path, module version.

Usage: info
        """
        self.stdout.write("- CLI path : %s\n" %
                          os.path.dirname(os.path.realpath(__file__)))
        try:
            self.stdout.write("- version : %s\n" % __version__)
        except:
            self.stdout.write("- version : not found.\n")

    @docopt_cmd
    def do_stats(self, arg):
        """
Displays different metrics about the active transport (ex : serial port).
This allows you to know if for instance corrupted frames are received, what fraction
of the maximum baudrate is being used, etc.

Usage: stats
        """
        measures = self.transport.stats()

        self.stdout.write("Raw IO:\n")
        for key, item in measures.items():
            self.stdout.write("\t%s : %s\n" % (key, item))

        measures = self.runner.stats()

        self.stdout.write("IO speeds:\n")
        for key, item in measures.items():
            self.stdout.write("\t%s : %s\n" % (key, item))

        measures = self.telemetry.stats()

        self.stdout.write("Framing:\n")
        for key, item in measures['framing'].items():
            self.stdout.write("\t%s : %s\n" % (key, item))

        self.stdout.write("Protocol:\n")
        for key, item in measures['protocol'].items():
            self.stdout.write("\t%s : %s\n" % (key, item))

    def do_quit(self, arg):
        """
Exits the terminal application.

Usage: quit
        """
        self.runner.terminate()
        self.do_disconnect("")
        self.stdout.write("Good Bye!\n")
        logger.info("Application quit.")
        exit()
def test_protocol_stats():
    t = transportMock()
    p = Pytelemetry(t)

    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 0
    assert measures["rx_corrupted_crc"] == 0
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # Add a frame inside the transport queue
    t.write(bytearray.fromhex("f70700666f6f0062617247027f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 0
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # replaced CRC '4702' by '4701' to corrupt crc
    t.write(bytearray.fromhex("f70700666f6f0062617247017f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 1
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # replaced byte n3 '00' by '10' to corrupt crc
    t.write(bytearray.fromhex("f70710666f6f0062617247027f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    #crc = crc16(bytearray.fromhex("0900666f6f00626172"))
    #print(crc)
    #crc = struct.pack("<H",crc)
    #print(crc.hex())

    # Replaced header from 0700 to 0900 to corrupt it. Crc is valid to not discard
    t.write(bytearray.fromhex("f70900666f6f006261725fc57f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 1
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    #crc = crc16(bytearray.fromhex("0700666f6f01626172"))
    #print(crc)
    #crc = struct.pack("<H",crc)
    #print(crc.hex())

    # Removed EOL. Crc is valid to not discard
    t.write(bytearray.fromhex("f70700666f6f0162617224417f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 1
    assert measures["rx_corrupted_eol"] == 1
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # Impossible to detect corrupted payload of type string because length is unkown. One more reason to store framesize inside frame

    # Use a frame of type u32 instead
    #crc = crc16(bytearray.fromhex("03006b6c6d6f707100ffffff"))
    #crc = struct.pack("<H",crc)
    #print(crc.hex())

    # Corruped payload by removing third & fourth hex number from end. Crc will pass
    #
    t.write(bytearray.fromhex("f703006b6c6d6f707100fffffff2dd7f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 1
    assert measures["rx_corrupted_eol"] == 1
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 1
    assert measures["tx_encoded_frames"] == 0

    topmeasures = p.stats()

    assert measures["rx_decoded_frames"] == topmeasures['protocol'][
        "rx_decoded_frames"]
    assert measures["rx_corrupted_crc"] == topmeasures['protocol'][
        "rx_corrupted_crc"]
    assert measures["rx_corrupted_header"] == topmeasures['protocol'][
        "rx_corrupted_header"]
    assert measures["rx_corrupted_eol"] == topmeasures['protocol'][
        "rx_corrupted_eol"]
    assert measures["rx_corrupted_topic"] == topmeasures['protocol'][
        "rx_corrupted_topic"]
    assert measures["rx_corrupted_payload"] == topmeasures['protocol'][
        "rx_corrupted_payload"]
    assert measures["tx_encoded_frames"] == topmeasures['protocol'][
        "tx_encoded_frames"]

    p.publish("boo", 123, "uint8")

    # get measurements
    measures = p.stats()

    assert measures['protocol']["rx_decoded_frames"] == 1
    assert measures['protocol']["rx_corrupted_crc"] == 2
    assert measures['protocol']["rx_corrupted_header"] == 1
    assert measures['protocol']["rx_corrupted_eol"] == 1
    assert measures['protocol']["rx_corrupted_topic"] == 0
    assert measures['protocol']["rx_corrupted_payload"] == 1
    assert measures['protocol']["tx_encoded_frames"] == 1

    measures = p.api.delimiter.stats()

    assert measures["rx_processed_bytes"] > 0
    assert measures["rx_discarded_bytes"] == 0
    assert measures["rx_escaped_bytes"] == 0
    assert measures["rx_complete_frames"] > 0
    assert measures["rx_uncomplete_frames"] == 0
    assert measures["tx_processed_bytes"] > 0
    assert measures["tx_encoded_frames"] > 0
    assert measures["tx_escaped_bytes"] == 0

    p.resetStats()
    measures = p.stats()

    assert measures['protocol']["rx_decoded_frames"] == 0
    assert measures['protocol']["rx_corrupted_crc"] == 0
    assert measures['protocol']["rx_corrupted_header"] == 0
    assert measures['protocol']["rx_corrupted_eol"] == 0
    assert measures['protocol']["rx_corrupted_topic"] == 0
    assert measures['protocol']["rx_corrupted_payload"] == 0
    assert measures['protocol']["tx_encoded_frames"] == 0

    assert measures['framing']["rx_processed_bytes"] == 0
    assert measures['framing']["rx_discarded_bytes"] == 0
    assert measures['framing']["rx_escaped_bytes"] == 0
    assert measures['framing']["rx_complete_frames"] == 0
    assert measures['framing']["rx_uncomplete_frames"] == 0
    assert measures['framing']["tx_processed_bytes"] == 0
    assert measures['framing']["tx_encoded_frames"] == 0
    assert measures['framing']["tx_escaped_bytes"] == 0
Exemple #3
0
class Application (cmd.Cmd):
    def __init__(self, transport=None, stdout=None):
        # cmd Initialization and configuration
        cmd.Cmd.__init__(self,stdout=stdout)
        self.intro = 'pytelemetry terminal started.' \
                 + ' (type help for a list of commands.)'
        self.prompt = ':> '
        self.file = None

        # pytelemetry setup
        if not transport:
            self.transport = transports.SerialTransport()
        else:
            self.transport = transport
        self.telemetry = Pytelemetry(self.transport)

        self.topics = Topics()
        self.plots = []
        self.plotsLock = Lock()
        self.runner = Runner(self.transport,
                             self.telemetry,
                             self.plots,
                             self.plotsLock,
                             self.topics)

        self.telemetry.subscribe(None,self.topics.process)

        self.types_lookup = {'--s'    :  'string',
                             '--u8'   :  'uint8',
                             '--u16'  :  'uint16',
                             '--u32'  :  'uint32',
                             '--i8'   :  'int8',
                             '--i16'  :  'int16',
                             '--i32'  :  'int32',
                             '--f32'  :  'float32'}
        logger.info("Module path : %s" % os.path.dirname(os.path.realpath(__file__)))
        try:
            logger.info("Module version : %s" % __version__)
        except:
            logger.warning("Module version : not found.")

    def emptyline(self):
        pass # Override default behavior to repeat last command if empty input

    @docopt_cmd
    def do_serial(self, arg):
        """
List serial ports or connect to one of them.

Usage: serial ((-l | --list) | <port> [options])

Options:
-b X, --bauds X         Connection speed in bauds  [default: 9600]
        """
        if arg['--list'] or arg['-l']:
            self.stdout.write("Available COM ports:\n")
            for port,desc,hid in list_ports.comports():
                self.stdout.write("%s \t %s\n" % (port,desc))
            return

        try:
            self.runner.disconnect()
            logger.warn("User requested connect without desconnecting first.")
        except (IOError,AttributeError) as e:
            logger.warn("Already disconnected. Continuing happily. E : {0}"
                         .format(e))
            pass

        self.topics.clear()
        logger.info("Cleared all topics for new session.")

        self.transport.resetStats(averaging_window=10)
        self.runner.resetStats()
        self.telemetry.resetStats()
        logger.info("Cleared all stats for new session.")

        try:
            b = int(arg['--bauds'])
            self.runner.connect(arg['<port>'],b)
        except IOError as e:
            self.stdout.write("Failed to connect to {0} at {1} (bauds).\n"
                    .format(arg['<port>'],b))

            logger.warn("Failed to connect to {0} at {1} (bauds). E : \n"
                          .format(arg['<port>'],b,e))
        else:
            s = "Connected to {0} at {1} (bauds).\n".format(arg['<port>'],b)
            self.stdout.write(s)
            logger.info(s)

    @docopt_cmd
    def do_print(self, arg):
        """
Prints X last received samples from <topic>.

Usage: print <topic> [options]

Options:
-a, --all        Display all received samples under <topic>
-l X, --limit X  Display X last received samples under <topic> [default: 1]

        """
        topic = arg['<topic>']
        if not self.topics.exists(topic):
            s = "Topic '{0}' unknown. Type 'ls' to list all available topics.\n".format(topic)
            self.stdout.write(s)
            logger.warn(s)
            return

        try:
            if arg['--all']:
                amount = 0 # 0 is understood as 'return all samples' by self.topics.samples()
            else:
                amount = int(arg['--limit'])
        except:
            s = "Could not cast --limit = '{0}' to integer. Using 1.\n".format(arg['--limit'])
            self.stdout.write(s)
            logger.warn(s)
            amount = 1

        s = self.topics.samples(topic,amount)

        if s is not None:
            for i in s:
                self.stdout.write("{0}\n".format(i))
        else:
            logger.error("Could not retrieve {0} sample(s) under topic '{1}'.\n".format(amount,topic))

    @docopt_cmd
    def do_ls(self, arg):
        """
Prints available topics. Topics are basically labels under which data is available (for display, plot, etc).
Data can come from remote source (a connected embedded device) or the command-line interface itself (reception speed, etc).

Without flags, prints a list of remote topics.

Usage: ls [options]

Options:
-c, --cli       Prints all CLI topics. Use this to display topics for monitoring reception speed, errors amount, etc.
        """
        if arg['--cli']:
            for topic in self.topics.ls(source='cli'):
                self.stdout.write("%s\n" % topic)
            return

        for topic in self.topics.ls(source='remote'):
            self.stdout.write("%s\n" % topic)


    @docopt_cmd
    def do_plot(self, arg):
        """
Plots <topic> in a graph window.

Usage: plot <topic>
        """

        topic = arg['<topic>']

        if not self.topics.exists(topic):
            s = "Topic '{0}' unknown. Type 'ls' to list all available topics.\n".format(topic)
            self.stdout.write(s)
            logger.warn(s)
            return

        if self.topics.intransfer(topic):
            s = "Topic '{0}' already plotting.\n".format(topic)
            self.stdout.write(s)
            logger.warn(s)
            return

        has_indexes = self.topics.has_indexed_data(arg['<topic>'])

        if has_indexes:
            plotType = PlotType.indexed
            transferType = "indexed"
        else:
            plotType = PlotType.linear
            transferType = "linear"

        p = Superplot(topic,plotType)
        q, ctrl = p.start()

        # Protect self.plots from modifications from the runner thread
        self.plotsLock.acquire()

        self.plots.append({
            'topic': topic,
            'plot': p,     # Plot handler
            'queue': q,    # Data queue
            'ctrl': ctrl   # Plot control pipe
        })

        self.plotsLock.release()

        self.topics.transfer(topic,q, transfer_type=transferType)

        s = "Plotting '{0}' in mode [{1}].\n".format(topic,transferType)
        logger.info(s)
        self.stdout.write(s)

    @docopt_cmd
    def do_pub(self, arg):
        """
Publishes a (value | string) on <topic>.

Usage: pub (--u8 | --u16 | --u32 | --i8 | --i16 | --i32 | --f32 | --s) <topic> <value>
        """

        if arg['--f32']:
            arg['<value>'] = float(arg['<value>'])
        elif not arg['--s']:
            try:
                arg['<value>'] = int(arg['<value>'])
            except:
                # User most likely entered a float with an integer flag
                inter = float(arg['<value>'])
                rounded = int(inter)

                if isclose(inter,rounded):
                    arg['<value>'] = rounded
                else:
                    s = "Aborted : Wrote decimal number ({0}) with integer flag.".format(arg['<value>'])
                    self.stdout.write(s + "\n")
                    logger.warning(s)
                    return


        subset = {k: arg[k] for k in ("--u8","--u16","--u32","--i8","--i16","--i32","--f32","--s")}

        valtype = None
        for i, k in subset.items():
            if k:
                valtype = self.types_lookup[i]

        if not valtype:
            logger.error(
                "Payload type [{0}] unkown."
                .format(arg))
            return

        try:
            self.telemetry.publish(arg['<topic>'],arg['<value>'],valtype)
        except SerialTimeoutException as e:
            self.stdout.write("Pub failed. Connection most likely terminated.")
            logger.error("Pub failed. Connection most likely terminated. exception : %s" % e)
            return
        except AttributeError as e:
            self.stdout.write("Pub failed because you are not connected to any device. Connect first using `serial` command.")
            logger.warning("Trying to publish while not connected. exception : %s" % e)
            return

        s = "Published on topic '{0}' : {1} [{2}]".format(arg['<topic>'], arg['<value>'],valtype)
        self.stdout.write(s + "\n")
        logger.info(s)

    @docopt_cmd
    def do_count(self, arg):
        """
Prints a count of received samples for each topic.

Usage: count
        """
        for topic in self.topics.ls():
            self.stdout.write("{0} : {1}\n".format(topic, self.topics.count(topic)))

    @docopt_cmd
    def do_disconnect(self, arg):
        """
Disconnects from any open connection.

Usage: disconnect
        """
        try:
            self.runner.disconnect()
            self.stdout.write("Disconnected.\n")
            logger.info("Disconnected.")

            measures = self.transport.stats()

            for key,item in measures.items():
                logger.info("Raw IO : %s : %s" % (key,item))

            measures = self.runner.stats()

            for key,item in measures.items():
                logger.info("IO speeds : %s : %s" % (key,item))

            measures = self.telemetry.stats()

            for key,item in measures['framing'].items():
                logger.info("Framing : %s : %s" % (key,item))

            for key,item in measures['protocol'].items():
                logger.info("Protocol : %s : %s" % (key,item))

            logger.info("Logged session statistics.")


        except:
            logger.warn("Already disconnected. Continuing happily.")

    @docopt_cmd
    def do_info(self, arg):
        """
Prints out cli.py full path, module version.

Usage: info
        """
        self.stdout.write("- CLI path : %s\n" % os.path.dirname(os.path.realpath(__file__)))
        try:
            self.stdout.write("- version : %s\n" % __version__)
        except:
            self.stdout.write("- version : not found.\n")

    @docopt_cmd
    def do_stats(self, arg):
        """
Displays different metrics about the active transport (ex : serial port).
This allows you to know if for instance corrupted frames are received, what fraction
of the maximum baudrate is being used, etc.

Usage: stats
        """
        measures = self.transport.stats()

        self.stdout.write("Raw IO:\n")
        for key,item in measures.items():
            self.stdout.write("\t%s : %s\n" % (key,item))

        measures = self.runner.stats()

        self.stdout.write("IO speeds:\n")
        for key,item in measures.items():
            self.stdout.write("\t%s : %s\n" % (key,item))

        measures = self.telemetry.stats()

        self.stdout.write("Framing:\n")
        for key,item in measures['framing'].items():
            self.stdout.write("\t%s : %s\n" % (key,item))

        self.stdout.write("Protocol:\n")
        for key,item in measures['protocol'].items():
            self.stdout.write("\t%s : %s\n" % (key,item))

    def do_quit(self, arg):
        """
Exits the terminal application.

Usage: quit
        """
        self.runner.terminate()
        self.do_disconnect("")
        self.stdout.write("Good Bye!\n")
        logger.info("Application quit.")
        exit()
def test_protocol_stats():
    t = transportMock()
    p = Pytelemetry(t)

    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 0
    assert measures["rx_corrupted_crc"] == 0
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # Add a frame inside the transport queue
    t.write(bytearray.fromhex("f70700666f6f0062617247027f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 0
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # replaced CRC '4702' by '4701' to corrupt crc
    t.write(bytearray.fromhex("f70700666f6f0062617247017f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 1
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # replaced byte n3 '00' by '10' to corrupt crc
    t.write(bytearray.fromhex("f70710666f6f0062617247027f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 0
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    #crc = crc16(bytearray.fromhex("0900666f6f00626172"))
    #print(crc)
    #crc = struct.pack("<H",crc)
    #print(crc.hex())

    # Replaced header from 0700 to 0900 to corrupt it. Crc is valid to not discard
    t.write(bytearray.fromhex("f70900666f6f006261725fc57f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 1
    assert measures["rx_corrupted_eol"] == 0
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    #crc = crc16(bytearray.fromhex("0700666f6f01626172"))
    #print(crc)
    #crc = struct.pack("<H",crc)
    #print(crc.hex())

    # Removed EOL. Crc is valid to not discard
    t.write(bytearray.fromhex("f70700666f6f0162617224417f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 1
    assert measures["rx_corrupted_eol"] == 1
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 0
    assert measures["tx_encoded_frames"] == 0

    # Impossible to detect corrupted payload of type string because length is unkown. One more reason to store framesize inside frame

    # Use a frame of type u32 instead
    #crc = crc16(bytearray.fromhex("03006b6c6d6f707100ffffff"))
    #crc = struct.pack("<H",crc)
    #print(crc.hex())

    # Corruped payload by removing third & fourth hex number from end. Crc will pass
    #
    t.write(bytearray.fromhex("f703006b6c6d6f707100fffffff2dd7f"))
    # update to read the new frame
    p.update()
    # get measurements
    measures = p.api.stats()

    assert measures["rx_decoded_frames"] == 1
    assert measures["rx_corrupted_crc"] == 2
    assert measures["rx_corrupted_header"] == 1
    assert measures["rx_corrupted_eol"] == 1
    assert measures["rx_corrupted_topic"] == 0
    assert measures["rx_corrupted_payload"] == 1
    assert measures["tx_encoded_frames"] == 0

    topmeasures = p.stats()

    assert measures["rx_decoded_frames"] == topmeasures['protocol']["rx_decoded_frames"]
    assert measures["rx_corrupted_crc"] == topmeasures['protocol']["rx_corrupted_crc"]
    assert measures["rx_corrupted_header"] == topmeasures['protocol']["rx_corrupted_header"]
    assert measures["rx_corrupted_eol"] == topmeasures['protocol']["rx_corrupted_eol"]
    assert measures["rx_corrupted_topic"] == topmeasures['protocol']["rx_corrupted_topic"]
    assert measures["rx_corrupted_payload"] == topmeasures['protocol']["rx_corrupted_payload"]
    assert measures["tx_encoded_frames"] == topmeasures['protocol']["tx_encoded_frames"]

    p.publish("boo", 123, "uint8")

    # get measurements
    measures = p.stats()

    assert measures['protocol']["rx_decoded_frames"] == 1
    assert measures['protocol']["rx_corrupted_crc"] == 2
    assert measures['protocol']["rx_corrupted_header"] == 1
    assert measures['protocol']["rx_corrupted_eol"] == 1
    assert measures['protocol']["rx_corrupted_topic"] == 0
    assert measures['protocol']["rx_corrupted_payload"] == 1
    assert measures['protocol']["tx_encoded_frames"] == 1

    measures = p.api.delimiter.stats()

    assert measures["rx_processed_bytes"] > 0
    assert measures["rx_discarded_bytes"] == 0
    assert measures["rx_escaped_bytes"] == 0
    assert measures["rx_complete_frames"] > 0
    assert measures["rx_uncomplete_frames"] == 0
    assert measures["tx_processed_bytes"] > 0
    assert measures["tx_encoded_frames"] > 0
    assert measures["tx_escaped_bytes"] == 0

    p.resetStats()
    measures = p.stats()

    assert measures['protocol']["rx_decoded_frames"] == 0
    assert measures['protocol']["rx_corrupted_crc"] == 0
    assert measures['protocol']["rx_corrupted_header"] == 0
    assert measures['protocol']["rx_corrupted_eol"] == 0
    assert measures['protocol']["rx_corrupted_topic"] == 0
    assert measures['protocol']["rx_corrupted_payload"] == 0
    assert measures['protocol']["tx_encoded_frames"] == 0

    assert measures['framing']["rx_processed_bytes"] == 0
    assert measures['framing']["rx_discarded_bytes"] == 0
    assert measures['framing']["rx_escaped_bytes"] == 0
    assert measures['framing']["rx_complete_frames"] == 0
    assert measures['framing']["rx_uncomplete_frames"] == 0
    assert measures['framing']["tx_processed_bytes"] == 0
    assert measures['framing']["tx_encoded_frames"] == 0
    assert measures['framing']["tx_escaped_bytes"] == 0