├── .gitattributes ├── .gitignore ├── .travis.yml ├── DESCRIPTION.rst ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── appveyor.yml ├── console.png ├── dev-requirements.txt ├── graph.png ├── overview.png ├── overview.svg ├── pytelemetrycli ├── __init__.py ├── cli.py ├── example.py ├── initialization.py ├── runner.py ├── test │ ├── test_cli.py │ └── test_topics.py ├── tools.py ├── topics.py └── ui │ ├── __init__.py │ └── superplot.py ├── setup.cfg └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.dll 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # ========================= 62 | # Operating System Files 63 | # ========================= 64 | 65 | # OSX 66 | # ========================= 67 | 68 | .DS_Store 69 | .AppleDouble 70 | .LSOverride 71 | 72 | # Thumbnails 73 | ._* 74 | 75 | # Files that might appear in the root of a volume 76 | .DocumentRevisions-V100 77 | .fseventsd 78 | .Spotlight-V100 79 | .TemporaryItems 80 | .Trashes 81 | .VolumeIcon.icns 82 | 83 | # Directories potentially created on remote AFP share 84 | .AppleDB 85 | .AppleDesktop 86 | Network Trash Folder 87 | Temporary Items 88 | .apdisk 89 | 90 | # Windows 91 | # ========================= 92 | 93 | # Windows image file caches 94 | Thumbs.db 95 | ehthumbs.db 96 | 97 | # Folder config file 98 | Desktop.ini 99 | 100 | # Recycle Bin used on file shares 101 | $RECYCLE.BIN/ 102 | 103 | # Windows Installer files 104 | *.cab 105 | *.msi 106 | *.msm 107 | *.msp 108 | 109 | # Windows shortcuts 110 | *.lnk 111 | 112 | # git repos 113 | # ===================== 114 | pytelemetry/ 115 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - os: linux 5 | python: '3.3' 6 | - os: linux 7 | python: '3.4' 8 | - os: linux 9 | python: '3.5' 10 | - os: linux 11 | python: '3.6' 12 | # command to install dependencies 13 | install: 14 | - sudo apt-get update 15 | # We do this conditionally because it saves us some downloading if the 16 | # version is the same. 17 | - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then 18 | wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; 19 | else 20 | wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 21 | fi 22 | - bash miniconda.sh -b -p $HOME/miniconda 23 | - export PATH="$HOME/miniconda/bin:$PATH" 24 | - hash -r 25 | - conda config --set always_yes yes --set changeps1 no 26 | - conda update -q conda 27 | # Useful for debugging any issues with conda 28 | - conda info -a 29 | 30 | # Replace dep1 dep2 ... with your dependencies 31 | - conda install numpy -y -q 32 | - conda install pyqt -y -q 33 | - conda install pip -y -q 34 | - pip install -r dev-requirements.txt 35 | - export PYTHONPATH="$PYTHONPATH:$TRAVIS_BUILD_DIR" 36 | 37 | # command to run tests 38 | script: 39 | - py.test -v --cov --timeout=100 40 | -------------------------------------------------------------------------------- /DESCRIPTION.rst: -------------------------------------------------------------------------------- 1 | Pytelemetry Command Line Interface 2 | ==================================== 3 | 4 | This module is a powerful command line interface for extremely fast debugging and communication with embedded systems. 5 | 6 | It allows for plotting embedded device's data on-the-fly, publishing values on any topics, listing serial ports and much more. 7 | 8 | The CLI relies on custom protocol, implemented in Python and C languages. 9 | The main strength of this protocol is strong decoupling between communicating devices, simplicity yet flexibility. 10 | 11 | For instance, the protocol supports sending standard linear data, but also arrays and sparse arrays in a network-friendly manner. 12 | 13 | - `Project page `__ 14 | 15 | .. image:: https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/master/console.png 16 | 17 | .. image:: https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/master/graph.png 18 | 19 | pytelemetry 20 | ============ 21 | 22 | This module is the Python implementation of the communication protocol. 23 | 24 | It can be used directly (without the CLI) to script communications with an embedded device. 25 | 26 | .. code:: python 27 | 28 | from pytelemetry import Pytelemetry 29 | from pytelemetry.transports.serialtransport import SerialTransport 30 | import time 31 | 32 | transport = SerialTransport() 33 | tlm = Pytelemetry(transport) 34 | transport.connect({port:'com9',baudrate:'9600'}) 35 | 36 | # publish once on topic 'throttle' (a named communication channel) 37 | tlm.publish('throttle',0.8,'float32') 38 | 39 | def printer(topic, data, opts): 40 | print(topic," : ", data) 41 | 42 | # Subscribe a `printer` function called on every frame with topic 'feedback'. 43 | tlm.subscribe("feedback", printer) 44 | 45 | # Update during 3 seconds 46 | timeout = time.time() + 3 47 | while True: 48 | tlm.update() 49 | if time.time() > timeout: 50 | break 51 | 52 | # disconnect 53 | transport.disconnect() 54 | print("Done.") 55 | 56 | Language C implementation 57 | ========================= 58 | 59 | Telemetry is the same protocol implemented in C language. 60 | 61 | - `Project page `__ 62 | 63 | Centralized documentation 64 | ========================= 65 | 66 | The documentation for all three projects is centralized `here `_. 67 | 68 | MIT License, (C) 2015-2016 Rémi Bèges (remi.beges@gmail.com) 69 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Rémi Bèges ( remi beges gmail com ) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.png 3 | include *.svg 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/pytelemetrycli.svg)](https://badge.fury.io/py/pytelemetrycli) 2 | [![Join the chat at https://gitter.im/Overdrivr/pytelemetry](https://badges.gitter.im/Overdrivr/pytelemetry.svg)](https://gitter.im/Overdrivr/pytelemetry?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | *Windows:* [![Build status](https://ci.appveyor.com/api/projects/status/35jkrkiu03dfav9v/branch/master?svg=true)](https://ci.appveyor.com/project/Overdrivr/pytelemetrycli/branch/master) 4 | *Unix* [![Build Status](https://travis-ci.org/Overdrivr/pytelemetrycli.svg?branch=master)](https://travis-ci.org/Overdrivr/pytelemetrycli) 5 | [![Stories in Ready](https://badge.waffle.io/Overdrivr/pytelemetrycli.svg?label=ready&title=Ready)](http://waffle.io/Overdrivr/pytelemetrycli) 6 | 7 | ## pytelemetry command line interface 8 | 9 | This command-line interface (CLI) enables data visualization and communication with embedded platforms. 10 | 11 | * **Fast prototyping and debugging**. Set everything up in a few minutes and start debugging any embedded device efficiently. Forget about `printf`. Forever. 12 | * **Communication-based applications**. Stop re-writing custom protocols for each new project. 13 | * **Real-time update of embedded application parameters**. Tune your application without loosing time compiling & flashing. 14 | * **Data-plotting**. Plot data from the device in no time, with a single command. Time-varying values, arrays and sparse arrays. 15 | * **Reusability**. Highly flexible protocol, loosely coupled to your application. Suited for a wide number of application scenarios. 16 | 17 | `Arduino` and `ARM mbed` are currently officially supported. 18 | 19 | ## interface and plot 20 | Aan example of listing serial ports `ls -s`, connecting to a device through COM20 `serial com20 --bauds 115200`, listing all received topics `ls` and opening a plot on topic touch `plot touch` 21 | 22 | ![Console example](https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/master/console.png) 23 | 24 | ![Plot example](https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/master/graph.png) 25 | 26 | 27 | ## overview 28 | 29 | The CLI provides a set of commands to connect to a device, read, plot, write data on it, log any received and sent data. 30 | 31 | The communication protocol that carry all exchanged information is implemented in Python and C: 32 | * [`pytelemetry`](https://github.com/Overdrivr/pytelemetry)[![PyPI version](https://badge.fury.io/py/pytelemetry.svg)](https://badge.fury.io/py/pytelemetry) for scripting the communication from your PC 33 | * [`telemetry`](https://github.com/Overdrivr/pytelemetry): for enabling communication in the embedded device. 34 | 35 | Officially supported embedded platforms are for now `Arduino` and `Mbed`. 36 | 37 | This CLI runs on Windows, Mac OS and Linux. 38 | 39 | See the [central documentation](https://github.com/Overdrivr/Telemetry/wiki) for installation tutorials and description of the protocol. 40 | 41 | 42 | ## installation 43 | `pytelemetrycli` requires python 3.3+, PyQt4 and numpy. 44 | 45 | ### Windows 46 | It is recommended to download [`numpy`](http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy) and [`PyQt4`](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pyqt4) wheels python packages (courtesy of Christoph Gohlke). 47 | 48 | In case you were wondering, **no** you **don't** have to install Qt. The binary wheel is enough. 49 | 50 | Install with `pip` the downloaded files 51 | 52 | ```bash 53 | pip install numpy-x.xx.x+vanilla-cp3x-none-winxxx.whl 54 | pip install PyQt4-x.xx.x-cp3x-none-winxxx.whl 55 | ``` 56 | 57 | Then, simply install `pytelemetrycli` with pip as usual 58 | 59 | ```bash 60 | pip install pytelemetrycli 61 | ``` 62 | 63 | ### Mac OS 64 | The easiest way to install numpy and PyQt4 seem to be using `homebrew`. 65 | lease note that you should also have installed python 3.5 with homebrew for this to work correctly. 66 | Also, avoid to have another python 3.5 distribution on your system otherwise you will face import issues as well. 67 | 68 | ```bash 69 | brew install python3 70 | brew install pyqt --with-python3 71 | pip3 install pytelemetrycli 72 | ``` 73 | 74 | ### Linux 75 | 76 | The setup used for testing relies on miniconda. 77 | ``` 78 | conda install numpy 79 | conda install pyqt 80 | conda install pip 81 | pip install pytelemetrycli 82 | ``` 83 | However, if you have PyQt4 and numpy already installed in your directory, simply run 84 | ``` 85 | pip install pytelemetrycli 86 | ``` 87 | 88 | 89 | ## List of commands 90 | The command line interface can be started like this 91 | ``` 92 | python3 -m pytelemetrycli.cli 93 | ``` 94 | If everything is installed properly, `:>` should welcome you. 95 | ``` 96 | pytelemetry terminal started. (type help for a list of commands.) 97 | :> _ 98 | ``` 99 | 100 | ### help [command] 101 | Without arguments, you get a list of all available commands. Otherwise the full `command` documentation. 102 | 103 | ### ls 104 | ```bash 105 | Without options, prints a list of all received topics. 106 | With the --serial flag, prints a list of all available COM ports 107 | 108 | Usage: ls [options] 109 | 110 | Options: 111 | -s, --serial Use this flag to print a list of all available serial ports 112 | ``` 113 | 114 | ### serial 115 | ```bash 116 | Connects pytelemetry to the serial port. 117 | 118 | Usage: serial [options] 119 | 120 | Options: 121 | -b X, --bauds X Connection speed in bauds [default: 9600] 122 | ``` 123 | 124 | ### print 125 | ```bash 126 | Prints X last received samples from . 127 | 128 | Usage: print [options] 129 | 130 | Options: 131 | -a X, --amount X Amount of samples to display [default: 1] 132 | ``` 133 | 134 | ### pub 135 | ```bash 136 | Publishes a (value | string) on . 137 | 138 | Usage: pub (--u8 | --u16 | --u32 | --i8 | --i16 | --i32 | --f32 | --s) 139 | ``` 140 | 141 | ### plot 142 | ```bash 143 | Plots in a graph window. 144 | 145 | Usage: plot 146 | ``` 147 | 148 | ### stats 149 | ```bash 150 | Displays different metrics about the active transport (ex : serial port). 151 | This allows you to know if for instance corrupted frames are received, what fraction 152 | of the maximum baudrate is being used, etc. 153 | 154 | Usage: stats 155 | ``` 156 | ### disconnect 157 | 158 | ```bash 159 | Disconnects from any open connection. 160 | 161 | Usage: disconnect 162 | ``` 163 | 164 | ### quit 165 | ```bash 166 | Exits the terminal application. 167 | 168 | Usage: quit 169 | ``` 170 | # Future milestones 171 | 172 | * improve and truly centralize documentation 173 | * export to Excel and CSV and replay command in the CLI for offline inspection. 174 | * support of Matrices, XYZ, and RGB-type codes. 175 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.1.{build}-{branch} 2 | 3 | build: false 4 | 5 | environment: 6 | matrix: 7 | - MINICONDA: "C:\\Miniconda3" 8 | PYTHON_ARCH: "32" 9 | platform: x86 10 | 11 | - MINICONDA: "C:\\Miniconda3" 12 | PYTHON_ARCH: "32" 13 | platform: x64 14 | 15 | - MINICONDA: "C:\\Miniconda3" 16 | PYTHON_ARCH: "64" 17 | platform: x64 18 | 19 | - MINICONDA: "C:\\Miniconda35" 20 | PYTHON_ARCH: "32" 21 | platform: x86 22 | 23 | - MINICONDA: "C:\\Miniconda35" 24 | PYTHON_ARCH: "32" 25 | platform: x64 26 | 27 | - MINICONDA: "C:\\Miniconda35" 28 | PYTHON_ARCH: "64" 29 | platform: x64 30 | 31 | - MINICONDA: "C:\\Miniconda36" 32 | PYTHON_ARCH: "32" 33 | platform: x64 34 | 35 | - MINICONDA: "C:\\Miniconda36" 36 | PYTHON_ARCH: "64" 37 | platform: x64 38 | 39 | init: 40 | - "ECHO %PYTHON_ARCH%" 41 | - "ECHO %PYTHON_VERSION%" 42 | - "ECHO %MINICONDA%" 43 | - "ECHO %APPVEYOR_BUILD_FOLDER%" 44 | - "ECHO %PYTHONPATH%" 45 | 46 | install: 47 | - "%MINICONDA%\\Scripts\\conda install numpy -y -q" 48 | - "%MINICONDA%\\Scripts\\conda install pyqt -y -q" 49 | - "%MINICONDA%\\Scripts\\conda install pip -y -q" 50 | - "cd %APPVEYOR_BUILD_FOLDER%" 51 | - "%MINICONDA%\\Scripts\\pip.exe install -r dev-requirements.txt" 52 | - "SET PYTHONPATH=%PYTHONPATH%;%APPVEYOR_BUILD_FOLDER%" 53 | test_script: 54 | - "%MINICONDA%\\Scripts\\py.test -v --cov --timeout=100" 55 | -------------------------------------------------------------------------------- /console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/8969ab7515341b9821ba70bab93d4db9c6b7ccf5/console.png -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytelemetry>1.1.0 2 | docopt 3 | sortedcontainers 4 | pyqtgraph 5 | pytest-cov 6 | pytest-timeout 7 | -------------------------------------------------------------------------------- /graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/8969ab7515341b9821ba70bab93d4db9c6b7ccf5/graph.png -------------------------------------------------------------------------------- /overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/8969ab7515341b9821ba70bab93d4db9c6b7ccf5/overview.png -------------------------------------------------------------------------------- /overview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 35 | 36 | 44 | 50 | 51 | 60 | 66 | 67 | 75 | 81 | 82 | 90 | 96 | 97 | 105 | 111 | 112 | 121 | 127 | 128 | 137 | 143 | 144 | 153 | 159 | 160 | 169 | 175 | 176 | 185 | 191 | 192 | 200 | 206 | 207 | 216 | 222 | 223 | 224 | 251 | 256 | 257 | 259 | 260 | 262 | image/svg+xml 263 | 265 | 266 | 267 | 268 | 269 | 274 | pytelemetry cli 287 | pytelemetry 300 | 309 | 319 | transport(serial, wifi, bluetooth serial,...) 337 | 346 | 353 | 360 | telemetry(C library) 378 | 387 | userapplication 406 | 414 | 418 | 424 | arduino 436 | 437 | 441 | 444 | 452 | mbed 464 | 465 | 466 | 475 | transportAPI 494 | etc. 508 | 513 | 520 | 529 | 538 | 545 | 552 | :> serial com20 -b 115200:> pub someTopic 0.3 --f32:> plot someData:>_ 584 | 591 | 597 | 602 | 607 | 612 | 617 | 622 | 627 | 632 | 637 | 642 | 647 | 652 | 657 | 662 | 667 | 672 | 677 | 682 | 687 | 692 | 697 | 702 | 707 | 712 | 717 | 722 | 727 | 732 | 737 | 742 | 747 | 752 | 757 | 762 | 769 | 770 | 779 | pytelemetry 793 | 803 | 813 | transport(serial, wifi, bluetooth serial,...) 832 | 842 | 850 | 858 | telemetry(C library) 877 | 887 | userapplication 907 | 916 | 922 | 928 | arduino 940 | 941 | 947 | 950 | 958 | mbed 970 | 971 | 972 | 982 | transport(uart, ...) 1002 | etc. 1017 | 1026 | 1038 | 1050 | pytelemetry 1064 | 1074 | 1083 | 1089 | 1095 | arduino 1107 | 1108 | 1114 | 1117 | 1125 | mbed 1137 | 1138 | 1139 | etc. 1154 | 1163 | 1175 | 1187 | python 1201 | 1211 | 1220 | ['foo', 123] 1234 | ['bar', 4.5e6] 1248 | telemetry 1267 | 1277 | C/C++ 1291 | 1300 | 'foo' 1314 | 'bar' 1328 | 1334 | 1340 | rx: 123 1351 | //your code 1362 | 1363 | 1369 | 1375 | //other code 1386 | rx: 4.5e6 1397 | 1398 | 1407 | ['qux', 'hello!'] 1421 | 1427 | 1433 | //other code 1444 | rx: 'hello!' 1455 | 1456 | 'qux' 1470 | 1479 | 1480 | 1481 | -------------------------------------------------------------------------------- /pytelemetrycli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/8969ab7515341b9821ba70bab93d4db9c6b7ccf5/pytelemetrycli/__init__.py -------------------------------------------------------------------------------- /pytelemetrycli/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import cmd 4 | from docopt import docopt, DocoptExit 5 | from pytelemetry import Pytelemetry 6 | import pytelemetry.transports.serialtransport as transports 7 | from pytelemetrycli.topics import Topics 8 | from pytelemetrycli.runner import Runner 9 | from pytelemetrycli.tools import isclose 10 | from serial.tools import list_ports 11 | from serial import SerialTimeoutException 12 | from pytelemetrycli.ui.superplot import Superplot, PlotType 13 | from threading import Lock 14 | from pytelemetrycli.initialization import init_logging 15 | import logging 16 | from logging import getLogger 17 | import os 18 | 19 | logger = getLogger('cli') 20 | 21 | def docopt_cmd(func): 22 | def fn(self, arg): 23 | try: 24 | if fn.__name__ == "do_pub": 25 | # Fix for negative numbers 26 | opt = docopt(fn.__doc__, arg, options_first=True) 27 | else: 28 | opt = docopt(fn.__doc__, arg) 29 | 30 | except DocoptExit as e: 31 | print('Command is invalid. See :') 32 | print(e) 33 | return 34 | 35 | except SystemExit: 36 | # The SystemExit exception prints the usage for --help 37 | # We do not need to do the print here. 38 | return 39 | 40 | return func(self, opt) 41 | 42 | fn.__name__ = func.__name__ 43 | fn.__doc__ = func.__doc__ 44 | fn.__dict__.update(func.__dict__) 45 | return fn 46 | 47 | class Application (cmd.Cmd): 48 | def __init__(self, transport=None, stdout=None): 49 | # cmd Initialization and configuration 50 | cmd.Cmd.__init__(self,stdout=stdout) 51 | self.intro = 'pytelemetry terminal started.' \ 52 | + ' (type help for a list of commands.)' 53 | self.prompt = ':> ' 54 | self.file = None 55 | 56 | # pytelemetry setup 57 | if not transport: 58 | self.transport = transports.SerialTransport() 59 | else: 60 | self.transport = transport 61 | self.telemetry = Pytelemetry(self.transport) 62 | 63 | self.topics = Topics() 64 | self.plots = [] 65 | self.plotsLock = Lock() 66 | self.runner = Runner(self.transport, 67 | self.telemetry, 68 | self.plots, 69 | self.plotsLock, 70 | self.topics) 71 | 72 | self.telemetry.subscribe(None,self.topics.process) 73 | 74 | self.types_lookup = {'--s' : 'string', 75 | '--u8' : 'uint8', 76 | '--u16' : 'uint16', 77 | '--u32' : 'uint32', 78 | '--i8' : 'int8', 79 | '--i16' : 'int16', 80 | '--i32' : 'int32', 81 | '--f32' : 'float32'} 82 | logger.info("Module path : %s" % os.path.dirname(os.path.realpath(__file__))) 83 | try: 84 | logger.info("Module version : %s" % __version__) 85 | except: 86 | logger.warning("Module version : not found.") 87 | 88 | def emptyline(self): 89 | pass # Override default behavior to repeat last command if empty input 90 | 91 | @docopt_cmd 92 | def do_serial(self, arg): 93 | """ 94 | List serial ports or connect to one of them. 95 | 96 | Usage: serial ((-l | --list) | [options]) 97 | 98 | Options: 99 | -b X, --bauds X Connection speed in bauds [default: 9600] 100 | """ 101 | if arg['--list'] or arg['-l']: 102 | self.stdout.write("Available COM ports:\n") 103 | for port,desc,hid in list_ports.comports(): 104 | self.stdout.write("%s \t %s\n" % (port,desc)) 105 | return 106 | 107 | try: 108 | self.runner.disconnect() 109 | logger.warn("User requested connect without desconnecting first.") 110 | except (IOError,AttributeError) as e: 111 | logger.warn("Already disconnected. Continuing happily. E : {0}" 112 | .format(e)) 113 | pass 114 | 115 | self.topics.clear() 116 | logger.info("Cleared all topics for new session.") 117 | 118 | self.transport.resetStats(averaging_window=10) 119 | self.runner.resetStats() 120 | self.telemetry.resetStats() 121 | logger.info("Cleared all stats for new session.") 122 | 123 | try: 124 | b = int(arg['--bauds']) 125 | self.runner.connect(arg[''],b) 126 | except IOError as e: 127 | self.stdout.write("Failed to connect to {0} at {1} (bauds).\n" 128 | .format(arg[''],b)) 129 | 130 | logger.warn("Failed to connect to {0} at {1} (bauds). E : \n" 131 | .format(arg[''],b,e)) 132 | else: 133 | s = "Connected to {0} at {1} (bauds).\n".format(arg[''],b) 134 | self.stdout.write(s) 135 | logger.info(s) 136 | 137 | @docopt_cmd 138 | def do_print(self, arg): 139 | """ 140 | Prints X last received samples from . 141 | 142 | Usage: print [options] 143 | 144 | Options: 145 | -a, --all Display all received samples under 146 | -l X, --limit X Display X last received samples under [default: 1] 147 | 148 | """ 149 | topic = arg[''] 150 | if not self.topics.exists(topic): 151 | s = "Topic '{0}' unknown. Type 'ls' to list all available topics.\n".format(topic) 152 | self.stdout.write(s) 153 | logger.warn(s) 154 | return 155 | 156 | try: 157 | if arg['--all']: 158 | amount = 0 # 0 is understood as 'return all samples' by self.topics.samples() 159 | else: 160 | amount = int(arg['--limit']) 161 | except: 162 | s = "Could not cast --limit = '{0}' to integer. Using 1.\n".format(arg['--limit']) 163 | self.stdout.write(s) 164 | logger.warn(s) 165 | amount = 1 166 | 167 | s = self.topics.samples(topic,amount) 168 | 169 | if s is not None: 170 | for i in s: 171 | self.stdout.write("{0}\n".format(i)) 172 | else: 173 | logger.error("Could not retrieve {0} sample(s) under topic '{1}'.\n".format(amount,topic)) 174 | 175 | @docopt_cmd 176 | def do_ls(self, arg): 177 | """ 178 | Prints available topics. Topics are basically labels under which data is available (for display, plot, etc). 179 | Data can come from remote source (a connected embedded device) or the command-line interface itself (reception speed, etc). 180 | 181 | Without flags, prints a list of remote topics. 182 | 183 | Usage: ls [options] 184 | 185 | Options: 186 | -c, --cli Prints all CLI topics. Use this to display topics for monitoring reception speed, errors amount, etc. 187 | """ 188 | if arg['--cli']: 189 | for topic in self.topics.ls(source='cli'): 190 | self.stdout.write("%s\n" % topic) 191 | return 192 | 193 | for topic in self.topics.ls(source='remote'): 194 | self.stdout.write("%s\n" % topic) 195 | 196 | 197 | @docopt_cmd 198 | def do_plot(self, arg): 199 | """ 200 | Plots in a graph window. 201 | 202 | Usage: plot 203 | """ 204 | 205 | topic = arg[''] 206 | 207 | if not self.topics.exists(topic): 208 | s = "Topic '{0}' unknown. Type 'ls' to list all available topics.\n".format(topic) 209 | self.stdout.write(s) 210 | logger.warn(s) 211 | return 212 | 213 | if self.topics.intransfer(topic): 214 | s = "Topic '{0}' already plotting.\n".format(topic) 215 | self.stdout.write(s) 216 | logger.warn(s) 217 | return 218 | 219 | has_indexes = self.topics.has_indexed_data(arg['']) 220 | 221 | if has_indexes: 222 | plotType = PlotType.indexed 223 | transferType = "indexed" 224 | else: 225 | plotType = PlotType.linear 226 | transferType = "linear" 227 | 228 | p = Superplot(topic,plotType) 229 | q, ctrl = p.start() 230 | 231 | # Protect self.plots from modifications from the runner thread 232 | self.plotsLock.acquire() 233 | 234 | self.plots.append({ 235 | 'topic': topic, 236 | 'plot': p, # Plot handler 237 | 'queue': q, # Data queue 238 | 'ctrl': ctrl # Plot control pipe 239 | }) 240 | 241 | self.plotsLock.release() 242 | 243 | self.topics.transfer(topic,q, transfer_type=transferType) 244 | 245 | s = "Plotting '{0}' in mode [{1}].\n".format(topic,transferType) 246 | logger.info(s) 247 | self.stdout.write(s) 248 | 249 | @docopt_cmd 250 | def do_pub(self, arg): 251 | """ 252 | Publishes a (value | string) on . 253 | 254 | Usage: pub (--u8 | --u16 | --u32 | --i8 | --i16 | --i32 | --f32 | --s) 255 | """ 256 | 257 | if arg['--f32']: 258 | arg[''] = float(arg['']) 259 | elif not arg['--s']: 260 | try: 261 | arg[''] = int(arg['']) 262 | except: 263 | # User most likely entered a float with an integer flag 264 | inter = float(arg['']) 265 | rounded = int(inter) 266 | 267 | if isclose(inter,rounded): 268 | arg[''] = rounded 269 | else: 270 | s = "Aborted : Wrote decimal number ({0}) with integer flag.".format(arg['']) 271 | self.stdout.write(s + "\n") 272 | logger.warning(s) 273 | return 274 | 275 | 276 | subset = {k: arg[k] for k in ("--u8","--u16","--u32","--i8","--i16","--i32","--f32","--s")} 277 | 278 | valtype = None 279 | for i, k in subset.items(): 280 | if k: 281 | valtype = self.types_lookup[i] 282 | 283 | if not valtype: 284 | logger.error( 285 | "Payload type [{0}] unkown." 286 | .format(arg)) 287 | return 288 | 289 | try: 290 | self.telemetry.publish(arg[''],arg[''],valtype) 291 | except SerialTimeoutException as e: 292 | self.stdout.write("Pub failed. Connection most likely terminated.") 293 | logger.error("Pub failed. Connection most likely terminated. exception : %s" % e) 294 | return 295 | except AttributeError as e: 296 | self.stdout.write("Pub failed because you are not connected to any device. Connect first using `serial` command.") 297 | logger.warning("Trying to publish while not connected. exception : %s" % e) 298 | return 299 | 300 | s = "Published on topic '{0}' : {1} [{2}]".format(arg[''], arg[''],valtype) 301 | self.stdout.write(s + "\n") 302 | logger.info(s) 303 | 304 | @docopt_cmd 305 | def do_count(self, arg): 306 | """ 307 | Prints a count of received samples for each topic. 308 | 309 | Usage: count 310 | """ 311 | for topic in self.topics.ls(): 312 | self.stdout.write("{0} : {1}\n".format(topic, self.topics.count(topic))) 313 | 314 | @docopt_cmd 315 | def do_disconnect(self, arg): 316 | """ 317 | Disconnects from any open connection. 318 | 319 | Usage: disconnect 320 | """ 321 | try: 322 | self.runner.disconnect() 323 | self.stdout.write("Disconnected.\n") 324 | logger.info("Disconnected.") 325 | 326 | measures = self.transport.stats() 327 | 328 | for key,item in measures.items(): 329 | logger.info("Raw IO : %s : %s" % (key,item)) 330 | 331 | measures = self.runner.stats() 332 | 333 | for key,item in measures.items(): 334 | logger.info("IO speeds : %s : %s" % (key,item)) 335 | 336 | measures = self.telemetry.stats() 337 | 338 | for key,item in measures['framing'].items(): 339 | logger.info("Framing : %s : %s" % (key,item)) 340 | 341 | for key,item in measures['protocol'].items(): 342 | logger.info("Protocol : %s : %s" % (key,item)) 343 | 344 | logger.info("Logged session statistics.") 345 | 346 | 347 | except: 348 | logger.warn("Already disconnected. Continuing happily.") 349 | 350 | @docopt_cmd 351 | def do_info(self, arg): 352 | """ 353 | Prints out cli.py full path, module version. 354 | 355 | Usage: info 356 | """ 357 | self.stdout.write("- CLI path : %s\n" % os.path.dirname(os.path.realpath(__file__))) 358 | try: 359 | self.stdout.write("- version : %s\n" % __version__) 360 | except: 361 | self.stdout.write("- version : not found.\n") 362 | 363 | @docopt_cmd 364 | def do_stats(self, arg): 365 | """ 366 | Displays different metrics about the active transport (ex : serial port). 367 | This allows you to know if for instance corrupted frames are received, what fraction 368 | of the maximum baudrate is being used, etc. 369 | 370 | Usage: stats 371 | """ 372 | measures = self.transport.stats() 373 | 374 | self.stdout.write("Raw IO:\n") 375 | for key,item in measures.items(): 376 | self.stdout.write("\t%s : %s\n" % (key,item)) 377 | 378 | measures = self.runner.stats() 379 | 380 | self.stdout.write("IO speeds:\n") 381 | for key,item in measures.items(): 382 | self.stdout.write("\t%s : %s\n" % (key,item)) 383 | 384 | measures = self.telemetry.stats() 385 | 386 | self.stdout.write("Framing:\n") 387 | for key,item in measures['framing'].items(): 388 | self.stdout.write("\t%s : %s\n" % (key,item)) 389 | 390 | self.stdout.write("Protocol:\n") 391 | for key,item in measures['protocol'].items(): 392 | self.stdout.write("\t%s : %s\n" % (key,item)) 393 | 394 | def do_quit(self, arg): 395 | """ 396 | Exits the terminal application. 397 | 398 | Usage: quit 399 | """ 400 | self.runner.terminate() 401 | self.do_disconnect("") 402 | self.stdout.write("Good Bye!\n") 403 | logger.info("Application quit.") 404 | exit() 405 | 406 | # Main function to start from script or from entry point 407 | def pytlm(): 408 | init_logging() 409 | try: 410 | Application().cmdloop() 411 | except SystemExit: 412 | pass 413 | except KeyboardInterrupt: 414 | pass 415 | 416 | if __name__ == '__main__': 417 | pytlm() 418 | -------------------------------------------------------------------------------- /pytelemetrycli/example.py: -------------------------------------------------------------------------------- 1 | import runner 2 | from pytelemetry import Pytelemetry 3 | import pytelemetry.transports.serialtransport as transports 4 | import time 5 | 6 | transport = transports.SerialTransport() 7 | telemetry = Pytelemetry(transport) 8 | app = runner.Runner(transport,telemetry) 9 | 10 | def printer(topic, data, options): 11 | if options: 12 | print(topic,'[',options['index'],"] : ", data) 13 | else: 14 | print(topic," : ", data) 15 | options = dict() 16 | port = "COM20" 17 | bauds = 115200 18 | 19 | app.connect(port,bauds) 20 | 21 | print("Connected.") 22 | 23 | telemetry.subscribe(None, printer) 24 | telemetry.publish('bar',1354,'int32') 25 | time.sleep(3) 26 | 27 | app.terminate() 28 | print("Done.") 29 | -------------------------------------------------------------------------------- /pytelemetrycli/initialization.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger, Formatter, FileHandler, StreamHandler 2 | from logging.handlers import RotatingFileHandler 3 | import logging 4 | import datetime 5 | import os 6 | 7 | def init_logging(): 8 | # Disable default stderr handler 9 | root = getLogger().addHandler(logging.NullHandler()) 10 | 11 | # Format how data will be .. formatted 12 | formatter = Formatter('%(asctime)s | %(levelname)s | %(message)s') 13 | sharedformatter = Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s') 14 | streamformatter = Formatter('%(message)s') 15 | 16 | # Get the loggers used in pytelemetry.telemetry.telemetry file 17 | rx = getLogger('telemetry.rx') 18 | tx = getLogger('telemetry.tx') 19 | topics = getLogger('topics') 20 | cli = getLogger('cli') 21 | 22 | rx.setLevel(logging.DEBUG) 23 | tx.setLevel(logging.DEBUG) 24 | topics.setLevel(logging.DEBUG) 25 | cli.setLevel(logging.DEBUG) 26 | 27 | # Create a handler to save logging output to a file 28 | dateTag = datetime.datetime.now().strftime("%Y-%b-%d_%H-%M-%S") 29 | 30 | # Create a folder to store all logs for the session 31 | os.makedirs('logs/{0}/'.format(dateTag)) 32 | 33 | # Handlers config 34 | rx_handler = FileHandler('logs/{0}/in-{0}.log'.format(dateTag)) 35 | rx_handler.setLevel(logging.DEBUG) 36 | rx_handler.setFormatter(formatter) 37 | 38 | tx_handler = FileHandler('logs/{0}/out-{0}.log'.format(dateTag)) 39 | tx_handler.setLevel(logging.DEBUG) 40 | tx_handler.setFormatter(formatter) 41 | 42 | app_handler = FileHandler('logs/{0}/cli-{0}.log'.format(dateTag)) 43 | app_handler.setLevel(logging.DEBUG) 44 | app_handler.setFormatter(sharedformatter) 45 | 46 | cli_stream_handler = StreamHandler() 47 | cli_stream_handler.setLevel(logging.INFO) 48 | cli_stream_handler.setFormatter(streamformatter) 49 | 50 | # Attach the logger to the handler 51 | rx.addHandler(rx_handler) 52 | tx.addHandler(tx_handler) 53 | topics.addHandler(app_handler) 54 | cli.addHandler(app_handler) 55 | #cli.addHandler(cli_stream_handler) 56 | -------------------------------------------------------------------------------- /pytelemetrycli/runner.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | # Main class 5 | class Runner: 6 | def __init__(self, transport, telemetry, plots, plotsLock, topics): 7 | 8 | self.transport = transport 9 | self.telemetryWrapper = telemetry 10 | self.plots = plots 11 | self.plotsLock = plotsLock 12 | self.topics = topics 13 | 14 | self.thread = None 15 | self.running = threading.Event() 16 | self.running.set() 17 | 18 | self.connected = threading.Event() 19 | self.connected.clear() 20 | 21 | self.resetStats() 22 | 23 | def connect(self,port,bauds): 24 | # Create monitoring topics 25 | self.topics.create("baudspeed",source="cli") 26 | self.topics.create("baudspeed_avg",source="cli") 27 | self.topics.create("rx_in_waiting",source="cli") 28 | self.topics.create("rx_in_waiting_max",source="cli") 29 | self.topics.create("rx_in_waiting_avg",source="cli") 30 | 31 | # Connection options 32 | options = dict() 33 | options['port'] = port 34 | options['baudrate'] = bauds 35 | 36 | self.baudrate = bauds 37 | self.transport.connect(options) 38 | 39 | self._start_thread() 40 | 41 | def _start_thread(self): 42 | self.connected.set() 43 | self.thread = threading.Thread(target=self.run) 44 | self.thread.start() 45 | 46 | def disconnect(self): 47 | self.connected.clear() 48 | self.transport.disconnect() 49 | 50 | def terminate(self): 51 | self.running.clear() 52 | if self.thread: 53 | self.thread.join() 54 | try: 55 | self.transport.disconnect() 56 | except: 57 | pass # Already disconnected 58 | 59 | def stats(self): 60 | return { 61 | "baudspeed" : self.baudspeed, 62 | "baudspeed_avg" : self.baudspeed_avg, 63 | "baudratio" : self.baudspeed / self.baudrate, 64 | "baudratio_avg" : self.baudspeed_avg / self.baudrate 65 | } 66 | 67 | def resetStats(self): 68 | self.baudrate = 1.0 69 | self.baudspeed = 0.0 70 | self.lasttime = time.time() 71 | self.lastamount = 0.0 72 | self.baudspeed_avg = 0.0 73 | 74 | def update(self): 75 | # Update protocol decoding 76 | self.telemetryWrapper.update() 77 | 78 | # Protect the self.plots data structure from 79 | # being modified from the main thread 80 | self.plotsLock.acquire() 81 | 82 | # Poll each poll pipe to see if user closed them 83 | plotToDelete = None 84 | for p, i in zip(self.plots,range(len(self.plots))): 85 | if p['ctrl'].poll(): 86 | if p['ctrl'].recv() == "closing": 87 | plotToDelete = i 88 | break 89 | 90 | # Delete a plot if needed 91 | if plotToDelete is not None: 92 | self.plots[plotToDelete]['ctrl'].close() 93 | topic = self.plots[plotToDelete]['topic'] 94 | self.topics.untransfer(topic) 95 | self.plots.pop(plotToDelete) 96 | 97 | self.plotsLock.release() 98 | 99 | def computeStats(self): 100 | 101 | current = time.time() 102 | difft = current - self.lasttime 103 | 104 | if difft > 0.05 : 105 | self.lasttime = current 106 | 107 | measures = self.transport.stats() 108 | diff = measures['rx_bytes'] - self.lastamount 109 | self.lastamount = measures['rx_bytes'] 110 | 111 | self.baudspeed = diff / difft 112 | 113 | # Compute rolling average baud speed on about 1 second window 114 | n = 20 115 | self.baudspeed_avg = (self.baudspeed + n * self.baudspeed_avg) / (n + 1) 116 | 117 | # Send cli system data to the topics so that they can be plotted. 118 | self.topics.process("baudspeed",self.baudspeed) 119 | self.topics.process("baudspeed_avg",self.baudspeed_avg) 120 | self.topics.process("rx_in_waiting",measures['rx_in_waiting']) 121 | self.topics.process("rx_in_waiting_max",measures['rx_in_waiting_max']) 122 | self.topics.process("rx_in_waiting_avg",measures['rx_in_waiting_avg']) 123 | 124 | 125 | def run(self): 126 | while self.running.is_set(): 127 | if self.connected.is_set(): 128 | self.update() 129 | self.computeStats() 130 | else: 131 | time.sleep(0.5) 132 | -------------------------------------------------------------------------------- /pytelemetrycli/test/test_cli.py: -------------------------------------------------------------------------------- 1 | from pytelemetrycli.cli import Application 2 | import pytest 3 | from unittest.mock import MagicMock 4 | import cmd 5 | import io 6 | import sys 7 | import queue 8 | 9 | class TransportMock: 10 | def __init__(self): 11 | self.q = queue.Queue() 12 | self.canConnect = False 13 | self.counter = 0 14 | def authorizeConnect(self,value): 15 | self.canConnect = value 16 | def connect(self,options): 17 | print("TransportMock trying to connect") 18 | if not self.canConnect: 19 | raise IOError("TransportMock denied connection") 20 | print("TransportMock connected") 21 | def disconnect(self): 22 | pass 23 | def read(self, maxbytes=1): 24 | amount = maxbytes if self.q.qsize() > maxbytes else self.q.qsize() 25 | self.counter += amount 26 | data = [] 27 | for i in range(amount): 28 | data.append(self.q.get()) 29 | return data 30 | def readable(self): 31 | return self.q.qsize() 32 | def write(self, data): 33 | for c in data: 34 | self.q.put(c) 35 | def writeable(self): 36 | return True 37 | def resetStats(self,averaging_window=1): 38 | self.counter = 0 39 | def stats(self): 40 | return { 41 | "rx_bytes": self.counter, 42 | "tx_bytes" : 0, 43 | "rx_chunks" : 0, 44 | "tx_chunks" : 0, 45 | "rx_in_waiting" : 0, 46 | "rx_in_waiting_avg" : 0, 47 | "rx_in_waiting_max" : 0 48 | } 49 | 50 | def clear(stream): 51 | stream.truncate(0) 52 | stream.seek(0) 53 | 54 | def test_pub_ls(): 55 | tr = TransportMock() 56 | outstream = io.StringIO() 57 | tlm = Application(transport=tr,stdout=outstream) 58 | 59 | tlm.onecmd("ls") 60 | tlm.runner.update() 61 | assert outstream.getvalue() == "" 62 | 63 | clear(outstream) 64 | 65 | tlm.onecmd("pub --f32 topicA 0.4") 66 | tlm.runner.update() 67 | assert outstream.getvalue() == "Published on topic 'topicA' : 0.4 [float32]\n" 68 | 69 | clear(outstream) 70 | 71 | tlm.onecmd("ls") 72 | tlm.runner.update() 73 | assert outstream.getvalue() == "topicA\n" 74 | 75 | clear(outstream) 76 | 77 | tlm.onecmd("pub --f32 topicB 0.4") 78 | tlm.runner.update() 79 | assert outstream.getvalue() == "Published on topic 'topicB' : 0.4 [float32]\n" 80 | 81 | clear(outstream) 82 | 83 | tlm.onecmd("ls") 84 | tlm.runner.update() 85 | assert outstream.getvalue() == "topicA\ntopicB\n" 86 | 87 | clear(outstream) 88 | 89 | tlm.onecmd("pub --i16 topicC 0.0") # Casting from float to int generates negligeable error. Publish will succeed 90 | tlm.runner.update() 91 | assert outstream.getvalue() == "Published on topic 'topicC' : 0 [int16]\n" 92 | 93 | clear(outstream) 94 | 95 | tlm.onecmd("pub --i16 topicC 0.1") # Casting from float to int generates non-negligeable error. Publish will fail 96 | tlm.runner.update() 97 | assert outstream.getvalue() == "Aborted : Wrote decimal number (0.1) with integer flag.\n" 98 | 99 | clear(outstream) 100 | 101 | def test_connect_fail(): 102 | tr = TransportMock() 103 | outstream = io.StringIO() 104 | tlm = Application(transport=tr,stdout=outstream) 105 | 106 | tlm.onecmd("serial com123") 107 | tlm.runner.update() 108 | assert outstream.getvalue() == "Failed to connect to com123 at 9600 (bauds).\n" 109 | 110 | clear(outstream) 111 | 112 | tlm.onecmd("serial com123 -b 115200") 113 | tlm.runner.update() 114 | assert outstream.getvalue() == "Failed to connect to com123 at 115200 (bauds).\n" 115 | 116 | clear(outstream) 117 | 118 | tlm.onecmd("serial com123 --bauds 57600") 119 | tlm.runner.update() 120 | assert outstream.getvalue() == "Failed to connect to com123 at 57600 (bauds).\n" 121 | 122 | clear(outstream) 123 | 124 | def test_print(): 125 | tr = TransportMock() 126 | outstream = io.StringIO() 127 | tlm = Application(transport=tr,stdout=outstream) 128 | 129 | tlm.onecmd("pub --i32 foo 2") 130 | tlm.runner.update() 131 | assert outstream.getvalue() == "Published on topic 'foo' : 2 [int32]\n" 132 | 133 | clear(outstream) 134 | 135 | tlm.onecmd("pub --i32 foo 3") 136 | tlm.runner.update() 137 | assert outstream.getvalue() == "Published on topic 'foo' : 3 [int32]\n" 138 | 139 | clear(outstream) 140 | 141 | tlm.onecmd("pub --s hello world") 142 | tlm.runner.update() 143 | assert outstream.getvalue() == "Published on topic 'hello' : world [string]\n" 144 | 145 | clear(outstream) 146 | 147 | tlm.onecmd("pub --i32 foo 4") 148 | tlm.runner.update() 149 | assert outstream.getvalue() == "Published on topic 'foo' : 4 [int32]\n" 150 | 151 | clear(outstream) 152 | 153 | tlm.onecmd("print foo") 154 | tlm.runner.update() 155 | assert outstream.getvalue() == "4\n" 156 | 157 | clear(outstream) 158 | 159 | tlm.onecmd("print foo -l 3") 160 | tlm.runner.update() 161 | assert outstream.getvalue() == "2\n3\n4\n" 162 | 163 | clear(outstream) 164 | 165 | tlm.onecmd("print foo -l 2") 166 | tlm.runner.update() 167 | assert outstream.getvalue() == "3\n4\n" 168 | 169 | clear(outstream) 170 | 171 | tlm.onecmd("print foo --limit 3") 172 | tlm.runner.update() 173 | assert outstream.getvalue() == "2\n3\n4\n" 174 | 175 | clear(outstream) 176 | 177 | tlm.onecmd("print foo --limit 10") 178 | tlm.runner.update() 179 | assert outstream.getvalue() == "2\n3\n4\n" 180 | 181 | clear(outstream) 182 | 183 | tlm.onecmd("print foo -a") 184 | tlm.runner.update() 185 | assert outstream.getvalue() == "2\n3\n4\n" 186 | 187 | clear(outstream) 188 | 189 | tlm.onecmd("print qux") 190 | tlm.runner.update() 191 | assert outstream.getvalue() == "Topic 'qux' unknown. Type 'ls' to list all available topics.\n" 192 | 193 | clear(outstream) 194 | 195 | tlm.onecmd("print hello") 196 | tlm.runner.update() 197 | assert outstream.getvalue() == "world\n" 198 | 199 | clear(outstream) 200 | 201 | tlm.onecmd("print foo -l 2.3") 202 | tlm.runner.update() 203 | assert outstream.getvalue() == "Could not cast --limit = '2.3' to integer. Using 1.\n4\n" 204 | 205 | clear(outstream) 206 | 207 | def test_count(): 208 | tr = TransportMock() 209 | outstream = io.StringIO() 210 | tlm = Application(transport=tr,stdout=outstream) 211 | 212 | tlm.onecmd("count") 213 | tlm.runner.update() 214 | assert outstream.getvalue() == "" 215 | 216 | clear(outstream) 217 | 218 | tlm.onecmd("pub --i32 foo 2") 219 | tlm.runner.update() 220 | assert outstream.getvalue() == "Published on topic 'foo' : 2 [int32]\n" 221 | 222 | clear(outstream) 223 | 224 | tlm.onecmd("count") 225 | tlm.runner.update() 226 | assert outstream.getvalue() == "foo : 1\n" 227 | 228 | clear(outstream) 229 | 230 | tlm.onecmd("pub --i32 foo 3") 231 | tlm.runner.update() 232 | assert outstream.getvalue() == "Published on topic 'foo' : 3 [int32]\n" 233 | 234 | clear(outstream) 235 | 236 | tlm.onecmd("count") 237 | tlm.runner.update() 238 | assert outstream.getvalue() == "foo : 2\n" 239 | 240 | clear(outstream) 241 | 242 | tlm.onecmd("pub --f32 bar 4.2") 243 | tlm.runner.update() 244 | assert outstream.getvalue() == "Published on topic 'bar' : 4.2 [float32]\n" 245 | 246 | clear(outstream) 247 | 248 | tlm.onecmd("count") 249 | tlm.runner.update() 250 | print(outstream.getvalue()) 251 | for i in tlm.topics.topic_list.items(): 252 | print(i) 253 | assert outstream.getvalue() == "bar : 1\nfoo : 2\n" 254 | 255 | clear(outstream) 256 | 257 | def test_disconnect_quit(): 258 | tr = TransportMock() 259 | outstream = io.StringIO() 260 | tlm = Application(transport=tr,stdout=outstream) 261 | 262 | tlm.onecmd("disconnect") 263 | assert outstream.getvalue() == "Disconnected.\n" 264 | 265 | clear(outstream) 266 | 267 | pytest.raises(SystemExit, tlm.onecmd, "quit") 268 | assert outstream.getvalue() == "Disconnected.\nGood Bye!\n" 269 | 270 | clear(outstream) 271 | 272 | def test_wrong_command(): 273 | tr = TransportMock() 274 | outstream = io.StringIO() 275 | tlm = Application(transport=tr,stdout=outstream) 276 | 277 | # Just check it doesn't raises 278 | tlm.onecmd("pub foo --i32 123") 279 | 280 | clear(outstream) 281 | 282 | def test_info(): 283 | tr = TransportMock() 284 | outstream = io.StringIO() 285 | tlm = Application(transport=tr,stdout=outstream) 286 | # Just check it doesn't raise 287 | tlm.onecmd("info") 288 | 289 | clear(outstream) 290 | 291 | # issue here 292 | def test_topics_are_cleared_after_reconnect(): 293 | tr = TransportMock() 294 | outstream = io.StringIO() 295 | tlm = Application(transport=tr,stdout=outstream) 296 | tlm.runner._start_thread = MagicMock() # Mock _start_thread to avoid starting thread 297 | tr.authorizeConnect(True) 298 | 299 | tlm.onecmd("serial com123") 300 | 301 | tlm.runner.update() 302 | assert outstream.getvalue() == "Connected to com123 at 9600 (bauds).\n" 303 | 304 | clear(outstream) 305 | 306 | tlm.onecmd("pub --f32 bar 4.2") 307 | tlm.runner.update() 308 | assert outstream.getvalue() == "Published on topic 'bar' : 4.2 [float32]\n" 309 | 310 | clear(outstream) 311 | 312 | tlm.onecmd("count") 313 | tlm.runner.update() 314 | assert outstream.getvalue() == "bar : 1\n" 315 | 316 | clear(outstream) 317 | 318 | tlm.onecmd("disconnect") 319 | assert outstream.getvalue() == "Disconnected.\n" 320 | 321 | clear(outstream) 322 | 323 | tlm.onecmd("count") 324 | tlm.runner.update() 325 | assert outstream.getvalue() == "bar : 1\n" 326 | 327 | clear(outstream) 328 | 329 | tlm.onecmd("ls") 330 | tlm.runner.update() 331 | assert outstream.getvalue() == "bar\n" 332 | 333 | clear(outstream) 334 | 335 | tlm.onecmd("serial com123") 336 | tlm.runner.update() 337 | assert outstream.getvalue() == "Connected to com123 at 9600 (bauds).\n" 338 | 339 | clear(outstream) 340 | 341 | # After the re-connection all previous topics should be cleared 342 | tlm.onecmd("count") 343 | tlm.runner.update() 344 | assert outstream.getvalue() == "" 345 | 346 | clear(outstream) 347 | 348 | tlm.onecmd("ls") 349 | tlm.runner.update() 350 | assert outstream.getvalue() == "" 351 | 352 | clear(outstream) 353 | 354 | # Here too 355 | def test_stats(): 356 | tr = TransportMock() 357 | outstream = io.StringIO() 358 | tlm = Application(transport=tr,stdout=outstream) 359 | tlm.runner._start_thread = MagicMock() # Mock _start_thread to avoid starting thread 360 | 361 | tr.resetStats() 362 | tlm.runner.resetStats() 363 | tlm.telemetry.resetStats() 364 | tlm.onecmd("stats") 365 | 366 | assert "Raw IO:\n" in outstream.getvalue() 367 | assert "\trx_bytes : 0\n" in outstream.getvalue() 368 | assert "IO speeds:\n" in outstream.getvalue() 369 | assert "\tbaudspeed : 0.0\n" in outstream.getvalue() 370 | assert "\tbaudratio : 0.0\n" in outstream.getvalue() 371 | assert "\tbaudratio_avg : 0.0\n" in outstream.getvalue() 372 | assert "\tbaudspeed_avg : 0.0\n" in outstream.getvalue() 373 | assert "Framing:\n" in outstream.getvalue() 374 | assert "\ttx_encoded_frames : 0\n" in outstream.getvalue() 375 | assert "\trx_uncomplete_frames : 0\n" in outstream.getvalue() 376 | assert "\ttx_processed_bytes : 0\n" in outstream.getvalue() 377 | assert "\trx_complete_frames : 0\n" in outstream.getvalue() 378 | assert "\ttx_escaped_bytes : 0\n" in outstream.getvalue() 379 | assert "\trx_discarded_bytes : 0\n" in outstream.getvalue() 380 | assert "\trx_processed_bytes : 0\n" in outstream.getvalue() 381 | assert "\trx_escaped_bytes : 0\n" in outstream.getvalue() 382 | assert "Protocol:\n" in outstream.getvalue() 383 | assert "\ttx_encoded_frames : 0\n" in outstream.getvalue() 384 | assert "\trx_corrupted_header : 0\n" in outstream.getvalue() 385 | assert "\trx_decoded_frames : 0\n" in outstream.getvalue() 386 | assert "\trx_corrupted_payload : 0\n" in outstream.getvalue() 387 | assert "\trx_corrupted_crc : 0\n" in outstream.getvalue() 388 | assert "\trx_corrupted_eol : 0\n" in outstream.getvalue() 389 | assert "\trx_corrupted_topic : 0\n" in outstream.getvalue() 390 | 391 | tlm.onecmd("pub --i32 foo 2") 392 | 393 | clear(outstream) 394 | 395 | tlm.runner.update() 396 | 397 | tlm.onecmd("stats") 398 | 399 | speeds = tlm.runner.stats() 400 | 401 | assert "Raw IO:\n" in outstream.getvalue() 402 | assert "\trx_bytes : 14\n" in outstream.getvalue() 403 | assert "IO speeds:\n" in outstream.getvalue() 404 | assert "\tbaudspeed : {0}\n".format(speeds['baudspeed']) in outstream.getvalue() 405 | assert "\tbaudratio : {0}\n".format(speeds['baudratio']) in outstream.getvalue() 406 | assert "\tbaudratio_avg : {0}\n".format(speeds['baudratio_avg']) in outstream.getvalue() 407 | assert "\tbaudspeed_avg : {0}\n".format(speeds['baudspeed_avg']) in outstream.getvalue() 408 | assert "Framing:\n" in outstream.getvalue() 409 | assert "\ttx_encoded_frames : 1\n" in outstream.getvalue() 410 | assert "\trx_uncomplete_frames : 0\n" in outstream.getvalue() 411 | assert "\ttx_processed_bytes : 12\n" in outstream.getvalue() 412 | assert "\trx_complete_frames : 1\n" in outstream.getvalue() 413 | assert "\ttx_escaped_bytes : 0\n" in outstream.getvalue() 414 | assert "\trx_discarded_bytes : 0\n" in outstream.getvalue() 415 | assert "\trx_processed_bytes : 14\n" in outstream.getvalue() 416 | assert "\trx_escaped_bytes : 0\n" in outstream.getvalue() 417 | assert "Protocol:\n" in outstream.getvalue() 418 | assert "\ttx_encoded_frames : 1\n" in outstream.getvalue() 419 | assert "\trx_corrupted_header : 0\n" in outstream.getvalue() 420 | assert "\trx_decoded_frames : 1\n" in outstream.getvalue() 421 | assert "\trx_corrupted_payload : 0\n" in outstream.getvalue() 422 | assert "\trx_corrupted_crc : 0\n" in outstream.getvalue() 423 | assert "\trx_corrupted_eol : 0\n" in outstream.getvalue() 424 | assert "\trx_corrupted_topic : 0\n" in outstream.getvalue() 425 | 426 | # Check stats are cleaned after restart 427 | tr.authorizeConnect(True) 428 | tlm.onecmd("serial com123") 429 | 430 | clear(outstream) 431 | 432 | tlm.onecmd("stats") 433 | 434 | assert "Raw IO:\n" in outstream.getvalue() 435 | assert "\trx_bytes : 0\n" in outstream.getvalue() 436 | assert "IO speeds:\n" in outstream.getvalue() 437 | assert "\tbaudspeed : 0.0\n" in outstream.getvalue() 438 | assert "\tbaudratio : 0.0\n" in outstream.getvalue() 439 | assert "\tbaudratio_avg : 0.0\n" in outstream.getvalue() 440 | assert "\tbaudspeed_avg : 0.0\n" in outstream.getvalue() 441 | assert "Framing:\n" in outstream.getvalue() 442 | assert "\ttx_encoded_frames : 0\n" in outstream.getvalue() 443 | assert "\trx_uncomplete_frames : 0\n" in outstream.getvalue() 444 | assert "\ttx_processed_bytes : 0\n" in outstream.getvalue() 445 | assert "\trx_complete_frames : 0\n" in outstream.getvalue() 446 | assert "\ttx_escaped_bytes : 0\n" in outstream.getvalue() 447 | assert "\trx_discarded_bytes : 0\n" in outstream.getvalue() 448 | assert "\trx_processed_bytes : 0\n" in outstream.getvalue() 449 | assert "\trx_escaped_bytes : 0\n" in outstream.getvalue() 450 | assert "Protocol:\n" in outstream.getvalue() 451 | assert "\ttx_encoded_frames : 0\n" in outstream.getvalue() 452 | assert "\trx_corrupted_header : 0\n" in outstream.getvalue() 453 | assert "\trx_decoded_frames : 0\n" in outstream.getvalue() 454 | assert "\trx_corrupted_payload : 0\n" in outstream.getvalue() 455 | assert "\trx_corrupted_crc : 0\n" in outstream.getvalue() 456 | assert "\trx_corrupted_eol : 0\n" in outstream.getvalue() 457 | assert "\trx_corrupted_topic : 0\n" in outstream.getvalue() 458 | -------------------------------------------------------------------------------- /pytelemetrycli/test/test_topics.py: -------------------------------------------------------------------------------- 1 | from pytelemetrycli.topics import Topics 2 | from multiprocessing import Queue 3 | 4 | def test_process(): 5 | t1 = "testTopic" 6 | t2 = "otherTestTopic" 7 | t3 = "unknownTopic" 8 | topic = Topics() 9 | 10 | topic.process(t1,123) 11 | assert t1 in topic.ls() 12 | assert len(topic.ls()) == 1 13 | 14 | topic.process(t2,"booyaa") 15 | assert t1 in topic.ls() 16 | assert t2 in topic.ls() 17 | assert len(topic.ls()) == 2 18 | 19 | topic.process(t1,456) 20 | assert len(topic.ls()) == 2 21 | 22 | assert topic.samples(t1,amount=1) == [456] 23 | 24 | assert topic.samples(t1,amount=0) == [123,456] 25 | 26 | assert topic.count(t1) == 2 27 | 28 | assert not topic.exists(t3) 29 | assert topic.exists(t1) 30 | assert topic.exists(t2) 31 | 32 | def test_transfert_queue(): 33 | t1 = "testTopic" 34 | topic = Topics() 35 | q = Queue() 36 | 37 | topic.process(t1,123) 38 | topic.process(t1,456) 39 | topic.process(t1,789) 40 | 41 | assert q.empty() 42 | 43 | topic.transfer(t1,q) 44 | 45 | assert q.qsize() > 0 46 | 47 | assert q.get() == [0, 123] 48 | assert q.get() == [1, 456] 49 | assert q.get() == [2, 789] 50 | 51 | topic.process(t1,111) 52 | topic.process(t1,222) 53 | 54 | assert q.qsize() > 0 55 | 56 | assert q.get() == [3, 111] 57 | assert q.get() == [4, 222] 58 | 59 | def test_transfert_queue_indexed_data(): 60 | t1 = "testTopic" 61 | topic = Topics() 62 | q = Queue() 63 | 64 | topic.process(t1,123, {'index': 5}) 65 | topic.process(t1,456, {'index': 6}) 66 | topic.process(t1,789, {'index': 7}) 67 | 68 | assert q.empty() 69 | 70 | topic.transfer(t1,q,transfer_type='indexed') 71 | 72 | assert topic.intransfer(t1) == True 73 | 74 | assert q.qsize() > 0 75 | 76 | assert q.get() == [5, 123] 77 | assert q.get() == [6, 456] 78 | assert q.get() == [7, 789] 79 | 80 | topic.process(t1,111, {'index': 5}) 81 | topic.process(t1,222, {'index': 6}) 82 | topic.process(t1,333, {'index': 7}) 83 | topic.process(t1,333, {'index': 8}) 84 | 85 | assert q.qsize() > 0 86 | 87 | assert q.get() == [5, 111] 88 | assert q.get() == [6, 222] 89 | assert q.get() == [7, 333] 90 | assert q.get() == [8, 333] 91 | 92 | topic.untransfer(t1) 93 | 94 | assert topic.intransfer(t1) == False 95 | 96 | topic.process(t1,111, {'index': 5}) 97 | topic.process(t1,222, {'index': 6}) 98 | topic.process(t1,333, {'index': 7}) 99 | topic.process(t1,333, {'index': 8}) 100 | 101 | assert q.qsize() == 0 102 | 103 | 104 | def test_different_sources(): 105 | t1 = "remoteTopic" 106 | t2 = "systemTopic" 107 | topics = Topics() 108 | 109 | topics.process(t1,123) 110 | topics.process(t1,456) 111 | topics.process(t1,789) 112 | 113 | assert t1 in topics.ls() 114 | assert len(topics.ls()) == 1 115 | 116 | topics.create(t2,source="cli") 117 | 118 | topics.process(t2,123) 119 | topics.process(t2,456) 120 | topics.process(t2,789) 121 | 122 | assert t1 in topics.ls() 123 | assert t2 not in topics.ls() 124 | assert len(topics.ls()) == 1 125 | 126 | assert t1 not in topics.ls(source="cli") 127 | assert t2 in topics.ls(source="cli") 128 | assert len(topics.ls(source="cli")) == 1 129 | -------------------------------------------------------------------------------- /pytelemetrycli/tools.py: -------------------------------------------------------------------------------- 1 | def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): 2 | return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) 3 | -------------------------------------------------------------------------------- /pytelemetrycli/topics.py: -------------------------------------------------------------------------------- 1 | from sortedcontainers import SortedDict 2 | from logging import getLogger 3 | 4 | class Topic: 5 | """ 6 | A class to store and manage all data under a given topic 7 | """ 8 | def __init__(self, name, source='remote'): 9 | self.raw = [] 10 | self.indexes = SortedDict() 11 | self.source = source 12 | self.name = name 13 | 14 | def has_indexed_data(self): 15 | return len(self.indexes) > 0 16 | 17 | def new_sample(self, sample, options): 18 | if options: 19 | self.indexes[options['index']] = sample 20 | else: 21 | self.raw.append(sample) 22 | 23 | class Topics: 24 | """ 25 | A class that manages a collection of `Topic`s. 26 | 27 | """ 28 | def __init__(self): 29 | self.logger = getLogger('topics') 30 | self.logger.info('started session') 31 | self.clear() 32 | 33 | def clear(self): 34 | self.logger.info('Cleared all topics and received data') 35 | self.topic_list = SortedDict() 36 | self.transfers = dict() 37 | 38 | def create(self, topic, source='remote'): 39 | # Create the topic if it doesn't exist already 40 | if not topic in self.topic_list: 41 | self.topic_list[topic] = Topic(topic,source=source) 42 | self.logger.info('new:topic ' + topic) 43 | 44 | def process(self, topic, payload, options=None): 45 | # Create the topic if it doesn't exist already 46 | self.create(topic) 47 | 48 | # Add the new sample 49 | self.topic_list[topic].new_sample(payload,options) 50 | 51 | # logging 52 | if options: 53 | self.logger.debug('new sample | {0} [{1}] {2}'.format(topic, options['index'], payload)) 54 | else: 55 | self.logger.debug('new sample | {0} {1}'.format(topic, payload)) 56 | 57 | # If there is an active transfer, transfer received data to the queue 58 | if topic in self.transfers: 59 | # If transfer requires indexed data, check there is an index 60 | if self.transfers[topic]['type'] == 'indexed' and options is not None: 61 | x = options['index'] 62 | self.transfers[topic]['queue'].put([x, payload]) 63 | # For linear data, provide sample id for x and payload for y 64 | elif self.transfers[topic]['type'] == 'linear': 65 | x = self.transfers[topic]['lastindex'] 66 | self.transfers[topic]['queue'].put([x, payload]) 67 | self.transfers[topic]['lastindex'] += 1 68 | 69 | def ls(self,source='remote'): 70 | if source is None: 71 | return sorted([t.name for t in self.topic_list.keys()]) 72 | else: 73 | return sorted([t.name for t in self.topic_list.values() if t.source == source]) 74 | 75 | def samples(self,topic,amount=1): 76 | if not topic in self.topic_list: 77 | return None 78 | 79 | if amount == 0 or amount is None: 80 | return self.topic_list[topic].raw 81 | 82 | return self.topic_list[topic].raw[-amount:] 83 | 84 | def count(self,topic): 85 | if not topic in self.topic_list: 86 | return 0 87 | 88 | return len(self.topic_list[topic].raw) 89 | 90 | def exists(self,topic): 91 | return topic in self.topic_list 92 | 93 | def transfer(self, topic, queue, transfer_type = "linear"): 94 | # If the topic data is not already transfered to some queue 95 | if not topic in self.transfers: 96 | self.transfers[topic] = dict() 97 | self.transfers[topic]['queue'] = queue 98 | self.transfers[topic]['lastindex'] = 0 99 | self.transfers[topic]['type'] = transfer_type 100 | 101 | self.logger.info('start transfer | {0}'.format(topic)) 102 | 103 | # If there is already existing data under the topic 104 | if topic in self.topic_list: 105 | if transfer_type == 'indexed': 106 | for key, value in self.topic_list[topic].indexes.iteritems(): 107 | queue.put([key, value]) 108 | elif transfer_type == 'linear': 109 | for item in self.topic_list[topic].raw: 110 | queue.put([self.transfers[topic]['lastindex'], item]) 111 | self.transfers[topic]['lastindex'] += 1 112 | 113 | def untransfer(self,topic): 114 | # If the topic data is already transfered to some queue 115 | if topic in self.transfers: 116 | # Remove it from the transfer list 117 | del self.transfers[topic] 118 | self.logger.info('stop transfer | {0}'.format(topic)) 119 | 120 | def intransfer(self,topic): 121 | return topic in self.transfers 122 | 123 | def has_indexed_data(self,topic): 124 | return self.topic_list[topic].has_indexed_data() 125 | -------------------------------------------------------------------------------- /pytelemetrycli/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Overdrivr/pytelemetrycli/8969ab7515341b9821ba70bab93d4db9c6b7ccf5/pytelemetrycli/ui/__init__.py -------------------------------------------------------------------------------- /pytelemetrycli/ui/superplot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from pyqtgraph.Qt import QtGui, QtCore 3 | import numpy as np 4 | import pyqtgraph as pg 5 | from multiprocessing import Process, Queue, Pipe 6 | import time, threading 7 | from sortedcontainers import SortedDict 8 | from enum import Enum 9 | 10 | class PlotType(Enum): 11 | linear = 0, 12 | indexed = 1 13 | 14 | class Superplot(): 15 | """ 16 | Self-contained plotting class that runs in its own process. 17 | Plotting functionality (reset the graph, .. ?) can be controlled 18 | by issuing message-based commands using a multiprocessing Pipe 19 | 20 | """ 21 | def __init__(self,name,plottype=PlotType.indexed): 22 | self.name = name 23 | self.plottype = plottype 24 | self._clear() 25 | 26 | def _clear(self): 27 | # Process-local buffers used to host the displayed data 28 | if self.plottype == PlotType.linear: 29 | self.set = True 30 | self.x = [] 31 | self.y = [] 32 | else: 33 | self.xy = SortedDict() 34 | # TODO : use this optimization, but for now raises issue 35 | # Can't pickle dict_key views ?? 36 | #self.x = self.xy.keys() 37 | #self.y = self.xy.values() 38 | self.set = False 39 | 40 | def start(self): 41 | # The queue that will be used to transfer data from the main process 42 | # to the plot 43 | self.q = Queue() 44 | main_pipe, self.in_process_pipe = Pipe() 45 | self.p = Process(target=self.run) 46 | self.p.start() 47 | # Return a handle to the data queue and the control pipe 48 | return self.q, main_pipe 49 | 50 | def join(self): 51 | self.p.join() 52 | 53 | def _update(self): 54 | # Empty data queue and process received data 55 | while not self.q.empty(): 56 | item = self.q.get() 57 | if self.plottype == PlotType.linear: 58 | self.x.append(item[0]) 59 | self.y.append(item[1]) 60 | else: 61 | # Seems pretty slow, 62 | # TODO : Profile 63 | # TODO : Eventually, need to find high performance alternative. Maybe numpy based 64 | self.xy[item[0]] = item[1] 65 | 66 | # Initialize view on data dictionnary only once for increased performance 67 | if not self.set: 68 | self.set = True 69 | self.x = self.xy.keys() 70 | self.y = self.xy.values() 71 | 72 | # Refresh plot data 73 | self.curve.setData(self.x,self.y) 74 | 75 | try: 76 | if self.in_process_pipe.poll(): 77 | msg = self.in_process_pipe.recv() 78 | self._process_msg(msg) 79 | except: 80 | # If the polling failed, then the application most likely shut down 81 | # So close the window and terminate as well 82 | self.app.quit() 83 | 84 | def _process_msg(self, msg): 85 | if msg == "exit": 86 | # TODO : Remove this line ? Redundant with send after app.exec_() ? 87 | self.in_process_pipe.send("closing") 88 | self.app.quit() 89 | elif msg == "clear": 90 | self._clear() 91 | 92 | def run(self): 93 | self.app = QtGui.QApplication([]) 94 | win = pg.GraphicsWindow(title="Basic plotting examples") 95 | win.resize(1000,600) 96 | win.setWindowTitle('pyqtgraph example: Plotting') 97 | plot = win.addPlot(title=self.name) 98 | self.curve = plot.plot(pen='y') 99 | 100 | timer = QtCore.QTimer() 101 | timer.timeout.connect(self._update) 102 | timer.start(50) 103 | 104 | self.app.exec_() 105 | try: 106 | self.in_process_pipe.send("closing") 107 | except: 108 | pass 109 | # Process was done, no need to process exception 110 | 111 | if __name__ == '__main__': 112 | # This is function is responsible for faking some data (IO, serial port, etc) 113 | # and forwarding it to the display 114 | # it is run in a thread 115 | def io_linear(running,q): 116 | t = 0 117 | while running.is_set(): 118 | for i in range(32): 119 | s = np.sin(2 * np.pi * t) 120 | q.put([t,s]) 121 | t += 0.01 122 | time.sleep(0.0001) 123 | print("Done") 124 | 125 | # This is function is responsible for faking some data (IO, serial port, etc) 126 | # and forwarding it to the display 127 | # it is run in a thread 128 | def io_indexed(running,q): 129 | t = 0 130 | while running.is_set(): 131 | t += 0.005 132 | for i in range(128): 133 | s = np.sin(2 * np.pi * t + i/10) 134 | q.put([i,s]) 135 | time.sleep(0.01) 136 | print("Done") 137 | #To stop IO thread 138 | run = threading.Event() 139 | run.set() 140 | 141 | # create the plot 142 | s = Superplot("somePlot",PlotType.linear) 143 | #s = Superplot("somePlot",PlotType.indexed) 144 | 145 | # get the queue used to exchange data 146 | q, ctrlPipe = s.start() 147 | 148 | # start IO thread 149 | t = threading.Thread(target=io_linear, args=(run,q)) 150 | #t = threading.Thread(target=io_indexed, args=(run,q)) 151 | t.start() 152 | 153 | while True: 154 | action = input("Type 'q' to quit. Type 'clear' to reset the graph. Type 'exit' to close the graph but stay on main thread.") 155 | if action == 'clear': 156 | ctrlPipe.send('clear') 157 | elif action == 'exit': 158 | ctrlPipe.send('exit') 159 | elif action == 'q': 160 | break 161 | 162 | run.clear() 163 | print("Waiting for IO thread to join...") 164 | t.join() 165 | print("Waiting for graph window process to join...") 166 | s.join() 167 | print("Process joined successfully. C YA !") 168 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=0 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages, Extension 2 | from codecs import open 3 | from os import path 4 | from setuptools.dist import Distribution 5 | import io 6 | 7 | with io.open('DESCRIPTION.rst', encoding="utf-8") as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name='pytelemetrycli', 12 | 13 | version='1.1.0', 14 | 15 | description='command-line interface for data visualization and communication with embedded devices', 16 | long_description=long_description, 17 | 18 | url='https://github.com/Overdrivr/pytelemetrycli', 19 | 20 | author='Rémi Bèges', 21 | author_email='remi.beges@gmail.com', 22 | 23 | license='MIT', 24 | 25 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 26 | classifiers=[ 27 | 'Development Status :: 4 - Beta', 28 | 29 | # Indicate who your project is intended for 30 | 'Intended Audience :: Developers', 31 | 'Intended Audience :: End Users/Desktop', 32 | 'Intended Audience :: Telecommunications Industry', 33 | 'Topic :: Communications', 34 | 'Topic :: Scientific/Engineering :: Human Machine Interfaces', 35 | 'Topic :: Software Development :: Embedded Systems', 36 | 'Topic :: Software Development :: Libraries :: Python Modules', 37 | 38 | # Pick your license as you wish (should match "license" above) 39 | 'License :: OSI Approved :: MIT License', 40 | 41 | # Specify the Python versions you support here. In particular, ensure 42 | # that you indicate whether you support Python 2, Python 3 or both. 43 | 'Programming Language :: Python :: 3.3', 44 | 'Programming Language :: Python :: 3.4', 45 | 'Programming Language :: Python :: 3.5', 46 | ], 47 | 48 | keywords='cli plot debug lightweight communication protocol embedded telemetry remote program control', 49 | 50 | # You can just specify the packages manually here if your project is 51 | # simple. Or you can use find_packages(). 52 | packages=find_packages(), 53 | 54 | # List run-time dependencies here. These will be installed by pip when 55 | # your project is installed. For an analysis of "install_requires" vs pip's 56 | # requirements files see: 57 | # https://packaging.python.org/en/latest/requirements.html 58 | install_requires=['pytelemetry>=1.1.6','pyserial>3.0.0','docopt','pyqtgraph','numpy','sortedcontainers',], 59 | 60 | # List additional groups of dependencies here (e.g. development 61 | # dependencies). You can install these using the following syntax, 62 | # for example: 63 | # $ pip install -e .[dev,test] 64 | extras_require={ 65 | 'dev': ['check-manifest'], 66 | 'test': ['pytest','pytest-cov'], 67 | }, 68 | 69 | # To provide executable scripts, use entry points in preference to the 70 | # "scripts" keyword. Entry points provide cross-platform support and allow 71 | # pip to create the appropriate form of executable for the target platform. 72 | entry_points={ 73 | 'console_scripts': [ 74 | 'pytlm = pytelemetrycli.cli:pytlm', 75 | ], 76 | }, 77 | ) 78 | --------------------------------------------------------------------------------