├── resources ├── .gitattributes ├── RE_logo_32p.png ├── player_icons │ ├── 131.png │ ├── 141.png │ ├── 142.png │ ├── 148.png │ └── 195.png ├── re_logo.ico └── shortcuts.txt ├── .idea ├── .gitignore ├── misc.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml ├── PYCHARM.iml ├── vcs.xml └── runConfigurations │ ├── main.xml │ ├── qbluetooth_test.xml │ ├── serial_speed_test.xml │ ├── socket_widget.xml │ ├── range_widget.xml │ └── shortcuts.xml ├── .editorconfig ├── requirements.txt ├── docu ├── readme_images │ ├── .gitattributes │ ├── luisabel_demo.gif │ └── luisabel_screenshot.png ├── features_future.txt └── packaging_notes.txt ├── .gitmodules ├── howto_pyinstaller.txt ├── main.py ├── README.md ├── labelled_animated_toggle.py ├── qbluetooth_test.py ├── .gitignore ├── serial_speed_test.py ├── range_dialog.py ├── my_graph.py ├── LICENSE.md └── main_window.py /resources/.gitattributes: -------------------------------------------------------------------------------- 1 | *.ico filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # tab indentation set to 4 spaces 3 | [*.py] 4 | indent_style = tab 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /resources/RE_logo_32p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raquenaengineering/luisabel_serial_plotter/HEAD/resources/RE_logo_32p.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future==0.18.2 2 | numpy==1.20.1 3 | pyqtgraph==0.11.1 4 | PyQt5==5.15.4 5 | pyserial==3.5 6 | qtwidgets==0.18 7 | -------------------------------------------------------------------------------- /resources/player_icons/131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raquenaengineering/luisabel_serial_plotter/HEAD/resources/player_icons/131.png -------------------------------------------------------------------------------- /resources/player_icons/141.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raquenaengineering/luisabel_serial_plotter/HEAD/resources/player_icons/141.png -------------------------------------------------------------------------------- /resources/player_icons/142.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raquenaengineering/luisabel_serial_plotter/HEAD/resources/player_icons/142.png -------------------------------------------------------------------------------- /resources/player_icons/148.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raquenaengineering/luisabel_serial_plotter/HEAD/resources/player_icons/148.png -------------------------------------------------------------------------------- /resources/player_icons/195.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raquenaengineering/luisabel_serial_plotter/HEAD/resources/player_icons/195.png -------------------------------------------------------------------------------- /resources/re_logo.ico: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7e26e13ca5bcf9d7d7b376666154984865f13f3cc8e1567b5cca2b8889078a69 3 | size 243774 4 | -------------------------------------------------------------------------------- /docu/readme_images/.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | *bmp filter=lfs diff=lfs merge=lfs -text 3 | *gif filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /docu/readme_images/luisabel_demo.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:29c4d0e1a0ef5475673eeb8a9fff10c8f30d5cd1010f471cd73a592f89ed0756 3 | size 1187128 4 | -------------------------------------------------------------------------------- /docu/readme_images/luisabel_screenshot.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3e855f36db8de11d1b17e03fd30196acb2ec9d83b0825491ae20243635aac4b0 3 | size 72061 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /resources/shortcuts.txt: -------------------------------------------------------------------------------- 1 | full screen, f 2 | Connect, c 3 | Disconnect, d 4 | Play, y 5 | Pause, p 6 | Record, r 7 | Stop, - 8 | Wider Y axis, Arrow up 9 | Narower Y axis, Arrow down 10 | Wider X axis, Arrow Left 11 | Narrower X axis, Arrow Right 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "re_pyqt_widgets"] 2 | path = re_pyqt_widgets 3 | url = https://github.com/raquenaengineering/re_pyqt_widgets 4 | [submodule "pyqt_common_resources"] 5 | path = pyqt_common_resources 6 | url = https://github.com/raquenaengineering/pyqt_common_resources.git 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/PYCHARM.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /howto_pyinstaller.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | SINGLE EXECUTABLE WITH LOGO: 4 | ------------------------------------------------------------------------ 5 | 6 | 1. Move to the folder containing the pyinstaller version for this project, in this case: 7 | cd ~/Desktop/PROYECTOS/luisabel_serial_plotter/venv/Scripts 8 | 9 | 2. Run Pyinstaller with the following commands: 10 | pyinstaller.exe --onefile --ico="../../resources/re_logo.ico" --noconsole ../../main.py 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docu/features_future.txt: -------------------------------------------------------------------------------- 1 | 2 | - CREATE TOOL TO VIEW AND MARK STORED DATA? 3 | - CREATE METHOD SAVE_DATASET(NPOINTS?)? 4 | - FEATURE: ENABLE/DISABLE AUTOSCALE. 5 | - FEATURE: HOME TO A CERTAIN DIMENSIONS (GIVEN AT CONFIG FILE?) 6 | - FEATURE: ADD CONFIG FILE. 7 | - FEATURE: ADD SHORTCUT LIST (AT LEAST WITH THE CURRENT SHORTCUTS) 8 | - FEATURE: SERIAL BUFFER SIZE AS A CONFIG PARAMETER. 9 | - FEATURE: ADD SHORTCUTS FOR NUMBER BUTTONS: 0 = all/none 10 | - each number = itsplot 11 | - FEATURE: AUTOUPDATE SERIAL PORTS. ---> USE TIMER. 12 | 13 | -------------------------------------------------------------------------------- /.idea/runConfigurations/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | Copyright (C) 2020 Raul Quesada Navarro 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | ''' 18 | 19 | import sys 20 | from main_window import * 21 | 22 | ## MAIN ## 23 | 24 | app = QApplication(sys.argv) 25 | app.setStyle("Fusion") # required to use it here 26 | mw = MainWindow() 27 | app.exec_() 28 | -------------------------------------------------------------------------------- /.idea/runConfigurations/qbluetooth_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | -------------------------------------------------------------------------------- /.idea/runConfigurations/serial_speed_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | -------------------------------------------------------------------------------- /.idea/runConfigurations/socket_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | -------------------------------------------------------------------------------- /.idea/runConfigurations/range_widget.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | -------------------------------------------------------------------------------- /.idea/runConfigurations/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LuIsabel 2 | 3 | Plotting tool based on the arduino plotter using python and Qt, with extended functionality. 4 | 5 | Here a little Demo: 6 | ![Luisabel small Demo](https://github.com/raquenaengineering/arduino_plotter_pyqt/blob/main/docu/readme_images/luisabel_demo.gif) 7 | 8 | ## Dependencies 9 | 10 | + Python 3.9(!) 11 | + Pyserial: 12 | pip install pyserial 13 | + PyQt5: 14 | pip install PyQt5 15 | + numpy: 16 | pip install numpy 17 | + pyqtgraph: 18 | pip install pyqtgraph 19 | + Qt Widgets: 20 | pip install qtwidgets 21 | + To simplify (in the project folder): 22 | pip install -r requirements.txt 23 | + submodules: 24 | git submodule update --init --recursive 25 | 26 | ## Many thanks to: 27 | 28 | + Qt Development team: 29 | Is such a great tool! 30 | + Developers who ported QT5 to python : 31 | Even better when it's so easy to develop! 32 | + [PyqtGraph Developers](https://github.com/pyqtgraph/pyqtgraph): 33 | This tool wouldn't have been possible without his fantastic pyqtgraph library. 34 | + [Martin Fitzpatrick](https://github.com/mfitzp): 35 | For writing qtwidgets, and also for his book about PyQt5. 36 | + [Yusuke Kamiyamane](https://p.yusukekamiyamane.com/): 37 | For the icons. 38 | -------------------------------------------------------------------------------- /labelled_animated_toggle.py: -------------------------------------------------------------------------------- 1 | 2 | from PyQt5.QtWidgets import( 3 | QApplication, 4 | QMainWindow, 5 | QWidget, 6 | QHBoxLayout, # create a new widget, which contains the MyGraph window 7 | QVBoxLayout, 8 | QLabel 9 | ) 10 | 11 | from PyQt5.QtCore import( 12 | QTimer 13 | ) 14 | import pyqtgraph as pg 15 | import qtwidgets 16 | from labelled_animated_toggle import * 17 | 18 | class LabelledAnimatedToggle(QWidget): 19 | 20 | def __init__(self,color = "#aaffaa", label_text = ""): # optional parameters instead ??? yes, thanks. 21 | super().__init__() 22 | self.label = QLabel(label_text) 23 | self.toggle = qtwidgets.AnimatedToggle(checked_color = color) 24 | 25 | self.layout = QHBoxLayout() 26 | self.setLayout(self.layout) 27 | self.layout.addWidget(self.toggle) 28 | self.layout.addWidget(self.label) 29 | self.layout.setContentsMargins(0,0,0,0) # reducing the space the toggle takes as much as possible 30 | self.layout.setSpacing(0) 31 | 32 | def setLabel(self,label_text): 33 | self.label.setText(label_text) 34 | def getLabel(self): 35 | label = self.label.text() 36 | return(label) 37 | 38 | def setChecked(self, val): 39 | self.toggle.setChecked(val) 40 | def isChecked(self): 41 | return(self.toggle.isChecked()) 42 | 43 | def setEnabled(self, val): 44 | self.toggle.setEnabled(val) 45 | def isEnabled(self): 46 | return(self.toggle.isEnabled()) 47 | -------------------------------------------------------------------------------- /qbluetooth_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import signal 3 | import sys 4 | import os 5 | 6 | from PyQt5.QtCore import QCoreApplication, QTimer 7 | from PyQt5.QtBluetooth import QBluetoothLocalDevice, QBluetoothDeviceDiscoveryAgent, QBluetoothDeviceInfo 8 | 9 | 10 | class Application(QCoreApplication): 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | 14 | self.dev = QBluetoothDeviceInfo() 15 | self.dlist = [] 16 | self.counter = 0 17 | 18 | self.localDevice = QBluetoothLocalDevice() 19 | print(self.localDevice.name()) 20 | 21 | self.scan_for_devices() 22 | self.exec() 23 | 24 | def display_status(self): 25 | d = 0 26 | 27 | # print(self.agent.isActive(), self.agent.discoveredDevices()) 28 | 29 | def fin(self, *args, **kwargs): 30 | self.agent.stop() 31 | self.dlist = self.agent.discoveredDevices() 32 | while self.counter < len(self.dlist): 33 | print(self.dlist[self.counter].name()) 34 | self.counter += 1 35 | os.environ['QT_EVENT_DISPATCHER_CORE_FOUNDATION'] = '0' 36 | sys.exit(0) 37 | 38 | def err(self, *args, **kwargs): 39 | print("Ein Fehler ist aufgetretten.") 40 | 41 | def scan_for_devices(self): 42 | self.agent = QBluetoothDeviceDiscoveryAgent(self) 43 | # self.agent.deviceDiscovered.connect(self.fin) 44 | self.agent.finished.connect(self.fin) 45 | self.agent.error.connect(self.err) 46 | self.agent.setLowEnergyDiscoveryTimeout(10000) 47 | # self.agent.discoveredDevices() 48 | 49 | self.agent.start() 50 | 51 | timer = QTimer(self.agent) 52 | timer.start(500) 53 | timer.timeout.connect(self.display_status) 54 | 55 | 56 | if __name__ == '__main__': 57 | import sys 58 | 59 | os.environ['QT_EVENT_DISPATCHER_CORE_FOUNDATION'] = '1' 60 | 61 | app = Application(sys.argv) 62 | 63 | -------------------------------------------------------------------------------- /docu/packaging_notes.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | WINDOWS: 4 | - TOOLS: 5 | - pyinstaller. 6 | - inno setup. 7 | 8 | - PROCESS: 9 | - PYINSTALLER: 10 | - Use pyinstaller at the folder where the source is. 11 | - this will create build and dist folder. 12 | - on dist/appname folder, there will be an .exe file. 13 | - test if the functionality is right. 14 | ------------------------------------------ 15 | - INNO SETUP: 16 | - Create script using wizard. 17 | - Set application name/version and rest of the data. 18 | - Allow user to change application folder is checked. 19 | - Application files: 20 | - main executable file: Browse: /path/to/exe_file generated with pyInstaller. 21 | - Other application files: Add folder: /path/to/dist/project_name --> get the whole directory, this is what works best. 22 | - associate file not checked (we don't have a file format) 23 | - Create shortcut checked, allow user for desktop shortcut checked. 24 | - Application documentation: all 3 unchecked. 25 | - Install mode: administrative install. 26 | - Language english. 27 | - Compiler settings: Add icon file (RE_logo.ico) 28 | - yes, use #define. 29 | - finish. 30 | - Compile new script now. 31 | - Save in safe place, use name of the project --> keep as a part of the repo? 32 | - Run (F9) in inno setup Compiler. 33 | 34 | - install and test. 35 | 36 | 37 | - NOTES/ISSUES: 38 | PYINSTALLED: 39 | - Only python files get compiled !!! 40 | - no external resources get added to the dist folder!!! 41 | - no images 42 | - no text files with configuration. 43 | ------------------------------------------ 44 | INNO SETUP: 45 | - IF INSTALLING IN PROGRAM FILES, THE SOFTWARE WILL NOT HAVE WRITE PERMISSION SO: 46 | - LOGS WON'T BE ABLE TO BE SAVED. 47 | - CHANGES IN CONFIGURATION WON'T WORK. 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # added by Raquena Engineering @ 2 | /local_docu 3 | /logs 4 | /re_pyqt_widgets 5 | /pyqt_common_resources 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | -------------------------------------------------------------------------------- /serial_speed_test.py: -------------------------------------------------------------------------------- 1 | 2 | # SERIAL_SPEED_TEST: 3 | 4 | # this script will be used to calculate the maximum throughput of the different modalities of serial ports:# 5 | # SIL2104 (ESP32 SERIAL TO USB CONVERTER) 6 | # BLUETOOTH (ESP32) 7 | # NATIVE USB(TEENSY) 8 | 9 | 10 | # standard imports # 11 | import sys # deal with OS, open files and so 12 | import time # delays, and time measurement ? 13 | import random # random numbers 14 | import os # dealing with directories 15 | 16 | import serial # required to handle serial communication 17 | import serial.tools.list_ports # to list already existing ports 18 | 19 | import csv 20 | import numpy as np # required to handle multidimensional arrays/matrices 21 | 22 | import logging 23 | #logging.basicConfig(level=logging.DEBUG) # enable debug messages 24 | logging.basicConfig(level = logging.WARNING) 25 | 26 | 27 | class serial_speed_tester(): 28 | 29 | serial_port = None 30 | 31 | def __init__(self): 32 | pass 33 | 34 | def serial_connect(self, port_name): 35 | logging.debug("serial_connect method called") 36 | logging.debug(port_name) 37 | logging.debug("port name " + port_name) 38 | 39 | try: # closing port just in case was already open. (SHOULDN'T BE !!!) 40 | self.serial_port.close() 41 | logging.debug("Serial port closed") 42 | logging.debug( 43 | "IT SHOULD HAVE BEEN ALWAYS CLOSED, REVIEW CODE!!!") # even though the port can't be closed, this message is shown. why ??? 44 | except: 45 | logging.debug("serial port couldn't be closed") 46 | logging.debug("Wasn't open, as it should always be") 47 | 48 | try: # try to establish serial connection 49 | self.serial_port = serial.Serial( # serial constructor 50 | port=port_name, 51 | baudrate=self.serial_baudrate, 52 | # baudrate = 115200, 53 | # bytesize=EIGHTBITS, 54 | # parity=PARITY_NONE, 55 | # stopbits=STOPBITS_ONE, 56 | # timeout=None, 57 | timeout=0, # whenever there's no dat on the buffer, returns inmediately (spits '\0') 58 | xonxoff=False, 59 | rtscts=False, 60 | write_timeout=None, 61 | dsrdtr=False, 62 | inter_byte_timeout=None, 63 | exclusive=None 64 | ) 65 | 66 | except Exception as e: # both port open, and somebody else blocking the port are IO errors. 67 | logging.debug("ERROR OPENING SERIAL PORT") 68 | #self.on_port_error(e) 69 | 70 | except: 71 | logging.debug("UNKNOWN ERROR OPENING SERIAL PORT") 72 | 73 | else: # IN CASE THERE'S NO EXCEPTION (I HOPE) 74 | logging.debug("SERIAL CONNECTION SUCCESFUL !") 75 | self.status_bar.showMessage("Connected") 76 | # here we should also add going to the "DISCONNECT" state. 77 | 78 | logging.debug("serial_port.is_open:") 79 | logging.debug(self.serial_port.is_open) 80 | logging.debug("done: ") 81 | 82 | 83 | # logging.debug(self.done) 84 | 85 | 86 | if __name__ == '__main__': 87 | # tester = serial_speed_tester() 88 | # tester.serial_connect("COM17") 89 | 90 | repetitions = 20 91 | time_span = 1 92 | n_vals_tot = 0 93 | 94 | #serial_port = serial.Serial("COM17") 95 | serial_port = serial.Serial("COM4") 96 | 97 | 98 | i = 0 99 | while (i < repetitions): 100 | t = time.time() 101 | t0 = time.time() 102 | dt = 0 103 | while (dt <= time_span): 104 | vals = serial_port.read(100); 105 | n_vals = len(vals) 106 | n_vals_tot = n_vals_tot + n_vals 107 | #print(vals); 108 | t = time.time() 109 | dt = t - t0 110 | print(dt) 111 | print(n_vals_tot) 112 | n_vals_tot = 0 113 | i = i + 1 114 | serial_port.close() 115 | -------------------------------------------------------------------------------- /range_dialog.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import logging 4 | 5 | import numpy as np 6 | 7 | import csv 8 | 9 | from PyQt5.QtWidgets import ( 10 | QApplication, 11 | QMainWindow, 12 | QWidget, 13 | QVBoxLayout, 14 | QPushButton, 15 | QHBoxLayout, 16 | QTableWidgetItem, 17 | QLineEdit, 18 | QLabel, 19 | QDialog, 20 | QDialogButtonBox, 21 | QFormLayout, 22 | ) 23 | 24 | from PyQt5.QtCore import ( 25 | QTimer, 26 | Qt, 27 | pyqtSignal, 28 | ) 29 | 30 | from PyQt5.QtGui import ( 31 | QIntValidator, 32 | ) 33 | 34 | COLORS = ["ff0000", "00ff00", "0000ff", "ffff00", "ff00ff", "00ffff", 35 | "ff0000", "00ff00", "0000ff", "ffff00", "ff00ff", "00ffff", 36 | "ff0000", "00ff00", "0000ff", "ffff00", "ff00ff", "00ffff", 37 | "ff0000", "00ff00", "0000ff", "ffff00", "ff00ff", "00ffff", 38 | 39 | ] 40 | 41 | MAX_PLOTS = 24 # Absolute maximum number of plots, change if needed !! 42 | ABS_MAX_RANGE = 1000000 43 | 44 | 45 | class RangeDialog(QDialog): 46 | def __init__(self, absolute_max_range = None): 47 | super().__init__() 48 | 49 | self.min_textbox = QLineEdit(self) 50 | self.max_textbox = QLineEdit(self) 51 | self.onlyInt = QIntValidator() 52 | self.min_textbox.setValidator(self.onlyInt) 53 | self.max_textbox.setValidator(self.onlyInt) 54 | buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self); 55 | 56 | layout = QFormLayout(self) 57 | layout.addRow("Minimum", self.min_textbox) 58 | layout.addRow("Maximum", self.max_textbox) 59 | layout.addWidget(buttonBox) 60 | 61 | buttonBox.accepted.connect(self.accept) 62 | buttonBox.rejected.connect(self.reject) 63 | 64 | def getInputs(self): 65 | return (self.min_textbox.text(), self.max_textbox.text()) 66 | 67 | 68 | class RangeDialogOldOld(QDialog): # this is supposed to be the python convention for classes. 69 | 70 | # variables # 71 | max = None # parameters to be returned 72 | min = None 73 | # signals # 74 | range = pyqtSignal(int, int) # This signal will be used to send the range chosen in the popup window. 75 | okPressed = pyqtSignal() 76 | cancelPressed = pyqtSignal() 77 | 78 | 79 | def __init__(self): 80 | 81 | super().__init__() 82 | self.setWindowTitle("Set Range") 83 | self.resize(400, 200) # setting initial window size 84 | self.layout = QVBoxLayout() 85 | self.setLayout(self.layout) 86 | # text boxes # 87 | self.textboxes_layout = QHBoxLayout() 88 | self.textbox_min = QLineEdit() 89 | self.textboxes_layout.addWidget(self.textbox_min) 90 | self.separator = QLabel("--") 91 | self.textboxes_layout.addWidget(self.separator) 92 | self.textbox_max = QLineEdit() 93 | self.textboxes_layout.addWidget(self.textbox_max) 94 | self.layout.addLayout(self.textboxes_layout) 95 | # buttons # 96 | self.buttons_layout = QHBoxLayout() 97 | self.layout.addLayout(self.buttons_layout) 98 | self.cancel_button = QPushButton("Cancel") 99 | self.buttons_layout.addWidget(self.cancel_button) 100 | self.cancel_button.clicked.connect(self.on_cancel) 101 | self.ok_button = QPushButton("Ok") 102 | self.buttons_layout.addWidget(self.ok_button) 103 | self.ok_button.clicked.connect(self.on_ok) 104 | 105 | self.setWindowModality(Qt.ApplicationModal) 106 | self.show() # window is created and destroyed every time we change shortcuts. 107 | 108 | # DISABLE STUFF WHICH CAN'T BE USED YET ######################## 109 | 110 | ################################################################ 111 | 112 | 113 | 114 | def on_ok(self): 115 | # use return values to spit back new range 116 | # alternatively, create a signal, for the parent window to subscribe, so I can save the data coming from this # 117 | # alternatively, this data can be stored in a configuration file, which should be reviewed after every change in config # 118 | pass 119 | return(self.max, self.min) 120 | self.close() 121 | 122 | def on_cancel(self): # NOT WORKING 123 | self.close() 124 | 125 | def accept(self): # 126 | pass 127 | 128 | def closeEvent(self, event): 129 | print("CLOSING AND CLEANING UP:") 130 | self.shortcuts_table = None 131 | super().close() 132 | # event.ignore() 133 | 134 | class RangeDialogOld(QDialog): 135 | 136 | min_val = None 137 | max_val = None 138 | 139 | def __init__(self, parent=None): 140 | 141 | super().__init__(parent) 142 | 143 | self.setWindowTitle("Set range") 144 | # main layout # 145 | self.layout = QVBoxLayout() 146 | self.setLayout(self.layout) 147 | # text boxes # 148 | self.textboxes_layout = QHBoxLayout() 149 | self.textbox_min = QLineEdit() 150 | self.textboxes_layout.addWidget(self.textbox_min) 151 | self.separator = QLabel("--") 152 | self.textboxes_layout.addWidget(self.separator) 153 | self.textbox_max = QLineEdit() 154 | self.textboxes_layout.addWidget(self.textbox_max) 155 | self.layout.addLayout(self.textboxes_layout) 156 | # add buttons to the button box (inherited from QDialog) 157 | QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel 158 | self.buttonBox = QDialogButtonBox(QBtn) 159 | self.buttonBox.accepted.connect(self.accept) 160 | self.buttonBox.rejected.connect(self.reject) 161 | self.layout.addWidget(self.buttonBox) 162 | 163 | # 164 | self.setModal(True) # should be by default, but didn't seem to work 165 | # 166 | # self.show() 167 | 168 | def accept(self): 169 | self.min_val = self.textbox_min.text() 170 | self.max_val = self.textbox_max.text() 171 | 172 | # print("The minimum value") 173 | # print(self.min_val) 174 | #return(self.min_val) 175 | self.close() 176 | print("reacher end of accept") 177 | 178 | class MainWindow(QMainWindow): 179 | # class variables # 180 | data_tick_ms = 5 181 | 182 | # creating a fixed size dataset # 183 | dataset = [] 184 | 185 | # constructor # 186 | def __init__(self): 187 | super().__init__() 188 | 189 | self.setWindowTitle("Testing shorcuts menu window") 190 | # create shortcuts widget to test the class # 191 | self.set_range_button = QPushButton("Set Range") 192 | self.set_range_button.clicked.connect(self.on_click_range_button) 193 | self.resize(1200, 800) # setting initial window size 194 | self.setCentralWidget(self.set_range_button) 195 | # last step is showing the window # 196 | #self.setWindowModality(Qt.ApplicationModal) # other windows are blocked until this window is closed 197 | self.show() # window is created and destroyed every time we change the values 198 | 199 | def on_click_range_button(self): # 200 | self.range_setter = RangeDialog(ABS_MAX_RANGE) # needs to be self, or it won't persist 201 | if self.range_setter.exec(): 202 | print(self.range_setter.getInputs()) 203 | 204 | for inp in self.range_setter.getInputs(): 205 | print(inp) 206 | 207 | ## THIS PART WON'T BE EXECUTED WHEN IMPORTED AS A SUBMODULE, BUT ONLY WHEN TESTED INDEPENDENTLY ## 208 | 209 | if __name__ == "__main__": 210 | 211 | app = QApplication([]) 212 | app.setStyle("Fusion") # required to use it here 213 | mw = MainWindow() 214 | app.exec_() 215 | 216 | 217 | -------------------------------------------------------------------------------- /my_graph.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | import time 4 | import logging 5 | import math 6 | 7 | import numpy as np 8 | 9 | 10 | from PyQt5.QtWidgets import( 11 | QApplication, 12 | QMainWindow, 13 | QWidget, 14 | QHBoxLayout, # create a new widget, which contains the MyGraph window 15 | QVBoxLayout, 16 | QLabel 17 | ) 18 | 19 | from PyQt5.QtCore import( 20 | QTimer 21 | ) 22 | import pyqtgraph as pg 23 | import qtwidgets 24 | from labelled_animated_toggle import * 25 | 26 | COLORS = ["ff0000","00ff00","0000ff","ffff00","ff00ff","00ffff", 27 | "FFA500","7fff00","00ff7f","007FFF","EE82EE","FF007F", 28 | "ff0000","00ff00","0000ff","ffff00","ff00ff","00ffff", 29 | "FFA500","7fff00","00ff7f","007FFF","EE82EE","FF007F",] 30 | 31 | MAX_PLOTS = 12 # Absolute maximum number of plots, change if needed !! 32 | ABS_Y_MAX = 1000000 # Absolute maximum Y range, is fixed, and can only be changed on compilation time. 33 | DEFAULT_Y_MAX = 100000 34 | DEFAULT_Y_MIN = -100000 35 | DEFAULT_MAX_POINTS = 2000 36 | CHANNEL_LABEL_MAX_LEN = 10 37 | 38 | class MyPlot(QWidget): 39 | 40 | n_plots = 12 # number of plots on the current plot. 41 | plot_tick_ms = 50 # every "plot_tick_ms", the plot updates, no matter if there's new data or not. 42 | dataset = [] # complete dataset, this should go to a file. 43 | toggles = [] # references to the toggles which enable/disable plots. 44 | 45 | def __init__(self, dataset = [], max_points = DEFAULT_MAX_POINTS): 46 | super().__init__() 47 | 48 | # central widget # 49 | self.layout = QHBoxLayout() # that's how we will lay out the window 50 | self.setLayout(self.layout) 51 | self.graph = MyGraph(dataset = dataset, max_points = max_points) 52 | self.layout.addWidget(self.graph) 53 | self.layout_channel_select = QVBoxLayout() 54 | self.layout.addLayout(self.layout_channel_select) 55 | self.channel_label = QLabel("Channels:") 56 | self.layout_channel_select.addWidget(self.channel_label) 57 | self.add_toggles() 58 | 59 | self.layout_channel_name = QVBoxLayout() 60 | # timer # 61 | self.plot_timer = QTimer() # used to update the plot 62 | self.plot_timer.timeout.connect(self.on_plot_timer) # 63 | self.start_plotting(self.plot_tick_ms) 64 | self.stop_plotting() 65 | 66 | self.set_enabled_graphs("none") # writes to a variable of graph indicating which graphs are on 67 | 68 | print("Init until set_enabled_graphs") 69 | 70 | for toggle in self.toggles: 71 | toggle.setChecked(True) 72 | toggle.setEnabled(True) 73 | 74 | # ~ for toggle in self.toggles: 75 | # ~ print("IsChecked") 76 | # ~ print(toggle.isChecked()) 77 | # ~ print("IsEnabled") 78 | # ~ print(toggle.isEnabled()) 79 | 80 | def add_toggles(self): # encapsulates the creation of the toggles, and their initial setup. 81 | for i in range(0, MAX_PLOTS): 82 | color = "#"+COLORS[i] 83 | label_toggle = LabelledAnimatedToggle(color = color) 84 | self.toggles.append(label_toggle) 85 | label_toggle.setChecked(False) # all toggles not checked by default # create new method to call the toggle method? 86 | label_toggle.setEnabled(True) # all toggles not enabled by default 87 | self.layout_channel_select.addWidget(label_toggle) 88 | 89 | def enable_toggles(self,val): 90 | if(val == "all"): 91 | for i in range(MAX_PLOTS): 92 | self.toggles[i].setEnabled(True) 93 | elif(val == "none"): 94 | for i in range(MAX_PLOTS): 95 | self.toggles[i].setEnabled(False) 96 | else: 97 | pass # fill with behavior if val is a vector 98 | 99 | def check_toggles(self,vals): 100 | if(vals == "all"): 101 | for i in range(MAX_PLOTS): 102 | print(self.toggles[i].isEnabled()) 103 | if(self.toggles[i].isEnabled()): # so we can only check enabled toggles. 104 | self.toggles[i].setChecked(True) 105 | elif(vals == "none"): 106 | for i in range(MAX_PLOTS): 107 | self.toggles[i].setChecked(False) 108 | else: 109 | for i in range (MAX_PLOTS): 110 | self.toggles[i].setChecked(vals[i]) # vals should be a list with as many elements as toggles 111 | 112 | def set_channels_labels(self,names): # each channel toggle has a label, set the text on that label. 113 | for i in range(MAX_PLOTS): # we only assign the names of the plots that can be plotted 114 | try: 115 | name = names[i][:CHANNEL_LABEL_MAX_LEN] 116 | self.toggles[i].setLabel(name) 117 | except Exception as e: 118 | logging.debug("more channels than labels") 119 | 120 | def clear_channels_labels(self): # clear all labels, usually to set them with new vals. 121 | for i in range(MAX_PLOTS): # we only assign the names of the plots that can be plotted 122 | try: 123 | self.toggles[i].setLabel('') 124 | except Exception as e: 125 | logging.debug("more channels than labels") 126 | 127 | def set_max_points(self, max_points): 128 | self.graph.max_points = max_points 129 | self.graph.setLimits(xMin=0, xMax=self.graph.max_points) 130 | self.graph.setXRange(0,self.graph.max_points) 131 | 132 | 133 | 134 | def create_plots(self): 135 | self.graph.create_plots() 136 | 137 | def clear_plot(self): # NOT WORKING 138 | self.graph.clear_plot() 139 | 140 | def on_plot_timer(self): # this is an option, to add together toggle processing and replot. 141 | enabled = [] 142 | for i in range(0,MAX_PLOTS): 143 | if(self.toggles[i].toggle.isChecked()): 144 | enabled.append(True) 145 | else: 146 | enabled.append(False) 147 | 148 | self.set_enabled_graphs(enabled) 149 | 150 | self.graph.dataset = self.dataset 151 | 152 | self.graph.on_plot_timer() # calls the regular plot timer from graph. 153 | 154 | def plot_timer_start(self): 155 | self.graph.timer.start() 156 | 157 | def update(self): # notifies a change in the dataset 158 | self.graph.dataset_changed = True # flag 159 | #self.graph.dataset = self.dataset 160 | 161 | def setBackground(self, color): 162 | self.graph.setBackground(color) 163 | 164 | def start_plotting(self, period = None): 165 | if(period == None): 166 | self.plot_timer.start() 167 | else: 168 | self.plot_timer.start(period) 169 | 170 | def stop_plotting(self): 171 | self.plot_timer.stop() 172 | 173 | def set_enabled_graphs(self, enable_list): 174 | if enable_list == "all": 175 | enable_list = [] 176 | for i in range(MAX_PLOTS): 177 | enable_list.append(True) 178 | elif enable_list == "none": 179 | enable_list = [] 180 | for i in range(MAX_PLOTS): 181 | enable_list.append(False) 182 | 183 | self.graph.set_enabled_graphs(enable_list) 184 | 185 | 186 | 187 | class MyGraph(pg.PlotWidget): # this is supposed to be the python convention for classes. 188 | 189 | # Arduino serial plotter has 500 points max. on the x axis. 190 | max_points = None # maximum points per plot 191 | tvec = [] # independent variable, with "max_points" points. 192 | n_plots = 12 # number of plots on the current plot. 193 | first = True # first iteration only creating the plots 194 | 195 | dataset = None # complete dataset, this should go to a file. 196 | np_dataset = None # used for reverting the matrix. 197 | np_dataset_t = None 198 | plot_refs = [] # references to the different added plots. 199 | plot_subset = [] 200 | enabled_graphs = [] # enabled graphs ON GRAPH WINDOW, not on toggles. 201 | 202 | for i in range(0,MAX_PLOTS): 203 | enabled_graphs.append(False) 204 | 205 | dataset_changed = False 206 | 207 | 208 | #dataset = np.array() 209 | 210 | def __init__(self, dataset = None, max_points = 500, title = ""): 211 | 212 | for i in range(max_points): # create a time vector --> move to NUMPY !!! 213 | self.tvec.append(i) 214 | 215 | self.dataset = dataset # get the reference to the dataset given as input for the constructor 216 | self.max_points = max_points 217 | 218 | #self.plot_subset = self.dataset[:self.n_plots][-(self.max_points):] # get only the portion of the dataset which needs to be printed. 219 | 220 | super().__init__() 221 | pg.setConfigOptions(antialias=False) # antialiasing for nicer view. 222 | self.setBackground([70,70,70]) # changing default background color. 223 | #self.showGrid(x = True, y = True, alpha = 0.5) 224 | self.setRange(xRange = [0,self.max_points], yRange = [-10,100]) # set default axes range 225 | self.setLimits(xMin=0, xMax=self.max_points, yMin=DEFAULT_Y_MIN, yMax=DEFAULT_Y_MAX) # THIS MAY ENTER IN CONFIG WITH PLOTTING !!! 226 | #self.enableAutoRange(axis='x', enable=True) # enabling autorange for x axis 227 | legend = self.addLegend() 228 | self.setTitle(title) # if title is wanted 229 | 230 | def create_plots(self): 231 | for i in range (MAX_PLOTS): 232 | logging.debug("val of i:" + str(i)) 233 | p = self.plot(pen = (COLORS[i%24])) 234 | self.plot_refs.append(p) 235 | 236 | 237 | def clear_plot(self): 238 | print("clear_plot method called") 239 | for i in range(len(self.plot_subset)): 240 | self.plot_refs[i].clear() # clears the plot 241 | self.plot_refs[i].setData([0]) # sets the data to 0, may not be necessary 242 | #self.plot_subset[i] = [] 243 | 244 | def set_enabled_graphs(self,enabled_graphs): # enabled/dsables graphs ON GRAPH WINDOW, not on the toggles. 245 | self.enabled_graphs = enabled_graphs 246 | 247 | 248 | def on_plot_timer(self): 249 | #print("PLOT_TIMER MyGraph") 250 | #print (self.dataset_changed) 251 | 252 | if self.first == True: # FIRST: CREATE THE PLOTS 253 | self.create_plots() 254 | print("len(self.plot_refs)") 255 | print(len(self.plot_refs)) 256 | self.first = False 257 | print("First plot timer") 258 | 259 | # SECOND: UPDATE THE PLOTS: 260 | 261 | if(self.dataset_changed == True): # redraw only if there are changes on the dataset 262 | #print("dataset has changed") 263 | #print("length of subset") 264 | #print(len(self.plot_subset)) 265 | self.dataset_changed = False 266 | 267 | self.np_dataset = np.matrix(self.dataset[:][-self.max_points:]) # we only use as subset the last max_points 268 | self.np_dataset_t = self.np_dataset.transpose() 269 | self.plot_subset = self.np_dataset_t.tolist() 270 | # ~ print("len(self.plot_subset[0])") 271 | # ~ print(len(self.plot_subset[0])) 272 | 273 | 274 | # ~ print("self.dataset") 275 | # ~ print(self.dataset) 276 | # ~ print("self.np_dataset") 277 | # ~ print(self.np_dataset) 278 | # ~ print("self.plot_subset") 279 | # ~ for var in self.plot_subset: 280 | # ~ print(var) 281 | 282 | 283 | for i in range(len(self.plot_subset)): 284 | # ~ print("len(self.plot_refs)") 285 | # ~ print(len(self.plot_refs)) 286 | if(self.enabled_graphs[i] == True): 287 | self.plot_refs[i].setData(self.plot_subset[i]) # required for update: reassign references to the plots 288 | else: 289 | self.plot_refs[i].setData([]) # empty plot, if toggle not active. 290 | 291 | self.dataset_changed = True 292 | 293 | pg.QtGui.QApplication.processEvents() # for whatever reason, works faster when using processEvent. 294 | 295 | 296 | ## THIS PART WON'T BE EXECUTED WHEN IMPORTED AS A SUBMODULE, BUT ONLY WHEN TESTED INDEPENDENTLY ## 297 | 298 | if __name__ == "__main__": 299 | 300 | class MainWindow(QMainWindow): 301 | 302 | # class variables # 303 | data_tick_ms = 20 304 | 305 | #creating a fixed size dataset # 306 | dataset = [] 307 | 308 | # constructor # 309 | def __init__(self): 310 | 311 | super().__init__() 312 | 313 | # add graph and show # 314 | #self.graph = MyGraph(dataset = self.dataset) 315 | self.plot = MyPlot(dataset = self.dataset) # extend the constructor, to force giving a reference to a dataset ??? 316 | self.plot.set_channels_labels(["Gastro Medialis", 317 | "Gastro Lateralis", 318 | "Australopitecute", 319 | "caracol", 320 | "col", 321 | "pene", 322 | "carapene", 323 | "Ermengildo II", 324 | "mondongo", 325 | "cagarruta", 326 | "Zurullo", 327 | "caca"]) 328 | 329 | self.plot.start_plotting() 330 | 331 | self.data_timer = QTimer() 332 | self.data_timer.timeout.connect(self.on_data_timer) 333 | self.data_timer.start(self.data_tick_ms) 334 | 335 | self.plot.check_toggles("all") 336 | self.plot.enable_toggles("all") 337 | 338 | 339 | 340 | self.setCentralWidget(self.plot) 341 | # last step is showing the window # 342 | self.show() 343 | 344 | #self.plot.graph.plot_timer.start() 345 | 346 | 347 | def on_data_timer(self): # simulate data coming from external source at regular rate. 348 | t0 = time.time() 349 | logging.debug("length of dataset: " + str(len(self.plot.dataset))) 350 | 351 | 352 | line = [] 353 | for i in range(0,MAX_PLOTS): 354 | line.append(random.randrange(0,100)) 355 | self.dataset.append(line) 356 | 357 | print("self.dataset") 358 | for data in self.dataset: 359 | print(data) 360 | 361 | self.plot.dataset = self.dataset # this SHOULD HAPPEN INTERNAL TO THE CLASS !!! 362 | 363 | self.plot.update() 364 | t = time.time() 365 | dt = t - t0 366 | logging.debug("execution time add_stuff_dataset " + str(dt)) 367 | 368 | 369 | app = QApplication([]) 370 | app.setStyle("Fusion") # required to use it here 371 | mw = MainWindow() 372 | app.exec_() 373 | 374 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /main_window.py: -------------------------------------------------------------------------------- 1 | # standard imports # 2 | import sys # deal with OS, open files and so 3 | import time # delays, and time measurement ? 4 | import random # random numbers 5 | import os # dealing with directories 6 | 7 | import serial # required to handle serial communication 8 | import serial.tools.list_ports # to list already existing ports 9 | 10 | import csv 11 | import numpy as np # required to handle multidimensional arrays/matrices 12 | 13 | import logging 14 | #logging.basicConfig(level=logging.DEBUG) # enable debug messages 15 | from future.utils import text_type 16 | 17 | logging.basicConfig(level = logging.WARNING) 18 | 19 | # custom packages # 20 | 21 | from pyqt_common_resources import pyqt_custom_palettes # moved to an independent repo, to reuse the palettes. 22 | from my_graph import MyPlot 23 | import my_graph # for the global variables of the namespace. 24 | from re_pyqt_widgets.shortcuts_widget import ShortcutsWidget # custom widget to display and edit shortcuts 25 | from range_dialog import RangeDialog 26 | 27 | # qt imports # 28 | from PyQt5.QtWidgets import ( 29 | QApplication, 30 | QMainWindow, 31 | QVBoxLayout, 32 | QHBoxLayout, 33 | QLabel, 34 | QComboBox, 35 | QLineEdit, 36 | QPushButton, 37 | QMenuBar, 38 | QToolBar, 39 | QStatusBar, 40 | QDialog, 41 | QMessageBox, # Dialog with extended functionality. 42 | QShortcut, 43 | QCheckBox, 44 | 45 | QSystemTrayIcon, 46 | QTextEdit, 47 | QMenu, 48 | QAction, 49 | QWidget, 50 | QInputDialog, 51 | ) 52 | 53 | from PyQt5.QtGui import ( 54 | QIcon, 55 | QKeySequence 56 | ) 57 | 58 | from PyQt5.QtCore import( 59 | Qt, 60 | QThreadPool, 61 | QRunnable, 62 | QObject, 63 | QSize, 64 | pyqtSignal, # those two are pyqt specific. 65 | pyqtSlot, 66 | QTimer # nasty stuff 67 | 68 | ) 69 | 70 | # GLOBAL VARIABLES # 71 | 72 | SERIAL_BUFFER_SIZE = 10000 # buffer size to store the incoming data from serial, to afterwards process it. 73 | SEPARATOR = "----------------------------------------------------------" 74 | 75 | 76 | SERIAL_SPEEDS = [ 77 | "300", 78 | "1200", 79 | "2400", 80 | "4800", 81 | "9600", 82 | "19200", 83 | "38400", 84 | "57600", 85 | "74880", 86 | "115200", 87 | "230400", 88 | "250000", 89 | "500000", 90 | "1000000", 91 | "2000000" 92 | ] 93 | 94 | ENDLINE_OPTIONS = [ 95 | "No Line Adjust", 96 | "New Line", 97 | "Carriage Return", 98 | "Both NL & CR" 99 | ] 100 | 101 | RECORD_PERIOD = 1000 # time in ms between two savings of the recorded data onto file 102 | POINTS_PER_PLOT = 2000 # width of x axis, corresponding to the number of dots to be plotted at each iteration 103 | 104 | # THREAD STUFF # (not needed ATM) 105 | 106 | 107 | # MAIN WINDOW # 108 | 109 | class MainWindow(QMainWindow): 110 | 111 | # class variables # 112 | serial_ports = list # list of serial ports detected, probably this is better somewhere else !!! 113 | serial_port = None # maybe better to decleare it somewhere else ??? serial port used for the comm. 114 | serial_port_name = None # used to pass it to the worker dealing with the serial port. 115 | serial_baudrate = 115200 # default baudrate, ALL THOSE VARIABLES SHOULD CONNECT TO WORKER_SERIALPORT! 116 | endline = '\r\n' # default value for endline is CR+NL 117 | error_type = None # used to try to fix the problem with dialog window, delete if can't fix !!! 118 | serial_message_to_send = None # if not none, is a message to be sent via serial port (the worker sends) 119 | full_screen_flag = False 120 | dataset = [] # dataset containing the data to be plotter/recorded. 121 | log_folder = "logs" # in the beginning, log folder, path and filename are fixed 122 | log_file_name = "log_file" # at some point, path needs to be selected by user. 123 | log_file_type = ".csv" # file extension 124 | n_logs = 0 125 | log_full_path = None # this variable will be the one used to record 126 | timeouts = 0 127 | parsing_style = "arduino" # defines the parsing style, for now Arduino, or EMG 128 | read_buffer = "" # if change to default parsing emg style: read_buffer = [], all chars read from serial come here, should it go somewhere else? 129 | recording = False # flag to start/stop recording. 130 | first_toggles = 0 # used to check the toggles which contain data on start graphing. 131 | n_data_points = POINTS_PER_PLOT # defaults to the POINTS_PER_PLOT value 132 | 133 | # constructor # 134 | def __init__(self): 135 | 136 | super().__init__() 137 | 138 | self.init_dataset() 139 | 140 | # timer to record data onto file periodically (DELETES OLD DATA IF RECORD NOT ENABLED)# 141 | self.record_timer = QTimer() 142 | self.record_timer.timeout.connect(self.on_record_timer) # will be enabled / disabled via button 143 | self.record_timer.start(RECORD_PERIOD) # deploys data onto file once a second 144 | self.record_timer.stop() 145 | # serial timer # 146 | self.serial_timer = QTimer() # we'll use timer instead of thread 147 | self.serial_timer.timeout.connect(self.on_serial_timer) 148 | self.serial_timer.start(50) # period needs to be relatively short 149 | self.serial_timer.stop() # by default the timer will be off, enabled by connect. 150 | # update serial ports timer # 151 | self.update_ports_timer = QTimer() 152 | self.update_ports_timer.timeout.connect( 153 | self.update_serial_ports) # updating serial port list in a regular time basis. 154 | self.update_ports_timer.start(3000) # every 3 seconds seems reasonable. 155 | self.update_ports_timer.stop() 156 | 157 | 158 | # shortcuts moved to the bottom # 159 | 160 | # theme(palette) # 161 | self.palette = pyqt_custom_palettes.dark_palette() 162 | self.setPalette(self.palette) 163 | 164 | # window stuff # 165 | self.setWindowTitle("Luisabel EMG Plotter") # relevant title 166 | self.setWindowIcon(QIcon("RE_logo_32p.png")) # basic raquena engineering branding 167 | self.resize(1200,800) # setting initial window size 168 | # menubar # 169 | menu = self.menuBar() # by default, the window already has an instance/object menubar 170 | # file # 171 | self.file_menu = menu.addMenu("&File") 172 | self.serial_port_menu = self.file_menu.addMenu("Serial Port") 173 | self.refresh_menu = self.file_menu.addAction("Refresh") 174 | self.refresh_menu.triggered.connect(self.update_serial_ports) 175 | # Preferences # 176 | self.preferences_menu = menu.addMenu("&Preferences") 177 | # theme # 178 | self.theme_submenu = self.preferences_menu.addMenu("Theme") 179 | self.dark_theme_option = self.theme_submenu.addAction("Dark") 180 | self.dark_theme_option.triggered.connect(self.set_dark_theme) 181 | self.light_theme_option = self.theme_submenu.addAction("Light") 182 | self.light_theme_option.triggered.connect(self.set_light_theme) 183 | self.re_theme_option = self.theme_submenu.addAction("Raquena") 184 | self.re_theme_option.triggered.connect(self.set_re_theme) 185 | # Parsing # 186 | self.parsing_submenu = self.preferences_menu.addMenu("Parsing mode") 187 | self.arduino_parsing_option = self.parsing_submenu.addAction("Arduino") 188 | self.arduino_parsing_option.triggered.connect(self.set_arduino_parsing) 189 | self.emg_parsing_option = self.parsing_submenu.addAction("EMG Sensor") 190 | self.emg_parsing_option.triggered.connect(self.set_emg_parsing) 191 | self.emg_parsing_new_option = self.parsing_submenu.addAction("EMG Sensor NEW") 192 | self.emg_parsing_new_option.triggered.connect(self.set_emg_parsing_new) 193 | # Set plot range # 194 | self.set_range_action = self.preferences_menu.addAction("Set Plot Y Range") 195 | self.set_range_action.triggered.connect(self.set_plot_range) 196 | # Set maximum plot points # 197 | self.set_n_plot_points_action = self.preferences_menu.addAction("Set max. plot points (X Range)") 198 | self.set_n_plot_points_action.triggered.connect(self.set_n_plot_points) 199 | # shortcuts # 200 | self.shortcuts_action = self.preferences_menu.addAction("Shortcuts") 201 | self.shortcuts_action.triggered.connect(self.shortcut_preferences) 202 | # about # 203 | help_menu = menu.addMenu("&Help") 204 | about_menu = help_menu.addAction("About") 205 | 206 | button_action = QAction() 207 | self.file_menu.addAction(button_action) 208 | 209 | #self.toolbar.setIconSize(Qsize(16,16)) 210 | #self.addToolBar(self.toolbar) 211 | #self.menubar.addMenu("&file") 212 | 213 | # central widget # 214 | self.widget = QWidget() 215 | self.layoutV1 = QVBoxLayout() # that's how we will lay out the window 216 | self.widget.setLayout(self.layoutV1) 217 | self.setCentralWidget(self.widget) 218 | # graph / plot # 219 | self.layout_plot = QHBoxLayout() # plot plus buttons to enable/disable graphs 220 | self.layoutV1.addLayout(self.layout_plot) 221 | self.plot_frame = MyPlot(dataset = self.dataset, 222 | max_points = self.n_data_points) # we'll use a custom class, so we can modify the defaults via class definition 223 | self.plot_frame.max_points = self.n_data_points # width of the plot in points, doesn't work !!! 224 | self.plot_frame.enable_toggles("none") 225 | self.plot_frame.check_toggles("none") 226 | self.layout_plot.addWidget(self.plot_frame) 227 | # buttons for plot # 228 | self.layout_player = QHBoxLayout() 229 | self.layoutV1.addLayout(self.layout_player) 230 | # play button # 231 | self.button_play = QPushButton("Play") 232 | self.button_play.setIcon(QIcon('resources/player_icons/131.png')) 233 | self.button_play.clicked.connect(self.on_button_play) 234 | self.button_play.setEnabled(False) 235 | self.layout_player.addWidget(self.button_play) 236 | # pause button # 237 | self.button_pause = QPushButton("Pause") 238 | self.button_pause.setIcon(QIcon('resources/player_icons/141.png')) 239 | self.button_pause.clicked.connect(self.on_button_pause) 240 | self.button_pause.setEnabled(False) 241 | self.layout_player.addWidget(self.button_pause) 242 | # record button # 243 | self.button_record = QPushButton("Record") 244 | self.button_record.setIcon(QIcon('resources/player_icons/148.png')) 245 | self.button_record.clicked.connect(self.on_button_record) # enables timer to periodically store the data onto a file. 246 | self.layout_player.addWidget(self.button_record) 247 | # stop button # 248 | self.button_stop = QPushButton("Stop") 249 | self.button_stop.setIcon(QIcon('resources/player_icons/142.png')) 250 | self.button_stop.clicked.connect(self.on_button_stop) # enables timer to periodically store the data onto a file. 251 | self.button_stop.setEnabled(False) 252 | self.layout_player.addWidget(self.button_stop) 253 | # autoscale # 254 | self.button_autoscale = QPushButton("Scale Fit") 255 | self.button_autoscale.setIcon(QIcon('resources/player_icons/195.png')) 256 | self.button_autoscale.clicked.connect(self.on_button_autoscale) 257 | self.layout_player.addWidget(self.button_autoscale) 258 | #self.button_autoscale.setCheckable(True) 259 | 260 | # ~ self.autoscale_toggle = LabelledAnimatedToggle(color = "#ffffff",label_text = "Autoscale") 261 | # ~ self.layout_player.addWidget(self.autoscale_toggle) 262 | 263 | 264 | # buttons / menus # 265 | self.layoutH1 = QHBoxLayout() 266 | self.layoutV1.addLayout(self.layoutH1) 267 | # connect button # 268 | self.button_serial_connect = QPushButton("Connect") 269 | self.button_serial_connect.clicked.connect(self.on_button_connect_click) 270 | self.layoutH1.addWidget(self.button_serial_connect) 271 | # disconnect button # 272 | self.button_serial_disconnect = QPushButton("Disconnect") 273 | self.button_serial_disconnect.clicked.connect(self.on_button_disconnect_click) 274 | self.button_serial_disconnect.setEnabled(False) 275 | self.layoutH1.addWidget(self.button_serial_disconnect) 276 | # combo serial port # 277 | self.combo_serial_port = QComboBox() 278 | self.layoutH1.addWidget(self.combo_serial_port) 279 | self.update_serial_ports() 280 | self.combo_serial_port.currentTextChanged.connect( # changing something at this label, triggers on_port select, which should trigger a serial port characteristics update. 281 | self.on_port_select) 282 | self.label_port = QLabel("Port") 283 | self.layoutH1.addWidget(self.label_port) 284 | # combo serial speed # 285 | self.combo_serial_speed = QComboBox() 286 | self.combo_serial_speed.setEditable(False) # by default it isn't editable, but just in case. 287 | self.combo_serial_speed.addItems(SERIAL_SPEEDS) 288 | self.combo_serial_speed.setCurrentIndex(SERIAL_SPEEDS.index(str(self.serial_baudrate))) # this index corresponds to 250000 as default baudrate. 289 | self.combo_serial_speed.currentTextChanged.connect( # on change on the serial speed textbox, we call the connected mthod 290 | self.change_serial_speed) # we'll figure out which is the serial speed at the method (would be possible to use a lambda?) 291 | self.layoutH1.addWidget(self.combo_serial_speed) # 292 | self.label_baud = QLabel("baud") 293 | self.layoutH1.addWidget(self.label_baud) 294 | # text box command # 295 | self.textbox_send_command = QLineEdit() 296 | self.textbox_send_command.returnPressed.connect(self.send_serial) # sends command via serial port 297 | self.textbox_send_command.setEnabled(False) # not enabled until serial port is connected. 298 | self.layoutH1.addWidget(self.textbox_send_command) 299 | # send button # 300 | self.b_send = QPushButton("Send") 301 | self.b_send.clicked.connect(self.send_serial) # same action as enter in textbox 302 | self.layoutH1.addWidget(self.b_send) 303 | # combo endline # 304 | self.combo_endline_params = QComboBox() 305 | self.combo_endline_params.addItems(ENDLINE_OPTIONS) 306 | self.combo_endline_params.setCurrentIndex(3) # defaults to endline with CR & NL 307 | self.combo_endline_params.currentTextChanged.connect(self.change_endline_style) 308 | self.layoutH1.addWidget(self.combo_endline_params) 309 | 310 | # status bar # 311 | self.status_bar = QStatusBar() 312 | self.setStatusBar(self.status_bar) 313 | self.status_bar.showMessage("Not Connected") 314 | # 3. write text saying no serial port is connected 315 | 316 | # show and done # 317 | self.show() 318 | 319 | # other stuff which can't be done before # 320 | self.serial_port_name = self.combo_serial_port.currentText() 321 | self.serial_baudrate = int(self.combo_serial_speed.currentText()) 322 | # self.serial_baudrate = ... 323 | # self.endline = ... 324 | 325 | 326 | # on close # 327 | 328 | def closeEvent(self, event): 329 | 330 | print("CLOSING AND CLEANING UP:") 331 | try: 332 | print("Closing serial port") 333 | self.serial_port.close() # need to explicitly close the serial port to release it 334 | except: 335 | print("Couldn't close serial port, probably already closed / never open") 336 | super().close() 337 | #event.ignore() # extremely useful to ignore the close event ! 338 | 339 | # actions # 340 | 341 | def set_logfile(self): 342 | # 1. be sure the folder where the log file should be placed exists 343 | # logs folder # 344 | print("Log file name:") 345 | print(self.log_file_name) 346 | print("Log file folder:") 347 | print(self.log_folder) 348 | 349 | if not os.path.exists(self.log_folder): # create logs folder to store the logs 350 | os.makedirs(self.log_folder) # mkdir only makes one directory on the path, makedirs, makes all 351 | # 2. check what's on that folder. 352 | path = os.getcwd() 353 | print("current path:") 354 | print(path) 355 | fullpath = path +'/'+ self.log_folder +'/'+ self.log_file_name + self.log_file_type 356 | print("Full file path") 357 | print(fullpath) 358 | if os.path.exists(fullpath): 359 | fullpath = path +'/'+ self.log_folder +'/'+ self.log_file_name + str(self.n_logs) + self.log_file_type 360 | self.n_logs = self.n_logs + 1 361 | 362 | self.log_full_path = fullpath; 363 | 364 | 365 | def send_serial(self, command = None): # if no command, we get text from textbox, if command, we send what we got internally 366 | logging.debug("Send Serial") 367 | if(command == None): 368 | command = self.textbox_send_command.text() # get what's on the textbox. 369 | self.textbox_send_command.setText("") 370 | # here the serial send command # 371 | self.serial_message_to_send = command.encode('utf-8') # this should have effect on the serial_thread 372 | 373 | logging.debug("serial_message_to_send") 374 | logging.debug(self.serial_message_to_send) 375 | self.serial_port.write(self.serial_message_to_send) 376 | 377 | # other methods # 378 | 379 | def get_serial_ports(self): # REWRITE THIS FUNCTION TO USE A DICTIONARY, AND MAKE IT WAY CLEANER !!! 380 | 381 | logging.debug('Running get_serial_ports') 382 | serial_port = None 383 | self.serial_ports = list(serial.tools.list_ports.comports()) # THIS IS THE ONLY PLACE WHERE THE OS SERIAL PORT LIST IS READ. 384 | 385 | port_names = [] # we store all port names in this variable 386 | port_descs = [] # all descriptions 387 | port_btenums = [] # all bluetooth enumerations, if proceeds 388 | for port in self.serial_ports: 389 | port_names.append(port[0]) # here all names get stored 390 | port_descs.append(port[1]) 391 | port_btenums.append(port[2]) 392 | 393 | for name in port_names: 394 | logging.debug(name) 395 | logging.debug("---------------------------------------------------") 396 | 397 | for desc in port_descs: 398 | logging.debug(desc) 399 | logging.debug("---------------------------------------------------") 400 | 401 | for btenum in port_btenums: 402 | logging.debug(btenum) 403 | logging.debug("---------------------------------------------------") 404 | 405 | # remove bad BT ports (windows creates 2 ports, only one is useful to connect) 406 | 407 | for port in self.serial_ports: 408 | 409 | port_desc = port[1] 410 | 411 | if (port_desc.find("Bluetooth") != -1): # Bluetooth found on description,so is a BT port (good or bad, dunno yet) 412 | 413 | # Using the description as the bt serial ports to find out the "good" bluetooth port. 414 | port_btenum = port[2] 415 | port_btenum = str(port_btenum) 416 | splitted_enum = port_btenum.split('&') 417 | logging.debug(splitted_enum) # uncomment this to see why this parameter was used to differentiate bt ports. 418 | last_param = splitted_enum[-1] # this contains the last parameter of the bt info, which is different between incoming and outgoing bt serial ports. 419 | last_field = last_param.split('_') # here there is the real difference between the two created com ports 420 | last_field = last_field[-1] # we get only the part after the '_' 421 | logging.debug(last_field) 422 | 423 | if(last_field == "C00000000"): # this special string is what defines what are the valid COM ports. 424 | discarded = 0 # the non-valid COM ports have a field liked this: "00000001", and subsequent. 425 | else: 426 | discarded = 1 427 | logging.debug("This port should be discarded!") 428 | self.serial_ports.remove(port) # removes by matching description 429 | 430 | def change_serial_speed(self): # this function is useless ATM, as the value is asked when serial open again. 431 | logging.debug("change_serial_speed method called") 432 | text_baud = self.combo_serial_speed.currentText() 433 | baudrate = int(text_baud) 434 | #self.serial_port.baudrate.set(baudrate) 435 | self.serial_baudrate = baudrate 436 | logging.debug(text_baud) 437 | 438 | def change_endline_style(self): # this and previous method are the same, use lambdas? 439 | logging.debug("change_endline_speed method called") 440 | endline_style = self.combo_endline_params.currentText() 441 | logging.debug(endline_style) 442 | # FIND A MORE ELEGANT AND PYTHONIC WAY TO DO THIS. 443 | if(endline_style == ENDLINE_OPTIONS[0]): # "No Line Adjust" 444 | self.endline = b"" 445 | elif (endline_style == ENDLINE_OPTIONS[1]): # "New Line" 446 | self.endline = b"\n" 447 | elif (endline_style == ENDLINE_OPTIONS[2]): # "Carriage Return" 448 | self.endline = b"\r" 449 | elif (endline_style == ENDLINE_OPTIONS[3]): # "Both NL & CR" 450 | self.endline = b"\r\n" 451 | 452 | logging.debug(self.endline) 453 | 454 | def on_button_connect_click(self): # this button changes text to disconnect when a connection is succesful. 455 | logging.debug("Connect Button Clicked") # how to determine a connection was succesful ??? 456 | self.button_serial_connect.setEnabled(False) 457 | self.button_serial_disconnect.setEnabled(True) 458 | self.combo_serial_port.setEnabled(False) 459 | self.combo_serial_speed.setEnabled(False) 460 | self.combo_endline_params.setEnabled(False) 461 | self.textbox_send_command.setEnabled(True) 462 | self.button_pause.setEnabled(True) 463 | self.record_timer.start() 464 | self.plot_frame.dataset = self.dataset 465 | self.status_bar.showMessage("Connecting...") # showing sth is happening. 466 | self.plot_frame.enable_toggles("all") 467 | self.start_serial() 468 | self.setup_slave() # depending on the choosen mode, there are some requirements to start getting data. 469 | self.on_button_play() 470 | 471 | self.first_toggles = 0 472 | 473 | 474 | def serial_connect(self, port_name): 475 | logging.debug("serial_connect method called") 476 | logging.debug(port_name) 477 | logging.debug("port name " + port_name) 478 | 479 | try: # closing port just in case was already open. (SHOULDN'T BE !!!) 480 | self.serial_port.close() 481 | logging.debug("Serial port closed") 482 | logging.debug("IT SHOULD HAVE BEEN ALWAYS CLOSED, REVIEW CODE!!!") # even though the port can't be closed, this message is shown. why ??? 483 | except: 484 | logging.debug("serial port couldn't be closed") 485 | logging.debug("Wasn't open, as it should always be") 486 | 487 | 488 | try: # try to establish serial connection 489 | self.serial_port = serial.Serial( # serial constructor 490 | port=port_name, 491 | baudrate= self.serial_baudrate, 492 | #baudrate = 115200, 493 | #bytesize=EIGHTBITS, 494 | #parity=PARITY_NONE, 495 | #stopbits=STOPBITS_ONE, 496 | #timeout=None, 497 | timeout=0, # whenever there's no dat on the buffer, returns inmediately (spits '\0') 498 | xonxoff=False, 499 | rtscts=False, 500 | write_timeout=None, 501 | dsrdtr=False, 502 | inter_byte_timeout=None, 503 | exclusive=None 504 | ) 505 | 506 | except Exception as e: # both port open, and somebody else blocking the port are IO errors. 507 | logging.debug("ERROR OPENING SERIAL PORT") 508 | self.on_port_error(e) 509 | 510 | except: 511 | logging.debug("UNKNOWN ERROR OPENING SERIAL PORT") 512 | 513 | else: # IN CASE THERE'S NO EXCEPTION (I HOPE) 514 | logging.debug("SERIAL CONNECTION SUCCESFUL !") 515 | self.status_bar.showMessage("Connected") 516 | # here we should also add going to the "DISCONNECT" state. 517 | 518 | logging.debug("serial_port.is_open:") 519 | logging.debug(self.serial_port.is_open) 520 | logging.debug("done: ") 521 | #logging.debug(self.done) 522 | 523 | def start_serial(self): 524 | # first ensure connection is properly made 525 | self.serial_connect(self.serial_port_name) 526 | # 2. move status to connected 527 | # 3. start the timer to collect the data 528 | self.serial_timer.start() 529 | # 4. Initialization stuff required by the remote serial device: 530 | self.init_emg_sensor() 531 | 532 | def init_emg_sensor(self): 533 | # initialization stuff (things required for the sensors to start sending shit) 534 | # message = "E=1;" # enable EMG data. 535 | # self.serial_message_to_send = message.encode('utf-8') # this should have effect on the serial_thread 536 | # logging.debug(self.serial_message_to_send) 537 | # self.serial_port.write(self.serial_message_to_send) 538 | # message = "START;" 539 | # self.serial_message_to_send = message.encode('utf-8') # this should have effect on the serial_thread 540 | # logging.debug(self.serial_message_to_send) 541 | # self.serial_port.write(self.serial_message_to_send) 542 | pass 543 | 544 | def on_button_disconnect_click(self): 545 | print("Disconnect Button Clicked") 546 | self.button_serial_disconnect.setEnabled(False) # toggle the enable of the connect/disconnect buttons 547 | self.button_serial_connect.setEnabled(True) 548 | self.combo_serial_port.setEnabled(True) 549 | self.combo_serial_speed.setEnabled(True) 550 | self.combo_endline_params.setEnabled(True) 551 | self.textbox_send_command.setEnabled(False) 552 | self.status_bar.showMessage("Disconnected") # showing sth is happening. 553 | self.plot_frame.clear_plot() # clear plot 554 | self.clear_dataset() 555 | self.plot_frame.dataset = self.dataset # when clearing the dataset, we need to reassign the plot frame !!! --> this is not right!!!, but works. 556 | print("self.plot_frame.dataset") 557 | print(self.plot_frame.dataset) 558 | self.plot_frame.clear_channels_labels() 559 | self.plot_frame.check_toggles("none") 560 | self.plot_frame.enable_toggles("none") 561 | self.serial_port.close() 562 | self.serial_timer.stop() 563 | self.plot_frame.plot_timer.stop() 564 | self.on_record_timer() # this should save what's left to the file and clear the dataset 565 | self.on_button_stop() # and this should disable the recording, if we disconnect the serial port 566 | self.record_timer.stop() 567 | print(SEPARATOR) 568 | 569 | 570 | def on_button_pause(self): 571 | # pause the plot: 572 | # so stop the update timer. # 573 | print("on_button_pause method: ") 574 | #self.plot_frame.plot_timer.stop() # but we won't be able to rearm... 575 | self.plot_frame.stop_plotting() 576 | self.button_pause.setEnabled(False) 577 | self.button_play.setEnabled(True) 578 | 579 | def on_button_play(self): 580 | # pause the plot: 581 | # so stop the update timer. # 582 | print("on_button_play method: ") 583 | self.button_play.setEnabled(False) 584 | self.button_pause.setEnabled(True) 585 | #self.plot_frame.plot_timer.start() # but we won't be able to rearm... 586 | self.plot_frame.start_plotting() 587 | 588 | def on_button_record(self): 589 | print("on_button_record method: ") 590 | self.start_recording() 591 | self.set_logfile() 592 | self.button_record.setEnabled(False) 593 | self.button_stop.setEnabled(True) 594 | 595 | def on_button_stop(self): 596 | print("on_button_stop method: ") 597 | self.stop_recording() 598 | #self.log_file.close() 599 | self.button_stop.setEnabled(False) 600 | self.button_record.setEnabled(True) 601 | 602 | def on_button_autoscale(self): 603 | # if self.button_autoscale.isChecked(): # if checked, we uncheck and disable autoscale. 604 | print("Autorange enabled") 605 | self.plot_frame.graph.enableAutoRange(axis='y') 606 | self.plot_frame.graph.setAutoVisible(y=True) # don't know what's this 607 | #self.button_autoscale.setChecked(True) 608 | 609 | # else: 610 | # print("Autorange disabled") 611 | # self.button_autoscale.setEnabled(True) 612 | # self.button_autoscale.setChecked(False) 613 | 614 | 615 | def on_port_select(self,port_name): # callback when COM port is selected at the menu. 616 | #1. get the selected port name via the text. 617 | #2. delete the old list, and regenerate it, so when we push again the com port list is updated. 618 | #3. create a thread for whatever related with the serial communication, and start running it. 619 | #. open a serial communication. --> this belongs to the thread. 620 | 621 | # START THE THREAD WHICH WILL BE IN CHARGE OF RECEIVING THE SERIAL DATA # 622 | #self.serial_connect(port_name) 623 | logging.debug("Method on_port_select called ") 624 | self.serial_port_name = port_name 625 | logging.debug(self.serial_port_name) 626 | 627 | def on_arrow_up(self): 628 | print("on_arrow_up method called") 629 | # change y axis from plot 630 | y_axis = self.plot_frame.graph.getAxis('left').range 631 | print(y_axis) 632 | for i in range(len(y_axis)): 633 | y_axis[i] = y_axis[i]/2 634 | self.plot_frame.graph.setRange(yRange = y_axis) 635 | 636 | def on_arrow_down(self): 637 | print("on_arrow_down method called") 638 | # change y axis from plot 639 | y_axis = self.plot_frame.graph.getAxis('left').range 640 | print(y_axis) 641 | for i in range(len(y_axis)): 642 | y_axis[i] = y_axis[i]*2 643 | self.plot_frame.graph.setRange(yRange = y_axis) 644 | 645 | def on_arrow_left(self): 646 | print("on_arrow_left method called") 647 | x_axis = self.plot_frame.graph.getAxis('bottom').range 648 | print(x_axis) 649 | for i in range(len(x_axis)): 650 | x_axis[i] = x_axis[i]/2 651 | self.plot_frame.graph.setRange(xRange = x_axis) 652 | 653 | def on_arrow_right(self): 654 | print("on_arrow_right method called") 655 | x_axis = self.plot_frame.graph.getAxis('bottom').range 656 | print(x_axis) 657 | for i in range(len(x_axis)): 658 | x_axis[i] = x_axis[i]*2 659 | self.plot_frame.graph.setRange(xRange = x_axis) 660 | 661 | def start_recording(self): 662 | self.set_logfile() 663 | self.recording = True 664 | 665 | def stop_recording(self): 666 | self.recording = False 667 | 668 | def on_record_timer(self): 669 | #print("on_record_timer method called:") 670 | t0 = time.time() 671 | 672 | # ~ print("self.dataset") 673 | # ~ for line in self.dataset: 674 | # ~ print(line) 675 | 676 | if(self.recording == True): 677 | print("saving data to file") 678 | # ~ np_data = np.array(self.dataset) 679 | # ~ np_data_t = np_data.transpose() 680 | # ~ print("Data") 681 | # ~ print(np_data) 682 | # ~ print(SEPARATOR) 683 | # ~ print("Transposed data") 684 | # ~ print(np_data_t) 685 | # ~ print(SEPARATOR) 686 | with open(self.log_full_path, mode = 'a', newline = '') as csv_file: # "log_file.csv" if will probably smash the data after first write!!! 687 | dataset_writer = csv.writer(csv_file, delimiter = ',') # standard way to write to csv file 688 | for value in self.dataset[0:self.n_data_points]: # only record the first "POINT_PER_PLOT" data points. 689 | dataset_writer.writerow(value) 690 | 691 | t = time.time() 692 | dt = t-t0 693 | # print(dt) 694 | # print(SEPARATOR) 695 | # 696 | # print("dataset lenght[0] on_record_timer():") 697 | # print(len(self.dataset)) # we need to use the first item, as dataset will have a lenght depending on the number of plots received from Arduino. 698 | # print("dataset lenght on_timer") 699 | 700 | 701 | while(len(self.dataset) > 3*self.n_data_points): # this ensures there's always enough data to plot the whole window. 702 | logging.debug("Dataset removing some points") 703 | # ~ for i in range(self.n_data_points-1): 704 | # ~ self.dataset.pop() 705 | logging.debug(len(self.dataset)) 706 | self.dataset = self.dataset[self.n_data_points:] # removes the first "self.n_data_points" values from the dataset. 707 | logging.debug(len(self.dataset)) 708 | self.plot_frame.dataset = self.dataset 709 | #self.clear_dataset() # doesn't make any difference 710 | self.plot_frame.dataset_changed = True # if we remove a part of the dataset, it is indeed changing. 711 | logging.debug("dataset_length after removing some points") 712 | logging.debug(len(self.dataset)) 713 | 714 | def on_serial_timer(self): 715 | if(self.parsing_style == "arduino"): 716 | self.add_arduino_data() 717 | elif(self.parsing_style == "emg"): 718 | self.add_emg_sensor_data() 719 | elif(self.parsing_style == "emg_new"): 720 | self.add_emg_new_sensor_data() 721 | 722 | def setup_slave(self): # READ/WRITE CONFIG this method performs all the tasks required to write and request data to/from slave 723 | print("setting up slave device") 724 | # depending on the parsing style, we identify different remote devices and data formats # 725 | if(self.parsing_style == "arduino"): # just writes the ASCII formated data, usually associted with the TEENSY device, or for any other GENERIC device (compatible with Arduino plotter) 726 | pass # no configuration to be done here (at least for now) 727 | elif(self.parsing_style == "emg"): 728 | # #self.send_serial("N?") # this command requests number of sensors in the remote device 729 | # self.send_serial("E=1") # ENABLES EMG data 730 | # self.send_serial("START") # STARTS COLLECTING EMG data 731 | pass 732 | elif(self.parsing_style == "emg_new"): 733 | print("configuring the emg_new device") 734 | print("reading the number of sensors") 735 | self.send_serial("N?") 736 | n_sensors = self.serial_port.readline() 737 | print(n_sensors) 738 | # for i in range(1,100): 739 | # n_sensors = self.serial_port.read(500) 740 | # print(n_sensors) 741 | 742 | 743 | # UNCOMMENT THESE TWO LINES WHEN FINISHED DEBUGGING !!!# 744 | self.send_serial("E=1") # ENABLES EMG data 745 | self.send_serial("START") # STARTS COLLECTING EMG data 746 | pass 747 | 748 | # read variable nSensors 749 | 750 | def add_arduino_data(self): 751 | 752 | byte_buffer = '' 753 | mid_buffer = '' 754 | 755 | try: 756 | byte_buffer = self.serial_port.read(SERIAL_BUFFER_SIZE) # up to 1000 or as much as in buffer. 757 | except Exception as e: 758 | self.on_port_error(e) 759 | self.on_button_disconnect_click() # we've crashed the serial, so disconnect and REFRESH PORTS!!! 760 | else: # if except doens't happen 761 | try: 762 | mid_buffer = byte_buffer.decode('utf-8') # SHOULDN'T THIS BE PARSING ALREADY??? 763 | except Exception as e: 764 | print(SEPARATOR) 765 | # print(e) 766 | self.on_port_error(e) 767 | else: 768 | self.read_buffer = self.read_buffer + mid_buffer 769 | data_points = self.read_buffer.split(self.endline) 770 | self.read_buffer = data_points[-1] # clean the buffer, saving the non completed data_points 771 | a = data_points[:-1] 772 | for data_point in a: # so all data points except last. 773 | self.arduino_parse(data_point) 774 | 775 | def arduino_parse(self,readed): # perform data processing as required (START WITH ARDUINO STYLE, AND ADD OTHER STYLES).# 776 | 777 | vals = readed.replace(' ',',') # replace empty spaces for commas. 778 | vals = vals.replace(':',',') # fast fix for inline labels incompatibility. MAKE IT BETTER !!! 779 | vals = vals.split(',') # arduino serial plotter splits with both characters. 780 | 781 | valsf = [] 782 | 783 | self.plot_frame.n_plots = 5 784 | 785 | if(vals[0] == ''): 786 | self.timeouts = self.timeouts + 1 787 | print("Timeout") 788 | print("Total number of timeouts: "+ str(self.timeouts)) 789 | else: 790 | # for val in vals: 791 | # try: 792 | # valsf.append(float(val)) 793 | # except: 794 | # logging.debug("It contains also text"); 795 | # # add to a captions vector 796 | # text_vals = vals 797 | # self.plot_frame.set_channels_labels(text_vals) # this sets all labels, I need to set them independently !!! 798 | # else: 799 | # self.add_values_to_dataset(valsf) 800 | text_vals = [] 801 | for val in vals: 802 | try: 803 | valsf.append(float(val)) 804 | except: 805 | # logging.debug("It contains also text") 806 | # add to a captions vector 807 | text_vals.append(val) 808 | self.plot_frame.set_channels_labels(text_vals) 809 | else: 810 | self.add_values_to_dataset(valsf) 811 | 812 | self.plot_frame.update() 813 | #print("dataset_changed = "+ str(self.plot_frame.graph.dataset_changed)) 814 | 815 | def add_values_to_dataset(self,values): 816 | # print("values =") 817 | # print(values) 818 | self.dataset.append(values) # appends all channels together 819 | # enabling corresponding toggles # 820 | for i in range(my_graph.MAX_PLOTS): # this may not be the greatest option. it's fine. 821 | try: 822 | a = values[i] # should crash after 4th element CHEAP FIX, MAKE IT BETTER !!! 823 | # self.dataset[i].append(valsf[i]) # if valsf has only 4 elements, it will throw error at 5th 824 | self.plot_frame.toggles[i].setEnabled(True) # enable all graphs conataining data 825 | 826 | if ( 827 | self.first_toggles <= 2): # THIS SHOULD BE IF NO DATA ON DATASET[I], OR ONLY ONE ELEMENT ON DATASET[I] 828 | self.plot_frame.toggles[i].setChecked(True) 829 | except: 830 | pass 831 | 832 | self.first_toggles = self.first_toggles + 1 # ??? 833 | 834 | def add_emg_sensor_data(self): # reads the data in the specific binary format of the emg sensor 835 | 836 | byte_buffer = [] 837 | num_buffer = [] 838 | 839 | try: 840 | byte_buffer = self.serial_port.read(SERIAL_BUFFER_SIZE) # up to 1000 or as much as in buffer. 841 | except Exception as e: 842 | self.on_port_error(e) 843 | self.on_button_disconnect_click() # we've crashed the serial, so disconnect and REFRESH PORTS!!! 844 | else: # if except doens't happen 845 | # print("byte_buffer:") 846 | # print(byte_buffer) 847 | 848 | try: 849 | for byte in byte_buffer: 850 | num = int(byte) 851 | num_buffer.append(num) 852 | # print("num_buffer:") 853 | # print(num_buffer) 854 | # here we will have a buffer with lots of numbers (32,45,33,0,45,54,33,0,32...) 855 | except Exception as e: 856 | print("ERROR: -------------------------") 857 | print(e) 858 | print(SEPARATOR) 859 | self.on_port_error(e) 860 | else: 861 | self.read_buffer = self.read_buffer + num_buffer # read buffer contains what was left from previous iteration 862 | data_points = self.split_number_array(self.read_buffer,0) # we split the buffer on 'number 0' received (end of data_points) 863 | self.read_buffer = data_points[-1] # clean the buffer, saving the non completed data_points 864 | a = data_points[:-1] # get all the completed ones 865 | for data_point in a: # so all data points except last. 866 | if len(data_point) == 4: # FIX THIS!: we expect 4 data values per data point, if not, it means corrupted data. 867 | self.add_values_to_dataset(data_point) 868 | 869 | # HERE IS WHERE WE GET THE PROBLEMATIC DATA POINTS, SO THE RIGHT PLACE TO FIX IF THERE AREN'T ENOUGH POINTS. 870 | else: 871 | for i in range(int(len(data_point)/4)+1): 872 | self.add_values_to_dataset([0,0,0,0]) # this indicates error code, data isn't having the 4 expected values 873 | 874 | 875 | 876 | 877 | pass 878 | self.plot_frame.update() 879 | # num = True 880 | # vals = [] 881 | # while(num != 0): 882 | # byte = self.serial_port.read() 883 | # num = int.from_bytes(byte, byteorder='big', signed=False) # decoding to store in file 884 | # num = float(num) 885 | # vals.append(num) 886 | # vals = vals[:-1] # remove the final zero 887 | # if(len(vals) > 1): # bad trick to remove zero data value array !!! 888 | # self.add_values_to_dataset(vals) 889 | # self.plot_frame.update() 890 | # #return(vals) 891 | def add_emg_new_sensor_data(self): # reads the data in the specific binary format of the emg sensor 892 | #print("add_emg_new_sensor_data") 893 | byte_buffer = [] 894 | num_buffer = [] 895 | 896 | try: 897 | byte_buffer = self.serial_port.read(SERIAL_BUFFER_SIZE) # up to 1000 or as much as in buffer. 898 | except Exception as e: 899 | self.on_port_error(e) 900 | self.on_button_disconnect_click() # we've crashed the serial, so disconnect and REFRESH PORTS!!! 901 | else: # if except doens't happen 902 | # print("byte_buffer:") 903 | # print(byte_buffer) 904 | 905 | try: 906 | for byte in byte_buffer: 907 | num = int(byte) 908 | num_buffer.append(num) 909 | # print("num_buffer:") 910 | # print(num_buffer) 911 | # here we will have a buffer with lots of numbers (32,45,33,0,45,54,33,0,32...) 912 | except Exception as e: 913 | print("ERROR: -------------------------") 914 | print(e) 915 | print(SEPARATOR) 916 | self.on_port_error(e) 917 | else: 918 | self.read_buffer = self.read_buffer + num_buffer # read buffer contains what was left from previous iteration 919 | data_points = self.split_number_array(self.read_buffer,0xFF) # we split the buffer on 'number FF' received (end of data_points) 920 | self.read_buffer = data_points[-1] # clean the buffer, saving the non completed data_points 921 | a = data_points[:-1] # get all the completed ones 922 | for data_point in a: # so all data points except last. 923 | if len(data_point) == 4: # FIX THIS!: we expect 4 data values per data point, if not, it means corrupted data. 924 | self.add_values_to_dataset(data_point) 925 | pass 926 | # HERE IS WHERE WE GET THE PROBLEMATIC DATA POINTS, SO THE RIGHT PLACE TO FIX IF THERE AREN'T ENOUGH POINTS. 927 | else: 928 | for i in range(int(len(data_point)/4)+1): 929 | self.add_values_to_dataset([0,0,0,0]) # this indicates error code, data isn't having the 4 expected values 930 | 931 | self.plot_frame.update() 932 | 933 | def split_number_array(self, array, separator): 934 | results = [] 935 | res = [] 936 | for val in array: 937 | # print(res) 938 | # print(val) 939 | if val is not separator: 940 | res.append(val) 941 | else: 942 | results.append(res) 943 | res = [] 944 | results.append(res) # what's left on the res after the separator 945 | 946 | return(results) 947 | 948 | def emg_parse(self): 949 | pass 950 | 951 | #num = int.from_bytes(byte, byteorder='big', signed=False) # decoding to store in file 952 | 953 | def init_dataset(self): 954 | self.dataset = [] 955 | 956 | def clear_dataset(self): 957 | # initializing empty dataset # 958 | self.dataset = [] 959 | 960 | def on_port_error(self,e): # triggered by the serial thread, shows a window saying port is used by sb else. 961 | 962 | desc = str(e) 963 | logging.debug(type(e)) 964 | logging.debug(desc) 965 | error_type = None 966 | i = desc.find("Port is already open.") 967 | if(i != -1): 968 | print("PORT ALREADY OPEN BY THIS APPLICATION") 969 | error_type = 1 970 | logging.debug(i) 971 | i = desc.find("FileNotFoundError") 972 | if(i != -1): 973 | logging.debug("DEVICE IS NOT CONNECTED, EVEN THOUGH PORT IS LISTED") 974 | error_type = 2 # 975 | i = desc.find("PermissionError") 976 | if(i != -1): 977 | logging.debug("SOMEONE ELSE HAS OPEN THE PORT") 978 | error_type = 3 # shows dialog the por is used (better mw or thread?) --> MW, IT'S GUI. 979 | 980 | i = desc.find("OSError") 981 | if(i != -1): 982 | logging.debug("BLUETOOTH DEVICE NOT REACHABLE ?") 983 | error_type = 4 984 | 985 | i = desc.find("ClearCommError") 986 | if(i != -1): 987 | logging.debug("DEVICE CABLE UNGRACEFULLY DISCONNECTED") 988 | error_type = 5 989 | 990 | 991 | 992 | # ~ i = desc.find("'utf-8' codec can't decode byte") # NOT WORKING !!! (GIVING MORE ISSUES THAN IT SOLVED) 993 | # ~ if(i != -1): 994 | # ~ logging.debug("WRONG SERIAL BAUDRATE?") 995 | # ~ error_type = 6 996 | 997 | self.error_type = error_type 998 | 999 | # ~ logging.debug("Error on serial port opening detected: ") 1000 | # ~ logging.debug(self.error_type) 1001 | self.handle_errors_flag = True # more global variables to fuck things up even more. 1002 | self.handle_port_errors() 1003 | 1004 | def handle_port_errors(self): # made a trick, port_errors is a class variable (yup, dirty as fuck !!!) 1005 | 1006 | if(self.error_type == 1): # this means already open, should never happen. 1007 | logging.warning("ERROR TYPE 1") 1008 | d = QMessageBox.critical( 1009 | self, 1010 | "Serial port Blocked", 1011 | "The serial port selected is in use by other application", 1012 | buttons=QMessageBox.Ok 1013 | ) 1014 | if(self.error_type == 2): # this means device not connected 1015 | logging.warning("ERROR TYPE 2") 1016 | d = QMessageBox.critical( 1017 | self, 1018 | "Serial Device is not connected", 1019 | "Device not connected.\n Please check your cables/connections. ", 1020 | buttons=QMessageBox.Ok 1021 | ) 1022 | if(self.error_type == 3): # this means locked by sb else. 1023 | d = QMessageBox.critical( 1024 | self, 1025 | "Serial port Blocked", 1026 | "The serial port selected is in use by other application. ", 1027 | buttons=QMessageBox.Ok 1028 | ) 1029 | 1030 | self.on_button_disconnect_click() # resetting to the default "waiting for connect" situation 1031 | self.handle_errors_flag = False 1032 | if(self.error_type == 4): # this means device not connected 1033 | logging.warning("ERROR TYPE 4") 1034 | d = QMessageBox.critical( 1035 | self, 1036 | "Serial Device Unreachable", 1037 | "Serial device couldn't be reached,\n Bluetooth device too far? ", 1038 | buttons=QMessageBox.Ok 1039 | ) 1040 | if(self.error_type == 5): # this means device not connected 1041 | logging.warning("ERROR TYPE 5") 1042 | d = QMessageBox.critical( 1043 | self, 1044 | "Serial Cable disconnected while transmitting", 1045 | "Serial device was ungracefully disconnected, please check the cables", 1046 | buttons=QMessageBox.Ok 1047 | ) 1048 | if(self.error_type == 6): # this means device not connected 1049 | logging.warning("ERROR TYPE 6") 1050 | d = QMessageBox.critical( 1051 | self, 1052 | "Serial wrong decoding", 1053 | "There are problems decoding the data\n probably due to a wrong baudrate.", 1054 | buttons=QMessageBox.Ok 1055 | ) 1056 | self.on_button_disconnect_click() # resetting to the default "waiting for connect" situation 1057 | self.handle_errors_flag = False 1058 | self.error_type = None # cleaning unhnandled errors flags. 1059 | 1060 | # check all themes and use lambda functions may be an option to use more themes # 1061 | def set_dark_theme(self): 1062 | self.palette = pyqt_custom_palettes.dark_palette() 1063 | self.setPalette(self.palette) 1064 | self.plot_frame.setBackground([60,60,60]) 1065 | 1066 | def set_light_theme(self): 1067 | self.palette = pyqt_custom_palettes.light_palette() 1068 | self.setPalette(self.palette) 1069 | self.plot_frame.setBackground([220,220,220]) 1070 | 1071 | def set_re_theme(self): 1072 | self.palette = pyqt_custom_palettes.re_palette() 1073 | self.setPalette(self.palette) 1074 | 1075 | def set_arduino_parsing(self): 1076 | self.read_buffer = "" 1077 | self.parsing_style = "arduino" 1078 | 1079 | def set_emg_parsing(self): 1080 | self.read_buffer = [] 1081 | self.parsing_style = "emg" 1082 | 1083 | def set_emg_parsing_new(self): 1084 | self.read_buffer = [] 1085 | self.parsing_style = "emg_new" 1086 | 1087 | def set_plot_range(self): 1088 | logging.debug("set_range_action method called") 1089 | plot_range_dialog = RangeDialog() 1090 | 1091 | if plot_range_dialog.exec_(): 1092 | print(plot_range_dialog.getInputs()) 1093 | self.plot_frame.graph.setLimits(yMin=int(plot_range_dialog.getInputs()[0]), 1094 | yMax=int(plot_range_dialog.getInputs()[1])) 1095 | 1096 | def set_n_plot_points(self): 1097 | print("set_n_plot_points method called") 1098 | i, okPressed = QInputDialog.getText(self, "Set n points", "Number of plot points (recommended max. 5000):") 1099 | if okPressed: 1100 | self.n_data_points = int(i) 1101 | self.plot_frame.set_max_points(self.n_data_points) 1102 | 1103 | def update_serial_ports(self): # we update the list every time we go over the list of serial ports. 1104 | # here we need to add an entry for each serial port avaiable at the computer 1105 | # 1. How to get the list of available serial ports ? 1106 | 1107 | self.serial_port_menu.clear() # deletes all old actions on serial port menu 1108 | self.combo_serial_port.clear() 1109 | 1110 | 1111 | self.get_serial_ports() # meeded to list the serial ports at the menu 1112 | # 3. How to ensure which serial ports are available ? (grey out the unusable ones) 1113 | # 4. How to display properly each available serial port at the menu ? 1114 | logging.debug (self.serial_ports) 1115 | if self.serial_ports != []: 1116 | for port in self.serial_ports: 1117 | port_name = port[0] 1118 | logging.debug(port_name) 1119 | b = self.serial_port_menu.addAction(port_name) 1120 | # WE WON'T TRIGGER THE CONNECTION FROM THE BUTTON PUSH ANYMORE. 1121 | b.triggered.connect((lambda serial_connect, port_name=port_name: self.on_port_select(port_name))) # just need to add somehow the serial port name here, and we're done. 1122 | 1123 | # here we need to add the connect method to the action click, and its result 1124 | 1125 | for port in self.serial_ports: # same as adding all ports to action menu, but now using combo box. 1126 | port_name = port[0] 1127 | item = self.combo_serial_port.addItem(port_name) # add new items 1128 | 1129 | #b.currentTextChanged.connect((lambda serial_connect, port_name=port_name: self.on_port_select(port_name))) 1130 | 1131 | else: 1132 | self.noserials = self.serial_port_menu.addAction("No serial Ports detected") 1133 | self.noserials.setDisabled(True) 1134 | 1135 | def shortcut_preferences(self): 1136 | self.shortcuts = ShortcutsWidget() # needs to be self, or it won't persist 1137 | #0. should be done on init(): Load the shortcuts from a file where they're stored ¿in json format? 1138 | #1. get the current shortcuts (stored somewhere in a variable, which also needs to be created(USE DICTIONARY)) 1139 | #2. create a widget containing a table with all the shortcuts and their shortcut value. 1140 | # 1141 | 1142 | def full_screen(self): # it should be able to be detected from window class !!! 1143 | if self.full_screen_flag == False: 1144 | self.showFullScreen() 1145 | self.full_screen_flag = True 1146 | logging.debug("Full Screen ENABLED") 1147 | else: 1148 | self.showNormal() 1149 | self.full_screen_flag = False 1150 | logging.debug("Full Screen DISABLED") 1151 | 1152 | # KEYPRESS HANDLER FOR SHORTCUTS #### 1153 | def keyPressEvent(self, event): 1154 | if not event.isAutoRepeat(): 1155 | print(event.text()) 1156 | # FULL SHORTCUT LIST # 1157 | # arrows# 1158 | if event.key() == Qt.Key_Up: 1159 | self.on_arrow_up() 1160 | elif event.key() == Qt.Key_Down: 1161 | self.on_arrow_down() 1162 | elif event.key() == Qt.Key_Left: 1163 | self.on_arrow_left() 1164 | elif event.key() == Qt.Key_Right: 1165 | self.on_arrow_right() 1166 | #letters# 1167 | elif event.text() == 'f': 1168 | self.full_screen() 1169 | elif event.text() == 'c': 1170 | self.on_button_connect_click() 1171 | elif event.text() == 'd': 1172 | self.on_button_disconnect_click() 1173 | elif event.text() == 'u': 1174 | self.update_serial_ports() 1175 | elif event.text() == 'p': 1176 | self.on_button_pause() 1177 | elif event.text() == 'y': 1178 | self.on_button_play() 1179 | elif event.text() == 'r': 1180 | self.on_button_record() 1181 | elif event.text() == 's': 1182 | self.on_button_autoscale() 1183 | # numbers # 1184 | elif event.text() == 'º': 1185 | print("enable all") 1186 | self.plot_frame.check_toggles("all") # this can't be done 1187 | elif event.text() == '0': # toggles plots all/none 1188 | self.plot_frame.check_toggles("none") 1189 | 1190 | 1191 | 1192 | if __name__ == '__main__': 1193 | 1194 | app = QApplication(sys.argv) 1195 | app.setStyle("Fusion") # required to use it here 1196 | mw = MainWindow() 1197 | app.exec_() 1198 | --------------------------------------------------------------------------------