├── .github └── workflows │ └── python-demos.yml ├── .gitignore ├── LICENSE.txt ├── README.rst ├── WaveGeneratorArduino.py ├── WaveGeneratorArduino_qdev.py ├── cpp_Arduino_wave_generator ├── .gitignore ├── builds │ └── Adafruit_feather_m4_express │ │ └── CURRENT.UF2 ├── lib │ └── DvG_StreamCommand-1.1.1 │ │ ├── .clang-format │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── LICENSE.txt │ │ ├── README.md │ │ ├── developer_readme.txt │ │ ├── doxyfile.txt │ │ ├── examples │ │ ├── BinaryStreamCommand │ │ │ └── BinaryStreamCommand.ino │ │ └── StreamCommand │ │ │ └── StreamCommand.ino │ │ ├── library.properties │ │ ├── platformio.ini │ │ └── src │ │ ├── DvG_StreamCommand.cpp │ │ └── DvG_StreamCommand.h ├── platformio.ini └── src │ └── main.cpp ├── demo_A_GUI_full.py ├── demo_B_GUI_minimal.py ├── demo_C_singlethread_for_comparison.py ├── demo_D_no_GUI.py ├── demo_E_no_GUI.py ├── images └── Arduino_PyQt_demo_with_multithreading.PNG └── requirements.txt /.github/workflows/python-demos.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Demos 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 20 | qt-lib: [pyqt5, pyside2] 21 | env: 22 | DISPLAY: ':99.0' 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python ${{ matrix.python-version }} using ${{ matrix.qt-lib }} 27 | uses: actions/setup-python@v3 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils 33 | /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX 34 | python -m pip install --upgrade pip 35 | python -m pip install flake8 pytest pytest-cov 36 | python -m pip install ${{ matrix.qt-lib }} 37 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 38 | - name: Lint with flake8 39 | run: | 40 | # stop the build if there are Python syntax errors or undefined names 41 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 42 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 43 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 44 | - name: Test with pytest 45 | run: | 46 | # python demo_A_GUI_full.py simulate 47 | python demo_A_GUI_full.py 48 | python demo_B_GUI_minimal.py 49 | python demo_C_singlethread_for_comparison.py 50 | python demo_D_no_GUI.py simulate 51 | python demo_E_no_GUI.py simulate 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \\.git/ 2 | 3 | # Specific to this library 4 | last_used_port.txt 5 | port.txt 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 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | 111 | .pio/ 112 | .vscode/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Dennis P.M. van Gils] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |demos| |black| |license| 2 | 3 | .. |demos| image:: https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo/actions/workflows/python-demos.yml/badge.svg 4 | :target: https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo/actions/workflows/python-demos.yml 5 | .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 6 | :target: https://github.com/psf/black 7 | .. |license| image:: https://img.shields.io/badge/License-MIT-purple.svg 8 | :target: https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo/blob/master/LICENSE.txt 9 | 10 | Arduino PyQt multithread demo 11 | ============================= 12 | 13 | Demonstration of multithreaded communication, real-time plotting and logging of live Arduino data using PyQt/PySide and PyQtGraph. 14 | 15 | This demo needs just a bare Arduino(-like) device that, for demonstration purposes, will act as a numerical waveform generator. The source files are included and they should compile over a wide range of Arduino boards. Connect the Arduino to any USB port on your computer and run this Python demo. It should automatically find the Arduino and show you a fully functioning GUI with live data at a stable acquisition rate of 100 Hz. 16 | 17 | :: 18 | 19 | Alternatively, you can simulate the Arduino by running `python demo_A_GUI_full.py simulate`. 20 | 21 | It features a graphical user-interface, with a PyQtGraph plot for fast real-time plotting of data. The main thread handles the GUI and redrawing of the plot, another thread deals with acquiring data (DAQ) from the Arduino at a fixed rate and a third thread maintains a thread-safe queue where messages to be sent out to the Arduino are managed. 22 | 23 | 24 | .. image:: /images/Arduino_PyQt_demo_with_multithreading.PNG 25 | 26 | Supports PyQt5, PyQt6, PySide2 and PySide6, one of which you'll have to have 27 | installed already in your Python environment. 28 | 29 | Other depencies you'll need for this demo can be installed by running:: 30 | 31 | pip install -r requirements.txt 32 | 33 | PyQtGraph & OpenGL performance 34 | ============================== 35 | 36 | Enabling OpenGL for plotting within PyQtGraph has major benefits as the drawing will get offloaded to the GPU, instead of being handled by the CPU. However, when PyOpenGL is installed in the Python environment and OpenGL acceleration is enabled inside PyQtGraph as such in order to get full OpenGL support:: 37 | 38 | import pyqtgraph as pg 39 | pg.setConfigOptions(useOpenGL=True) 40 | pg.setConfigOptions(enableExperimental=True) 41 | 42 | the specific version of PyQtGraph will have a major influence on the timing stability of the DAQ routine, even though it is running in a separate dedicated thread. This becomes visible as a fluctuating time stamp in the recorded log file. Remarkably, I observe that ``PyQtGraph==0.11.1 leads to a superior timing stability`` of +/- 1 ms in the recorded time stamps, whereas ``0.12.x`` and ``0.13.1`` are very detrimental to the stability with values of +/- 20 ms. The cause for this lies in the different way that PyQtGraph v0.12+ handles `PlotDataItem()` with PyOpenGL. I suspect that the Python GIL (Global Interpreter Lock) is responsible for this, somehow. There is nothing I can do about that and hopefully this gets resolved in future PyQtGraph versions. 43 | 44 | Note to myself 45 | -------------- 46 | You can circumvent the DAQ stability issue by going to a more advanced scheme. It would involve having the Arduino perform measurements at a fixed rate *autonomously* and sending this data including the Arduino time stamp over to Python in chunks. The DAQ_worker inside Python should not be of type ``DAQ_TRIGGER.INTERNAL_TIMER`` as per this repo, i.e. Python should not act as a 'master' device, but rather should be of type ``DAQ_TRIGGER.CONTINUOUS`` to act as a slave to the Arduino and continously listen for its data. This advanced scheme is actually implemented in my `Arduino Lock-in Amplifier `__ project with good success. 47 | 48 | Recommendation 49 | -------------- 50 | 51 | ``Stick with pyqtgraph==0.11.1`` when OpenGL is needed and when consistent and high (> 10 Hz) DAQ rates are required. Unfortunately, ``0.11.1`` only supports PyQt5 or PySide2, not PyQt6 or PySide6 which get supported from of version ``0.12+``. 52 | 53 | Note: ``pyqtgraph==0.11.0`` has a line width issue with OpenGL curves and is stuck at 1 pixel, unless you apply `my monkeypatch `_. 54 | 55 | -------------------------------------------------------------------------------- /WaveGeneratorArduino.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Provides classes `WaveGeneratorArduino` and `FakeWaveGeneratorArduino`. 4 | """ 5 | __author__ = "Dennis van Gils" 6 | __authoremail__ = "vangils.dennis@gmail.com" 7 | __url__ = "https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo" 8 | __date__ = "11-06-2024" 9 | __version__ = "9.0" 10 | 11 | import time 12 | import datetime 13 | 14 | import numpy as np 15 | 16 | from dvg_debug_functions import dprint, print_fancy_traceback as pft 17 | from dvg_devices.Arduino_protocol_serial import Arduino 18 | 19 | # ------------------------------------------------------------------------------ 20 | # WaveGeneratorArduino 21 | # ------------------------------------------------------------------------------ 22 | 23 | 24 | class WaveGeneratorArduino(Arduino): 25 | """Manages serial communication with an Arduino that is programmed as a wave 26 | generator.""" 27 | 28 | class State: 29 | """Container for the process and measurement variables of the wave 30 | generator Arduino.""" 31 | 32 | time_0 = np.nan 33 | """[s] Time at start of data acquisition""" 34 | time = np.nan 35 | """[s] Time at reading_1""" 36 | reading_1 = np.nan 37 | """[arbitrary units]""" 38 | 39 | def __init__( 40 | self, 41 | name="Ard", 42 | long_name="Arduino", 43 | connect_to_specific_ID="Wave generator", 44 | ): 45 | super().__init__( 46 | name=name, 47 | long_name=long_name, 48 | connect_to_specific_ID=connect_to_specific_ID, 49 | ) 50 | 51 | # Container for the process and measurement variables 52 | self.state = self.State 53 | 54 | """Remember, you might need a mutex for proper multithreading. If the 55 | state variables are not atomic or thread-safe, you should lock and 56 | unlock this mutex for each read and write operation. In this demo we 57 | don't need it, hence it's commented out but I keep it as a reminder.""" 58 | # self.mutex = QtCore.QMutex() 59 | 60 | def set_waveform_to_sine(self): 61 | """Send the instruction to the Arduino to change to a sine wave.""" 62 | self.write("sine") 63 | 64 | def set_waveform_to_square(self): 65 | """Send the instruction to the Arduino to change to a square wave.""" 66 | self.write("square") 67 | 68 | def set_waveform_to_sawtooth(self): 69 | """Send the instruction to the Arduino to change to a sawtooth wave.""" 70 | self.write("sawtooth") 71 | 72 | def perform_DAQ(self) -> bool: 73 | """Query the Arduino for new readings, parse them and update the 74 | corresponding variables of its `state` member. 75 | 76 | Returns: True if successful, False otherwise. 77 | """ 78 | # We will catch any exceptions and report on them, but will deliberately 79 | # not reraise them. Design choice: The show must go on regardless. 80 | 81 | # Query the Arduino for its state 82 | success, reply = self.query_ascii_values("?", delimiter="\t") 83 | if not success: 84 | dprint( 85 | f"'{self.name}' reports IOError @ " 86 | f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" 87 | ) 88 | return False 89 | 90 | # Parse readings into separate state variables 91 | try: 92 | ( # pylint: disable=unbalanced-tuple-unpacking 93 | self.state.time, 94 | self.state.reading_1, 95 | ) = reply 96 | self.state.time /= 1000 # Transform [ms] to [s] 97 | except Exception as err: # pylint: disable=broad-except 98 | pft(err, 3) 99 | dprint( 100 | f"'{self.name}' reports IOError @ " 101 | f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" 102 | ) 103 | return False 104 | 105 | return True 106 | 107 | 108 | # ------------------------------------------------------------------------------ 109 | # FakeWaveGeneratorArduino 110 | # ------------------------------------------------------------------------------ 111 | 112 | 113 | class FakeWaveGeneratorArduino: 114 | """Simulates via software the serial communication with a faked Arduino that 115 | is programmed as a wave generator. Mimics class `WaveGeneratorArduino`. 116 | """ 117 | 118 | class State: 119 | """Container for the process and measurement variables of the wave 120 | generator Arduino.""" 121 | 122 | time_0 = np.nan # [s] Start of data acquisition 123 | time = np.nan # [s] 124 | reading_1 = np.nan # [arbitrary units] 125 | 126 | def __init__(self): 127 | self.serial_settings = {} 128 | self.name = "FakeArd" 129 | self.long_name = "FakeArduino" 130 | self.is_alive = True 131 | 132 | # Container for the process and measurement variables 133 | self.state = self.State 134 | 135 | self.wave_freq = 0.3 # [Hz] 136 | self.wave_type = "sine" 137 | 138 | def set_waveform_to_sine(self): 139 | """Send the instruction to the Arduino to change to a sine wave.""" 140 | self.wave_type = "sine" 141 | 142 | def set_waveform_to_square(self): 143 | """Send the instruction to the Arduino to change to a sine wave.""" 144 | self.wave_type = "square" 145 | 146 | def set_waveform_to_sawtooth(self): 147 | """Send the instruction to the Arduino to change to a sine wave.""" 148 | self.wave_type = "sawtooth" 149 | 150 | def perform_DAQ(self) -> bool: 151 | """Query the Arduino for new readings, parse them and update the 152 | corresponding variables of its `state` member. 153 | 154 | Returns: True if successful, False otherwise. 155 | """ 156 | t = time.perf_counter() 157 | 158 | if self.wave_type == "sine": 159 | value = np.sin(2 * np.pi * self.wave_freq * t) 160 | elif self.wave_type == "square": 161 | value = 1 if np.mod(self.wave_freq * t, 1.0) > 0.5 else -1 162 | elif self.wave_type == "sawtooth": 163 | value = 2 * np.mod(self.wave_freq * t, 1.0) - 1 164 | 165 | self.state.time = t * 1000 166 | self.state.reading_1 = value 167 | 168 | return True 169 | 170 | def close(self): 171 | """Close the serial connection to the Arduino.""" 172 | return 173 | 174 | def auto_connect(self): 175 | """Auto connect to the Arduino via serial.""" 176 | return True 177 | -------------------------------------------------------------------------------- /WaveGeneratorArduino_qdev.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Provides class `WaveGeneratorArduino_qdev`. 4 | """ 5 | __author__ = "Dennis van Gils" 6 | __authoremail__ = "vangils.dennis@gmail.com" 7 | __url__ = "https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo" 8 | __date__ = "11-06-2024" 9 | __version__ = "9.0" 10 | 11 | from typing import Union, Callable 12 | 13 | from qtpy import QtCore 14 | 15 | from dvg_qdeviceio import QDeviceIO, DAQ_TRIGGER 16 | from WaveGeneratorArduino import WaveGeneratorArduino, FakeWaveGeneratorArduino 17 | 18 | # ------------------------------------------------------------------------------ 19 | # WaveGeneratorArduino_qdev 20 | # ------------------------------------------------------------------------------ 21 | 22 | 23 | class WaveGeneratorArduino_qdev(QDeviceIO): 24 | """Manages multithreaded communication and periodical data acquisition 25 | for an Arduino that is programmed as a wave generator, referred to as the 26 | 'device'.""" 27 | 28 | def __init__( 29 | self, 30 | dev: Union[WaveGeneratorArduino, FakeWaveGeneratorArduino], 31 | DAQ_function: Union[Callable[[], bool], None] = None, 32 | DAQ_interval_ms=10, 33 | DAQ_timer_type=QtCore.Qt.TimerType.PreciseTimer, 34 | critical_not_alive_count=1, 35 | debug=False, 36 | **kwargs, 37 | ): 38 | super().__init__(dev, **kwargs) # Pass kwargs onto QtCore.QObject() 39 | self.dev: WaveGeneratorArduino # Enforce type: removes `_NoDevice()` 40 | 41 | self.create_worker_DAQ( 42 | DAQ_trigger=DAQ_TRIGGER.INTERNAL_TIMER, 43 | DAQ_function=DAQ_function, 44 | DAQ_interval_ms=DAQ_interval_ms, 45 | DAQ_timer_type=DAQ_timer_type, 46 | critical_not_alive_count=critical_not_alive_count, 47 | debug=debug, 48 | ) 49 | self.create_worker_jobs(debug=debug) 50 | 51 | def request_set_waveform_to_sine(self): 52 | """Request sending out a new instruction to the Arduino to change to a 53 | sine wave.""" 54 | self.send(self.dev.set_waveform_to_sine) 55 | 56 | def request_set_waveform_to_square(self): 57 | """Request sending out a new instruction to the Arduino to change to a 58 | square wave.""" 59 | self.send(self.dev.set_waveform_to_square) 60 | 61 | def request_set_waveform_to_sawtooth(self): 62 | """Request sending out a new instruction to the Arduino to change to a 63 | sawtooth wave.""" 64 | self.send(self.dev.set_waveform_to_sawtooth) 65 | -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/builds/Adafruit_feather_m4_express/CURRENT.UF2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo/f29fe0a628e5e951da168a038fe75d247860b125/cpp_Arduino_wave_generator/builds/Adafruit_feather_m4_express/CURRENT.UF2 -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/.clang-format: -------------------------------------------------------------------------------- 1 | { BasedOnStyle: LLVM, UseTab: Never, IndentWidth: 2, TabWidth: 2, BreakBeforeBraces: Attach, AllowShortBlocksOnASingleLine: Empty, AllowShortIfStatementsOnASingleLine: true, IndentCaseLabels: true, ColumnLimit: 80, AccessModifierOffset: -2, NamespaceIndentation: All, IndentPPDirectives: AfterHash} -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore custom symlinks to develop this library inside VSCode 2 | src/main.cpp 3 | 4 | # VSCode IDE temp files 5 | .vscode 6 | .pio 7 | 8 | # Prerequisites 9 | *.d 10 | 11 | # Compiled Object files 12 | *.slo 13 | *.lo 14 | *.o 15 | *.obj 16 | 17 | # Precompiled Headers 18 | *.gch 19 | *.pch 20 | 21 | # Compiled Dynamic libraries 22 | *.so 23 | *.dylib 24 | *.dll 25 | 26 | # Fortran module files 27 | *.mod 28 | *.smod 29 | 30 | # Compiled Static libraries 31 | *.lai 32 | *.la 33 | *.a 34 | *.lib 35 | 36 | # Executables 37 | *.exe 38 | *.out 39 | *.app 40 | -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.9" 4 | 5 | # Cache PlatformIO packages using Travis CI container-based infrastructure 6 | cache: 7 | directories: 8 | - "~/.platformio" 9 | - $HOME/.cache/pip 10 | 11 | env: 12 | - PLATFORMIO_CI_SRC=examples/StreamCommand/StreamCommand.ino 13 | - PLATFORMIO_CI_SRC=examples/BinaryStreamCommand/BinaryStreamCommand.ino 14 | 15 | install: 16 | - pip install -U platformio 17 | - pio update 18 | 19 | script: 20 | - pio ci --lib="." --project-conf=platformio.ini 21 | -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Dennis P.M. van Gils] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/README.md: -------------------------------------------------------------------------------- 1 | # DvG_StreamCommand ![Latest release](https://img.shields.io/github/v/release/Dennis-van-Gils/DvG_StreamCommand) [![Build Status](https://travis-ci.com/Dennis-van-Gils/DvG_StreamCommand.svg?branch=main)](https://app.travis-ci.com/github/Dennis-van-Gils/DvG_StreamCommand) [![License:MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://github.com/Dennis-van-Gils/DvG_StreamCommand/blob/master/LICENSE.txt) [![Documentation](https://img.shields.io/badge/Docs-Doxygen-blue)](https://dennis-van-gils.github.io/DvG_StreamCommand) 2 | 3 | A lightweight Arduino library to listen for commands over a stream. 4 | 5 | It provides two classes to allow listening to a stream, such as Serial or Wire, for incoming commands (or data packets in general) and act upon them. Class `DvG_StreamCommand` will listen for ASCII commands, while class `DvG_BinaryStreamCommand` will listen for binary commands. 6 | 7 | The API documentation and examples can be found here: https://dennis-van-gils.github.io/DvG_StreamCommand -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/developer_readme.txt: -------------------------------------------------------------------------------- 1 | # I developed this library using PlatformIO inside VSCode. The folder structure 2 | # required by PlatformIO differs from how an Arduino library package should be 3 | # structured like. Hence, I'll use symbolic file and folder links to 'fool' 4 | # VSCode, such that it can find this libraries' .cpp and .h files and will be 5 | # able to compile the examples. 6 | # 7 | # Dennis van Gils, 29-08-2022 8 | 9 | # Windows cmd command: 10 | mklink /H src\main.cpp examples\StreamCommand\StreamCommand.ino 11 | 12 | # Or Windows PowerShell: 13 | New-Item -ItemType Junction -Path "src\main.cpp" -Target "examples\StreamCommand\StreamCommand.ino" 14 | 15 | # Also, remember to put `.nojekyll` file inside \docs folder, otherwise the 16 | html pages containing the source code will disappear because they start with the 17 | '_' character. Jekyll will filter out files starting with '_'. -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/examples/BinaryStreamCommand/BinaryStreamCommand.ino: -------------------------------------------------------------------------------- 1 | /** @example BinaryStreamCommand.ino 2 | 3 | Listen to the serial port for binary commands and act upon them. 4 | 5 | A binary command is a byte stream ending in a specific order as set by the 6 | end-of-line (EOL) sentinel. 7 | 8 | For convenience, this demo has the EOL set to '!!' such that you can type these 9 | ASCII characters in a terminal to terminate the command. This demo will simply 10 | print all received bytes back to the terminal once a complete command has been 11 | received. 12 | */ 13 | 14 | #include "DvG_StreamCommand.h" 15 | #include 16 | 17 | // Instantiate serial port listener for receiving binary commands 18 | const uint8_t BIN_BUF_LEN = 16; // Length of the binary command buffer 19 | uint8_t bin_buf[BIN_BUF_LEN]; // The binary command buffer 20 | const uint8_t EOL[] = {0x21, 0x21}; // End-of-line sentinel, 0x21 = '!' 21 | DvG_BinaryStreamCommand bsc(Serial, bin_buf, BIN_BUF_LEN, EOL, sizeof(EOL)); 22 | 23 | void setup() { Serial.begin(9600); } 24 | 25 | void loop() { 26 | // Poll the Serial stream for incoming bytes 27 | int8_t bsc_available = bsc.available(); 28 | 29 | if (bsc_available == -1) { 30 | Serial.println("Buffer has overrun and bytes got dropped."); 31 | bsc.reset(); 32 | 33 | } else if (bsc_available) { 34 | // A new command is available --> Get the number of bytes and act upon it 35 | uint16_t data_len = bsc.getCommandLength(); 36 | 37 | // Simply print all received bytes back to the terminal 38 | Serial.println("Received command bytes:"); 39 | for (uint16_t idx = 0; idx < data_len; ++idx) { 40 | Serial.print((char)bin_buf[idx]); 41 | Serial.write('\t'); 42 | Serial.print(bin_buf[idx], DEC); 43 | Serial.write('\t'); 44 | Serial.println(bin_buf[idx], HEX); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/examples/StreamCommand/StreamCommand.ino: -------------------------------------------------------------------------------- 1 | /** @example StreamCommand.ino 2 | 3 | Listen to the serial port for ASCII commands and act upon them. 4 | 5 | An ASCII command is any string ending with a newline '\\n' character. 6 | 7 | The following demo commands exist: 8 | - 'on' : Turn onboard LED on. 9 | - 'off' : Turn onboard LED off. 10 | - 'set ##.##': Print the passed value back to the terminal, interpreted as a 11 | float, integer and boolean. 12 | */ 13 | 14 | #include "DvG_StreamCommand.h" 15 | #include 16 | 17 | // Instantiate serial port listener for receiving ASCII commands 18 | const uint8_t CMD_BUF_LEN = 16; // Length of the ASCII command buffer 19 | char cmd_buf[CMD_BUF_LEN]{'\0'}; // The ASCII command buffer 20 | DvG_StreamCommand sc(Serial, cmd_buf, CMD_BUF_LEN); 21 | 22 | void setup() { 23 | Serial.begin(9600); 24 | pinMode(PIN_LED, OUTPUT); 25 | digitalWrite(PIN_LED, LOW); 26 | } 27 | 28 | void loop() { 29 | // Poll the Serial stream for incoming characters and check if a new 30 | // completely received command is available 31 | if (sc.available()) { 32 | // A new command is available --> Get it and act upon it 33 | char *str_cmd = sc.getCommand(); 34 | 35 | Serial.print("Received: "); 36 | Serial.println(str_cmd); 37 | 38 | if (strcmp(str_cmd, "on") == 0) { 39 | Serial.println(" -> LED ON"); 40 | digitalWrite(PIN_LED, HIGH); 41 | 42 | } else if (strcmp(str_cmd, "off") == 0) { 43 | Serial.println(" -> LED OFF"); 44 | digitalWrite(PIN_LED, LOW); 45 | 46 | } else if (strncmp(str_cmd, "set", 3) == 0) { 47 | Serial.print(" As float : "); 48 | Serial.println(parseFloatInString(str_cmd, 3)); 49 | Serial.print(" As integer: "); 50 | Serial.println(parseIntInString(str_cmd, 3)); 51 | Serial.print(" As boolean: "); 52 | Serial.println(parseBoolInString(str_cmd, 3) ? "true" : "false"); 53 | 54 | } else { 55 | Serial.println(" Unknown command"); 56 | } 57 | 58 | Serial.println(""); 59 | } 60 | } -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/library.properties: -------------------------------------------------------------------------------- 1 | name=DvG_StreamCommand 2 | version=1.1.1 3 | author=Dennis van Gils 4 | maintainer=Dennis van Gils 5 | sentence=A lightweight Arduino library to listen for commands over a stream 6 | paragraph= 7 | category=Communication 8 | url=https://github.com/Dennis-van-Gils/DvG_StreamCommand/ 9 | architectures=* 10 | -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | default_envs = developer 13 | 14 | [env:developer] 15 | platform = atmelsam 16 | board = adafruit_feather_m4 17 | framework = arduino -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/src/DvG_StreamCommand.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DvG_StreamCommand.cpp 3 | * @author Dennis van Gils (vangils.dennis@gmail.com) 4 | * @version https://github.com/Dennis-van-Gils/DvG_StreamCommand 5 | * @version 1.1.0 6 | * @date 30-08-2022 7 | * 8 | * @mainpage A lightweight Arduino library to listen for commands over a stream. 9 | * 10 | * It provides two classes to allow listening to a stream, such as Serial or 11 | * Wire, for incoming commands (or data packets in general) and act upon them. 12 | * Class `DvG_StreamCommand` will listen for ASCII commands, while class 13 | * `DvG_BinaryStreamCommand` will listen for binary commands. 14 | * 15 | * @section Usage 16 | * Method `available()` should be called repeatedly to poll for characters or 17 | * bytes incoming to the stream. It will return true when a new completely 18 | * received command is ready to be processed by the user. See the examples @ref 19 | * StreamCommand.ino and @ref BinaryStreamCommand.ino. 20 | * 21 | * @section author Author 22 | * Dennis van Gils (vangils.dennis@gmail.com) 23 | * 24 | * @section version Version 25 | * - https://github.com/Dennis-van-Gils/DvG_StreamCommand 26 | * - v1.1.0 27 | * 28 | * @section Changelog 29 | * - v1.1.0 - Added method `reset()` 30 | * - v1.0.0 - Initial commit. This is the improved successor to 31 | * `DvG_SerialCommand`. 32 | * 33 | * @section license License 34 | * MIT License. See the LICENSE file for details. 35 | */ 36 | 37 | #include "DvG_StreamCommand.h" 38 | 39 | /******************************************************************************* 40 | DvG_StreamCommand 41 | *******************************************************************************/ 42 | 43 | DvG_StreamCommand::DvG_StreamCommand(Stream &stream, char *buffer, 44 | uint16_t max_len) 45 | : _stream(stream) // Initialize reference before body 46 | { 47 | _buffer = buffer; 48 | _max_len = max_len; 49 | reset(); 50 | } 51 | 52 | bool DvG_StreamCommand::available() { 53 | char c; 54 | 55 | // Poll the input buffer of the stream for data 56 | if (_stream.available()) { 57 | _fTerminated = false; 58 | while (_stream.available()) { 59 | c = _stream.peek(); 60 | 61 | if (c == 13) { 62 | // Ignore ASCII 13 (carriage return) 63 | _stream.read(); // Remove char from the stream input buffer 64 | 65 | } else if (c == 10) { 66 | // Found ASCII 10 (line feed) --> Terminate string 67 | _stream.read(); // Remove char from the stream input buffer 68 | _buffer[_cur_len] = '\0'; 69 | _fTerminated = true; 70 | break; 71 | 72 | } else if (_cur_len < _max_len - 1) { 73 | // Append char to string 74 | _stream.read(); // Remove char from the stream input buffer 75 | _buffer[_cur_len] = c; 76 | _cur_len++; 77 | 78 | } else { 79 | // Maximum buffer length is reached. Forcefully terminate the string 80 | // in the command buffer now. Leave the char in the stream input buffer. 81 | _buffer[_cur_len] = '\0'; 82 | _fTerminated = true; 83 | break; 84 | } 85 | } 86 | } 87 | 88 | return _fTerminated; 89 | } 90 | 91 | char *DvG_StreamCommand::getCommand() { 92 | if (_fTerminated) { 93 | _fTerminated = false; 94 | _cur_len = 0; 95 | return _buffer; 96 | 97 | } else { 98 | return (char *)_empty; 99 | } 100 | } 101 | 102 | /******************************************************************************* 103 | DvG_BinaryStreamCommand 104 | *******************************************************************************/ 105 | 106 | DvG_BinaryStreamCommand::DvG_BinaryStreamCommand(Stream &stream, 107 | uint8_t *buffer, 108 | uint16_t max_len, 109 | const uint8_t *EOL, 110 | uint8_t EOL_len) 111 | : _stream(stream) // Initialize reference before body 112 | { 113 | _buffer = buffer; 114 | _max_len = max_len; 115 | _EOL = EOL; 116 | _EOL_len = EOL_len; 117 | reset(); 118 | } 119 | 120 | int8_t DvG_BinaryStreamCommand::available(bool debug_info) { 121 | uint8_t c; 122 | 123 | // Poll the input buffer of the stream for data 124 | while (_stream.available()) { 125 | c = _stream.read(); 126 | if (debug_info) { 127 | _stream.print(c, HEX); 128 | _stream.write('\t'); 129 | } 130 | 131 | if (_cur_len < _max_len) { 132 | _buffer[_cur_len] = c; 133 | _cur_len++; 134 | } else { 135 | // Maximum buffer length is reached. Drop the byte and return the special 136 | // value of -1 to signal the user. 137 | return -1; 138 | } 139 | 140 | // Check for EOL at the end 141 | if (_cur_len >= _EOL_len) { 142 | _found_EOL = true; 143 | for (uint8_t i = 0; i < _EOL_len; ++i) { 144 | if (_buffer[_cur_len - i - 1] != _EOL[_EOL_len - i - 1]) { 145 | _found_EOL = false; 146 | break; // Any mismatch will exit early 147 | } 148 | } 149 | if (_found_EOL) { 150 | // Wait with reading in more bytes from the stream input buffer to let 151 | // the user act upon the currently received command 152 | if (debug_info) { 153 | _stream.print("EOL\t"); 154 | } 155 | break; 156 | } 157 | } 158 | } 159 | 160 | return _found_EOL; 161 | } 162 | 163 | uint16_t DvG_BinaryStreamCommand::getCommandLength() { 164 | uint16_t len; 165 | 166 | if (_found_EOL) { 167 | len = _cur_len - _EOL_len; 168 | _found_EOL = false; 169 | _cur_len = 0; 170 | 171 | } else { 172 | len = 0; 173 | } 174 | 175 | return len; 176 | } 177 | 178 | /******************************************************************************* 179 | Parse functions 180 | *******************************************************************************/ 181 | 182 | float parseFloatInString(const char *str_in, uint16_t pos) { 183 | if (strlen(str_in) > pos) { 184 | return (float)atof(&str_in[pos]); 185 | } else { 186 | return 0.0f; 187 | } 188 | } 189 | 190 | bool parseBoolInString(const char *str_in, uint16_t pos) { 191 | if (strlen(str_in) > pos) { 192 | if (strncmp(&str_in[pos], "true", 4) == 0 || // 193 | strncmp(&str_in[pos], "True", 4) == 0 || // 194 | strncmp(&str_in[pos], "TRUE", 4) == 0) { 195 | return true; 196 | } 197 | return (atoi(&str_in[pos]) != 0); 198 | } else { 199 | return false; 200 | } 201 | } 202 | 203 | int parseIntInString(const char *str_in, uint16_t pos) { 204 | if (strlen(str_in) > pos) { 205 | return atoi(&str_in[pos]); 206 | } else { 207 | return 0; 208 | } 209 | } -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/lib/DvG_StreamCommand-1.1.1/src/DvG_StreamCommand.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file DvG_StreamCommand.h 3 | * @author Dennis van Gils (vangils.dennis@gmail.com) 4 | * @brief A lightweight Arduino library to listen to a stream, such as Serial or 5 | * Wire, for incoming commands (or data packets in general) and act upon them. 6 | * @copyright MIT License. See the LICENSE file for details. 7 | */ 8 | 9 | #ifndef DVG_STREAMCOMMAND_H_ 10 | #define DVG_STREAMCOMMAND_H_ 11 | 12 | #include 13 | 14 | /******************************************************************************* 15 | DvG_StreamCommand 16 | *******************************************************************************/ 17 | 18 | /** 19 | * @brief Class to manage listening to a stream, such as Serial or Wire, for 20 | * incoming ASCII commands (or ASCII data packets in general) and act upon them. 21 | * 22 | * We speak of a completely received 'command' once a linefeed ('\\n', ASCII 10) 23 | * character is received, or else when the number of incoming characters has 24 | * exceeded the command buffer size. Carriage return ('\\r', ASCII 13) 25 | * characters are ignored from the stream. 26 | * 27 | * The command buffer is supplied by the user and must be a fixed-size character 28 | * array (C-string, i.e. '\0' terminated) to store incoming characters into. 29 | * This keeps the memory usage low and unfragmented, instead of relying on 30 | * memory hungry C++ strings. 31 | */ 32 | class DvG_StreamCommand { 33 | public: 34 | /** 35 | * @brief Construct a new DvG_StreamCommand object. 36 | * 37 | * @param stream Reference to a stream to listen to, e.g. Serial, Wire, etc. 38 | * @param buffer Reference to the command buffer: A fixed-size character array 39 | * which will be managed by this class to hold a single incoming command. The 40 | * buffer should be one character larger than the longest incoming command to 41 | * allow for the C-string termination character '\0' to become appended. 42 | * @param max_len Array size of @p buffer including '\0'. Do not exceed the 43 | * maximum size of 2^^16 = 65536 characters. 44 | */ 45 | DvG_StreamCommand(Stream &stream, char *buffer, uint16_t max_len); 46 | 47 | /** 48 | * @brief Poll the stream for incoming characters and append them one-by-one 49 | * to the command buffer @p buffer. This method should be called repeatedly. 50 | * 51 | * @return True when a complete command has been received and is ready to be 52 | * returned by @ref getCommand(), false otherwise. 53 | */ 54 | bool available(); 55 | 56 | /** 57 | * @brief Return the reference to the command buffer only when a complete 58 | * command has been received. Otherwise, an empty C-string is returned. 59 | * 60 | * @return char* 61 | */ 62 | char *getCommand(); 63 | 64 | /** 65 | * @brief Empty the command buffer. 66 | */ 67 | inline void reset() { 68 | _fTerminated = false; 69 | _buffer[0] = '\0'; 70 | _cur_len = 0; 71 | } 72 | 73 | private: 74 | Stream &_stream; // Reference to the stream to listen to 75 | char *_buffer; // Reference to the command buffer 76 | uint16_t _max_len; // Array size of the command buffer 77 | uint16_t _cur_len; // Number of currently received command characters 78 | bool _fTerminated; // Has a complete command been received? 79 | const char *_empty = ""; // Empty reply, which is just the '\0' character 80 | }; 81 | 82 | /******************************************************************************* 83 | DvG_BinaryStreamCommand 84 | *******************************************************************************/ 85 | 86 | /** 87 | * @brief Class to manage listening to a stream, such as Serial or Wire, for 88 | * incoming binary commands (or binary data packets in general) and act upon 89 | * them. 90 | * 91 | * We speak of a completely received 'command' once a sequence of bytes is 92 | * received that matches the 'end-of-line' (EOL) sentinel. The EOL sentinel is 93 | * supplied by the user and should be a single sequence of bytes of fixed length 94 | * that is unique, i.e. will not appear anywhere inside of the command / data 95 | * packet that it is suffixing. 96 | * 97 | * The command buffer is supplied by the user and must be a fixed-size uint8_t 98 | * array to store incoming bytes into. 99 | */ 100 | class DvG_BinaryStreamCommand { 101 | public: 102 | /** 103 | * @brief Construct a new DvG_BinaryStreamCommand object. 104 | * 105 | * @param stream Reference to a stream to listen to, e.g. Serial, Wire, etc. 106 | * @param buffer Reference to the command buffer: A fixed-size uint8_t array 107 | * which will be managed by this class to hold a single incoming command. 108 | * @param max_len Array size of @p buffer. Do not exceed the maximum size of 109 | * 2^^16 = 65536 bytes. 110 | * @param EOL Reference to the end-of-line sentinel: A fixed-size uint8_t 111 | * array containing a unique sequence of bytes. 112 | * @param EOL_len Array size of @p EOL. Do not exceed the maximum size of 113 | * 2^^8 = 256 bytes. 114 | */ 115 | DvG_BinaryStreamCommand(Stream &stream, uint8_t *buffer, uint16_t max_len, 116 | const uint8_t *EOL, uint8_t EOL_len); 117 | 118 | /** 119 | * @brief Poll the stream for incoming bytes and append them one-by-one to the 120 | * command buffer @p buffer. This method should be called repeatedly. 121 | * 122 | * @param debug_info When true it will print debug information to @p stream 123 | * as a tab-delimited list of all received bytes in HEX format. WARNING: 124 | * Enabling this will likely interfere with downstream code listening in to 125 | * the @p stream for I/O, so use it only for troubleshooting while developing. 126 | * 127 | * @return 1 (true) when a complete command has been received and its size is 128 | * ready to be returned by @ref getCommandLength(). Otherwise 0 (false) when 129 | * no complete command has been received yet, or -1 (true!) as a special value 130 | * to indicate that the command buffer was overrun and that the exceeding byte 131 | * got dropped. 132 | */ 133 | int8_t available(bool debug_info = false); 134 | 135 | /** 136 | * @brief Return the length of the command without the EOL sentinel in bytes, 137 | * only when a complete command has been received. The received command can be 138 | * read from the user-supplied command buffer up to this length. Otherwise, 0 139 | * is returned. 140 | * 141 | * @return uint16_t 142 | */ 143 | uint16_t getCommandLength(); 144 | 145 | /** 146 | * @brief Empty the command buffer. 147 | */ 148 | inline void reset() { 149 | for (uint16_t i = 0; i < _max_len; ++i) { 150 | _buffer[i] = 0; 151 | } 152 | _found_EOL = false; 153 | _cur_len = 0; 154 | } 155 | 156 | private: 157 | Stream &_stream; // Reference to the stream to listen to 158 | uint8_t *_buffer; // Reference to the command buffer 159 | uint16_t _max_len; // Array size of the command buffer 160 | uint16_t _cur_len; // Number of currently received command bytes 161 | const uint8_t *_EOL; // Reference to the end-of-line sentinel 162 | uint16_t _EOL_len; // Array size of the end-of-line sentinel 163 | bool _found_EOL; // Has a complete command been received? 164 | }; 165 | 166 | /******************************************************************************* 167 | Parse functions 168 | *******************************************************************************/ 169 | 170 | /** 171 | * @brief Safely parse a float value in C-string @p str_in from of position 172 | * @p pos. 173 | * 174 | * @return The parsed float value when successful, 0.0 otherwise. 175 | */ 176 | float parseFloatInString(const char *str_in, uint16_t pos = 0); 177 | 178 | /** 179 | * @brief Safely parse a boolean value in C-string @p str_in from of position 180 | * @p pos. 181 | * 182 | * @return 183 | * - False, when @p str_in is empty or @p pos is past the @p str_in length. 184 | * - True, when the string perfectly matches 'true', 'True' or 'TRUE'. 185 | * - Else, it will interpret the string as an integer, where 0 is considered 186 | * to be false and all other integers are considered be true. Leading spaces, 187 | * zeros or signs will be ignored from the integer. 188 | */ 189 | bool parseBoolInString(const char *str_in, uint16_t pos = 0); 190 | 191 | /** 192 | * @brief Safely parse an integer value in C-string @p str_in from of position 193 | * @p pos. 194 | * 195 | * @return The parsed integer value when successful, 0 otherwise. 196 | */ 197 | int parseIntInString(const char *str_in, uint16_t pos = 0); 198 | 199 | #endif 200 | -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | [platformio] 11 | default_envs = adafruit_feather_m4 12 | 13 | [env:mzeropro] 14 | platform = atmelsam 15 | board = mzeropro 16 | framework = arduino 17 | 18 | [env:adafruit_feather_m4] 19 | platform = atmelsam 20 | board = adafruit_feather_m4 21 | framework = arduino 22 | upload_protocol = sam-ba 23 | lib_ldf_mode = chain+ 24 | 25 | [env:adafruit_itsybitsy_m4] 26 | platform = atmelsam 27 | board = adafruit_itsybitsy_m4 28 | framework = arduino -------------------------------------------------------------------------------- /cpp_Arduino_wave_generator/src/main.cpp: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Dennis van Gils 3 | 14-09-2022 4 | ******************************************************************************/ 5 | 6 | #include 7 | #include 8 | 9 | #include "DvG_StreamCommand.h" 10 | 11 | // On the Arduino M0 Pro: 12 | // Serial : Programming USB port 13 | // SerialUSB: Native USB port. Baudrate setting gets ignored and is always as 14 | // fast as possible. 15 | #define Ser Serial 16 | 17 | // Instantiate serial port listener for receiving ASCII commands 18 | const uint8_t CMD_BUF_LEN = 16; // Length of the ASCII command buffer 19 | char cmd_buf[CMD_BUF_LEN]{'\0'}; // The ASCII command buffer 20 | DvG_StreamCommand sc(Ser, cmd_buf, CMD_BUF_LEN); 21 | 22 | /*------------------------------------------------------------------------------ 23 | Setup 24 | ------------------------------------------------------------------------------*/ 25 | 26 | void setup() { Ser.begin(115200); } 27 | 28 | /*------------------------------------------------------------------------------ 29 | Loop 30 | ------------------------------------------------------------------------------*/ 31 | 32 | #define WAVE_SINE 1 33 | #define WAVE_SQUARE 2 34 | #define WAVE_SAWTOOTH 3 35 | 36 | uint8_t wave_type = WAVE_SINE; 37 | double wave_freq = 0.3; // [Hz] 38 | double wave = 0.0; 39 | 40 | uint32_t curMillis = millis(); 41 | uint32_t prevMillis = 0; 42 | 43 | void loop() { 44 | // Generate wave sample every millisecond 45 | curMillis = millis(); 46 | if (curMillis - prevMillis >= 1) { 47 | 48 | if (wave_type == WAVE_SINE) { 49 | wave = sin(2 * PI * wave_freq * curMillis / 1e3); 50 | } else if (wave_type == WAVE_SQUARE) { 51 | wave = (fmod(wave_freq * curMillis / 1e3, (double)(1.0)) > 0.5) ? 1 : -1; 52 | } else if (wave_type == WAVE_SAWTOOTH) { 53 | wave = 2 * fmod(wave_freq * curMillis / 1e3, (double)(1.0)) - 1; 54 | } 55 | 56 | prevMillis = curMillis; 57 | } 58 | 59 | // Poll the Serial stream for incoming characters and check if a new 60 | // completely received command is available 61 | if (sc.available()) { 62 | // A new command is available --> Get it and act upon it 63 | char *str_cmd = sc.getCommand(); 64 | 65 | if (strcmp(str_cmd, "id?") == 0) { 66 | Ser.println("Arduino, Wave generator"); 67 | 68 | } else if (strcmp(str_cmd, "sine") == 0) { 69 | wave_type = WAVE_SINE; 70 | } else if (strcmp(str_cmd, "square") == 0) { 71 | wave_type = WAVE_SQUARE; 72 | } else if (strcmp(str_cmd, "sawtooth") == 0) { 73 | wave_type = WAVE_SAWTOOTH; 74 | 75 | } else if (strcmp(str_cmd, "?") == 0) { 76 | Ser.print(curMillis); 77 | Ser.print('\t'); 78 | Ser.println(wave, 4); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /demo_A_GUI_full.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Demonstration of multithreaded real-time plotting and logging of live Arduino 4 | data using PyQt/PySide and PyQtGraph. 5 | """ 6 | __author__ = "Dennis van Gils" 7 | __authoremail__ = "vangils.dennis@gmail.com" 8 | __url__ = "https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo" 9 | __date__ = "11-06-2024" 10 | __version__ = "9.0" 11 | # pylint: disable=missing-function-docstring, unnecessary-lambda 12 | 13 | import os 14 | import sys 15 | import time 16 | import datetime 17 | 18 | import qtpy 19 | from qtpy import QtCore, QtGui, QtWidgets as QtWid 20 | from qtpy.QtCore import Slot # type: ignore 21 | 22 | import psutil 23 | import pyqtgraph as pg 24 | 25 | from dvg_debug_functions import tprint 26 | from dvg_pyqtgraph_threadsafe import HistoryChartCurve, PlotManager 27 | from dvg_pyqt_filelogger import FileLogger 28 | import dvg_pyqt_controls as controls 29 | 30 | from WaveGeneratorArduino import WaveGeneratorArduino, FakeWaveGeneratorArduino 31 | from WaveGeneratorArduino_qdev import WaveGeneratorArduino_qdev 32 | 33 | # Constants 34 | DAQ_INTERVAL_MS = 10 35 | """[ms] Update interval for the data acquisition (DAQ)""" 36 | CHART_INTERVAL_MS = 20 37 | """[ms] Update interval for the chart""" 38 | CHART_HISTORY_TIME = 10 39 | """[s] History length of the chart""" 40 | 41 | # Global flags 42 | TRY_USING_OPENGL = True 43 | USE_LARGER_TEXT = False 44 | """For demonstration on a beamer""" 45 | USE_PC_TIME = True 46 | """Use Arduino time or PC time?""" 47 | SIMULATE_ARDUINO = False 48 | """Simulate an Arduino in software?""" 49 | if sys.argv[-1] == "simulate": 50 | SIMULATE_ARDUINO = True 51 | 52 | DEBUG = False 53 | """Show debug info in terminal? Warning: Slow! Do not leave on unintentionally. 54 | """ 55 | 56 | print( 57 | f"{qtpy.API_NAME:9s} " 58 | f"{qtpy.QT_VERSION}" # pyright: ignore[reportPrivateImportUsage] 59 | ) 60 | print(f"PyQtGraph {pg.__version__}") 61 | 62 | if TRY_USING_OPENGL: 63 | try: 64 | import OpenGL.GL as gl # pylint: disable=unused-import 65 | from OpenGL.version import __version__ as gl_version 66 | except Exception: # pylint: disable=broad-except 67 | print("PyOpenGL not found") 68 | print("To install: `conda install pyopengl` or `pip install pyopengl`") 69 | else: 70 | print(f"PyOpenGL {gl_version}") 71 | pg.setConfigOptions(useOpenGL=True) 72 | pg.setConfigOptions(antialias=True) 73 | pg.setConfigOptions(enableExperimental=True) 74 | else: 75 | print("PyOpenGL disabled") 76 | 77 | # Global pyqtgraph configuration 78 | # pg.setConfigOptions(leftButtonPan=False) 79 | pg.setConfigOption("foreground", "#EEE") 80 | 81 | # ------------------------------------------------------------------------------ 82 | # MainWindow 83 | # ------------------------------------------------------------------------------ 84 | 85 | 86 | class MainWindow(QtWid.QWidget): 87 | def __init__( 88 | self, 89 | qdev: WaveGeneratorArduino_qdev, 90 | qlog: FileLogger, 91 | parent=None, 92 | **kwargs, 93 | ): 94 | super().__init__(parent, **kwargs) 95 | 96 | self.qdev = qdev 97 | self.qdev.signal_DAQ_updated.connect(self.update_GUI) 98 | self.qlog = qlog 99 | 100 | # Run/pause mechanism on updating the GUI and graphs 101 | self.allow_GUI_update_of_readings = True 102 | 103 | self.setWindowTitle("Arduino & PyQt multithread demo") 104 | if USE_LARGER_TEXT: 105 | self.setGeometry(40, 60, 1024, 768) 106 | else: 107 | self.setGeometry(40, 60, 960, 660) 108 | self.setStyleSheet(controls.SS_TEXTBOX_READ_ONLY + controls.SS_GROUP) 109 | 110 | # ------------------------- 111 | # Chart refresh timer 112 | # ------------------------- 113 | 114 | self.timer_chart = QtCore.QTimer() 115 | self.timer_chart.setTimerType(QtCore.Qt.TimerType.PreciseTimer) 116 | self.timer_chart.timeout.connect(self.update_chart) 117 | 118 | # ------------------------- 119 | # Top frame 120 | # ------------------------- 121 | 122 | # Left box 123 | self.qlbl_update_counter = QtWid.QLabel("0") 124 | self.qlbl_DAQ_rate = QtWid.QLabel("DAQ: nan Hz") 125 | self.qlbl_DAQ_rate.setStyleSheet("QLabel {min-width: 7em}") 126 | self.qlbl_recording_time = QtWid.QLabel() 127 | 128 | vbox_left = QtWid.QVBoxLayout() 129 | vbox_left.addWidget(self.qlbl_update_counter, stretch=0) 130 | vbox_left.addStretch(1) 131 | vbox_left.addWidget(self.qlbl_recording_time, stretch=0) 132 | vbox_left.addWidget(self.qlbl_DAQ_rate, stretch=0) 133 | 134 | # Middle box 135 | self.qlbl_title = QtWid.QLabel("Arduino & PyQt multithread demo") 136 | self.qlbl_title.setFont( 137 | QtGui.QFont( 138 | "Palatino", 139 | 20 if USE_LARGER_TEXT else 14, 140 | weight=QtGui.QFont.Weight.Bold, 141 | ) 142 | ) 143 | self.qlbl_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 144 | 145 | self.qlbl_cur_date_time = QtWid.QLabel("00-00-0000 00:00:00") 146 | self.qlbl_cur_date_time.setAlignment( 147 | QtCore.Qt.AlignmentFlag.AlignCenter 148 | ) 149 | 150 | self.qpbt_record = controls.create_Toggle_button( 151 | "Click to start recording to file", minimumHeight=40 152 | ) 153 | self.qpbt_record.setMinimumWidth(400) 154 | self.qpbt_record.clicked.connect(lambda state: qlog.record(state)) 155 | 156 | vbox_middle = QtWid.QVBoxLayout() 157 | vbox_middle.addWidget(self.qlbl_title) 158 | vbox_middle.addWidget(self.qlbl_cur_date_time) 159 | vbox_middle.addWidget(self.qpbt_record) 160 | 161 | # Right box 162 | p = { 163 | "alignment": QtCore.Qt.AlignmentFlag.AlignRight 164 | | QtCore.Qt.AlignmentFlag.AlignVCenter, 165 | "parent": self, 166 | } 167 | 168 | self.qpbt_exit = QtWid.QPushButton("Exit") 169 | self.qpbt_exit.clicked.connect(self.close) 170 | self.qpbt_exit.setMinimumHeight(30) 171 | 172 | self.qlbl_GitHub = QtWid.QLabel( 173 | f'GitHub source', **p 174 | ) 175 | self.qlbl_GitHub.setTextFormat(QtCore.Qt.TextFormat.RichText) 176 | self.qlbl_GitHub.setTextInteractionFlags( 177 | QtCore.Qt.TextInteractionFlag.TextBrowserInteraction 178 | ) 179 | self.qlbl_GitHub.setOpenExternalLinks(True) 180 | 181 | vbox_right = QtWid.QVBoxLayout() 182 | vbox_right.setSpacing(4) 183 | vbox_right.addWidget(self.qpbt_exit, stretch=0) 184 | vbox_right.addStretch(1) 185 | vbox_right.addWidget(QtWid.QLabel(__author__, **p)) 186 | vbox_right.addWidget(self.qlbl_GitHub) 187 | vbox_right.addWidget(QtWid.QLabel(f"v{__version__}", **p)) 188 | 189 | # Round up top frame 190 | hbox_top = QtWid.QHBoxLayout() 191 | hbox_top.addLayout(vbox_left, stretch=0) 192 | hbox_top.addStretch(1) 193 | hbox_top.addLayout(vbox_middle, stretch=0) 194 | hbox_top.addStretch(1) 195 | hbox_top.addLayout(vbox_right, stretch=0) 196 | 197 | # ------------------------- 198 | # Bottom frame 199 | # ------------------------- 200 | 201 | # GraphicsLayoutWidget 202 | self.gw = pg.GraphicsLayoutWidget() 203 | self.plot = self.gw.addPlot() 204 | 205 | p = { 206 | "color": "#EEE", 207 | "font-size": "20pt" if USE_LARGER_TEXT else "10pt", 208 | } 209 | self.plot.setClipToView(True) 210 | self.plot.showGrid(x=1, y=1) 211 | self.plot.setLabel("bottom", text="history (sec)", **p) 212 | self.plot.setLabel("left", text="amplitude", **p) 213 | self.plot.setRange( 214 | xRange=[-1.04 * CHART_HISTORY_TIME, CHART_HISTORY_TIME * 0.04], 215 | yRange=[-1.1, 1.1], 216 | disableAutoRange=True, 217 | ) 218 | 219 | if USE_LARGER_TEXT: 220 | font = QtGui.QFont() 221 | font.setPixelSize(26) 222 | self.plot.getAxis("bottom").setTickFont(font) 223 | self.plot.getAxis("bottom").setStyle(tickTextOffset=20) 224 | self.plot.getAxis("bottom").setHeight(90) 225 | self.plot.getAxis("left").setTickFont(font) 226 | self.plot.getAxis("left").setStyle(tickTextOffset=20) 227 | self.plot.getAxis("left").setWidth(120) 228 | 229 | self.history_chart_curve = HistoryChartCurve( 230 | capacity=round(CHART_HISTORY_TIME * 1e3 / DAQ_INTERVAL_MS), 231 | linked_curve=self.plot.plot( 232 | pen=pg.mkPen(color=[255, 255, 0], width=3) 233 | ), 234 | ) 235 | 236 | # 'Readings' 237 | p = {"readOnly": True, "maximumWidth": 112 if USE_LARGER_TEXT else 63} 238 | self.qlin_reading_t = QtWid.QLineEdit(**p) 239 | self.qlin_reading_1 = QtWid.QLineEdit(**p) 240 | self.qpbt_running = controls.create_Toggle_button( 241 | "Running", checked=True 242 | ) 243 | self.qpbt_running.clicked.connect( 244 | lambda state: self.process_qpbt_running(state) 245 | ) 246 | 247 | # fmt: off 248 | grid = QtWid.QGridLayout() 249 | grid.addWidget(self.qpbt_running , 0, 0, 1, 2) 250 | grid.addWidget(QtWid.QLabel("time"), 1, 0) 251 | grid.addWidget(self.qlin_reading_t , 1, 1) 252 | grid.addWidget(QtWid.QLabel("#01") , 2, 0) 253 | grid.addWidget(self.qlin_reading_1 , 2, 1) 254 | grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) 255 | # fmt: on 256 | 257 | qgrp_readings = QtWid.QGroupBox("Readings") 258 | qgrp_readings.setLayout(grid) 259 | 260 | # 'Wave type' 261 | self.qpbt_wave_sine = QtWid.QPushButton("Sine") 262 | self.qpbt_wave_sine.clicked.connect( 263 | self.qdev.request_set_waveform_to_sine 264 | ) 265 | self.qpbt_wave_square = QtWid.QPushButton("Square") 266 | self.qpbt_wave_square.clicked.connect( 267 | self.qdev.request_set_waveform_to_square 268 | ) 269 | self.qpbt_wave_sawtooth = QtWid.QPushButton("Sawtooth") 270 | self.qpbt_wave_sawtooth.clicked.connect( 271 | self.qdev.request_set_waveform_to_sawtooth 272 | ) 273 | 274 | # fmt: off 275 | grid = QtWid.QGridLayout() 276 | grid.addWidget(self.qpbt_wave_sine , 0, 0) 277 | grid.addWidget(self.qpbt_wave_square , 1, 0) 278 | grid.addWidget(self.qpbt_wave_sawtooth, 2, 0) 279 | grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) 280 | # fmt: on 281 | 282 | qgrp_wave_type = QtWid.QGroupBox("Wave type") 283 | qgrp_wave_type.setLayout(grid) 284 | 285 | # ------------------------- 286 | # PlotManager 287 | # ------------------------- 288 | 289 | self.plot_manager = PlotManager(parent=self) 290 | self.plot_manager.add_autorange_buttons(linked_plots=self.plot) 291 | self.plot_manager.add_preset_buttons( 292 | linked_plots=self.plot, 293 | linked_curves=[self.history_chart_curve], 294 | presets=[ 295 | { 296 | "button_label": "0.100", 297 | "x_axis_label": "history (msec)", 298 | "x_axis_divisor": 1e-3, 299 | "x_axis_range": (-101, 0), 300 | }, 301 | { 302 | "button_label": "0:05", 303 | "x_axis_label": "history (sec)", 304 | "x_axis_divisor": 1, 305 | "x_axis_range": (-5.05, 0), 306 | }, 307 | { 308 | "button_label": "0:10", 309 | "x_axis_label": "history (sec)", 310 | "x_axis_divisor": 1, 311 | "x_axis_range": (-10.1, 0), 312 | }, 313 | ], 314 | ) 315 | self.plot_manager.add_clear_button( 316 | linked_curves=[self.history_chart_curve] 317 | ) 318 | self.plot_manager.perform_preset(1) 319 | 320 | qgrp_chart = QtWid.QGroupBox("Chart") 321 | qgrp_chart.setLayout(self.plot_manager.grid) 322 | 323 | vbox = QtWid.QVBoxLayout() 324 | vbox.addWidget(qgrp_readings) 325 | vbox.addWidget(qgrp_wave_type) 326 | vbox.addWidget(qgrp_chart) 327 | vbox.addStretch() 328 | 329 | # Round up bottom frame 330 | hbox_bot = QtWid.QHBoxLayout() 331 | hbox_bot.addWidget(self.gw, 1) 332 | hbox_bot.addLayout(vbox, 0) 333 | 334 | # ------------------------- 335 | # Round up full window 336 | # ------------------------- 337 | 338 | vbox = QtWid.QVBoxLayout(self) 339 | vbox.addLayout(hbox_top, stretch=0) 340 | vbox.addSpacerItem(QtWid.QSpacerItem(0, 10)) 341 | vbox.addLayout(hbox_bot, stretch=1) 342 | 343 | # -------------------------------------------------------------------------- 344 | # Handle controls 345 | # -------------------------------------------------------------------------- 346 | 347 | @Slot(bool) 348 | def process_qpbt_running(self, state: bool): 349 | self.qpbt_running.setText("Running" if state else "Paused") 350 | self.allow_GUI_update_of_readings = state 351 | 352 | @Slot() 353 | def update_GUI(self): 354 | state = self.qdev.dev.state # Shorthand 355 | 356 | self.qlbl_cur_date_time.setText( 357 | f"{datetime.datetime.now().strftime('%d-%m-%Y %H:%M:%S')}" 358 | ) 359 | self.qlbl_update_counter.setText(f"{self.qdev.update_counter_DAQ}") 360 | self.qlbl_DAQ_rate.setText( 361 | f"DAQ: {self.qdev.obtained_DAQ_rate_Hz:.1f} Hz" 362 | ) 363 | self.qlbl_recording_time.setText( 364 | f"REC: {self.qlog.pretty_elapsed()}" 365 | if self.qlog.is_recording() 366 | else "" 367 | ) 368 | 369 | if not self.allow_GUI_update_of_readings: 370 | return 371 | 372 | self.qlin_reading_t.setText(f"{state.time:.3f}") 373 | self.qlin_reading_1.setText(f"{state.reading_1:.4f}") 374 | 375 | @Slot() 376 | def update_chart(self): 377 | if not self.allow_GUI_update_of_readings: 378 | return 379 | 380 | if DEBUG: 381 | tprint("update_curve") 382 | 383 | self.history_chart_curve.update() 384 | 385 | 386 | # ------------------------------------------------------------------------------ 387 | # Main 388 | # ------------------------------------------------------------------------------ 389 | 390 | if __name__ == "__main__": 391 | # Set priority of this process to maximum in the operating system 392 | print(f"PID: {os.getpid()}\n") 393 | try: 394 | proc = psutil.Process(os.getpid()) 395 | if os.name == "nt": 396 | proc.nice(psutil.REALTIME_PRIORITY_CLASS) # Windows 397 | else: 398 | proc.nice(-20) # Other 399 | except Exception: # pylint: disable=broad-except 400 | print("Warning: Could not set process to maximum priority.\n") 401 | 402 | # -------------------------------------------------------------------------- 403 | # Connect to Arduino 404 | # -------------------------------------------------------------------------- 405 | 406 | if SIMULATE_ARDUINO: 407 | ard = FakeWaveGeneratorArduino() 408 | else: 409 | ard = WaveGeneratorArduino() 410 | 411 | ard.serial_settings["baudrate"] = 115200 412 | ard.auto_connect() 413 | 414 | if not ard.is_alive: 415 | print("\nCheck connection and try resetting the Arduino.") 416 | print("Exiting...\n") 417 | sys.exit(0) 418 | 419 | # -------------------------------------------------------------------------- 420 | # Create application 421 | # -------------------------------------------------------------------------- 422 | 423 | main_thread = QtCore.QThread.currentThread() 424 | if isinstance(main_thread, QtCore.QThread): 425 | main_thread.setObjectName("MAIN") # For DEBUG info 426 | 427 | app = QtWid.QApplication(sys.argv) 428 | if USE_LARGER_TEXT: 429 | app.setFont(QtGui.QFont(QtWid.QApplication.font().family(), 16)) 430 | 431 | # -------------------------------------------------------------------------- 432 | # Set up multithreaded communication with the Arduino 433 | # -------------------------------------------------------------------------- 434 | 435 | def DAQ_function() -> bool: 436 | """Perform a single data acquisition and append this data to the chart 437 | and log. 438 | 439 | Returns: True if successful, False otherwise. 440 | """ 441 | # Query the Arduino for new readings, parse them and update the 442 | # corresponding variables of its `state` member. 443 | if not ard.perform_DAQ(): 444 | return False 445 | 446 | # Use Arduino time or PC time? 447 | now = time.perf_counter() if USE_PC_TIME else ard.state.time 448 | if ard_qdev.update_counter_DAQ == 1: 449 | ard.state.time_0 = now 450 | ard.state.time = 0 451 | else: 452 | ard.state.time = now - ard.state.time_0 453 | 454 | # Add readings to chart history 455 | window.history_chart_curve.appendData( 456 | ard.state.time, ard.state.reading_1 457 | ) 458 | 459 | # Create and add readings to the log 460 | log.update() 461 | 462 | return True 463 | 464 | ard_qdev = WaveGeneratorArduino_qdev( 465 | dev=ard, 466 | DAQ_function=DAQ_function, 467 | DAQ_interval_ms=DAQ_INTERVAL_MS, 468 | debug=DEBUG, 469 | ) 470 | 471 | # -------------------------------------------------------------------------- 472 | # File logger 473 | # -------------------------------------------------------------------------- 474 | 475 | def write_header_to_log(): 476 | log.write("elapsed [s]\treading_1\n") 477 | 478 | def write_data_to_log(): 479 | if USE_PC_TIME: 480 | timestamp = log.elapsed() # Starts at 0 s every recording 481 | else: 482 | timestamp = ard.state.time 483 | 484 | log.write(f"{timestamp:.3f}\t{ard.state.reading_1:.4f}\n") 485 | 486 | log = FileLogger( 487 | write_header_function=write_header_to_log, 488 | write_data_function=write_data_to_log, 489 | ) 490 | log.signal_recording_started.connect( 491 | lambda filepath: window.qpbt_record.setText( 492 | f"Recording to file: {filepath}" 493 | ) 494 | ) 495 | log.signal_recording_stopped.connect( 496 | lambda: window.qpbt_record.setText("Click to start recording to file") 497 | ) 498 | 499 | # -------------------------------------------------------------------------- 500 | # Program termination routines 501 | # -------------------------------------------------------------------------- 502 | 503 | def stop_running(): 504 | app.processEvents() 505 | log.close() 506 | ard_qdev.quit() 507 | ard.close() 508 | 509 | print("Stopping timers: ", end="") 510 | window.timer_chart.stop() 511 | print("done.") 512 | 513 | def about_to_quit(): 514 | print("\nAbout to quit") 515 | stop_running() 516 | 517 | @Slot() 518 | def notify_connection_lost(): 519 | stop_running() 520 | 521 | window.qlbl_title.setText("! ! ! LOST CONNECTION ! ! !") 522 | str_msg = ( 523 | f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" 524 | "Lost connection to Arduino." 525 | ) 526 | print(f"\nCRITICAL ERROR @ {str_msg}") 527 | QtWid.QMessageBox.warning( 528 | window, 529 | "CRITICAL ERROR", 530 | str_msg, 531 | QtWid.QMessageBox.StandardButton.Ok, 532 | ) 533 | 534 | # -------------------------------------------------------------------------- 535 | # Start the main GUI event loop 536 | # -------------------------------------------------------------------------- 537 | 538 | window = MainWindow(qdev=ard_qdev, qlog=log) 539 | window.timer_chart.start(CHART_INTERVAL_MS) 540 | window.show() 541 | 542 | ard_qdev.signal_connection_lost.connect(notify_connection_lost) 543 | ard_qdev.start(DAQ_priority=QtCore.QThread.Priority.TimeCriticalPriority) 544 | 545 | app.aboutToQuit.connect(about_to_quit) 546 | sys.exit(app.exec()) 547 | -------------------------------------------------------------------------------- /demo_B_GUI_minimal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Demonstration of multithreaded real-time plotting and logging of live Arduino 4 | data using PyQt/PySide and PyQtGraph. 5 | """ 6 | __author__ = "Dennis van Gils" 7 | __authoremail__ = "vangils.dennis@gmail.com" 8 | __url__ = "https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo" 9 | __date__ = "11-06-2024" 10 | __version__ = "9.0" 11 | # pylint: disable=missing-function-docstring 12 | 13 | import os 14 | import sys 15 | import time 16 | 17 | import qtpy 18 | from qtpy import QtCore, QtWidgets as QtWid 19 | from qtpy.QtCore import Slot # type: ignore 20 | 21 | import psutil 22 | import pyqtgraph as pg 23 | 24 | from dvg_pyqtgraph_threadsafe import HistoryChartCurve 25 | 26 | from WaveGeneratorArduino import WaveGeneratorArduino, FakeWaveGeneratorArduino 27 | from WaveGeneratorArduino_qdev import WaveGeneratorArduino_qdev 28 | 29 | # Constants 30 | DAQ_INTERVAL_MS = 10 31 | """[ms] Update interval for the data acquisition (DAQ)""" 32 | CHART_INTERVAL_MS = 20 33 | """[ms] Update interval for the chart""" 34 | CHART_HISTORY_TIME = 10 35 | """[s] History length of the chart""" 36 | 37 | # Global flags 38 | TRY_USING_OPENGL = True 39 | USE_PC_TIME = True 40 | """Use Arduino time or PC time?""" 41 | SIMULATE_ARDUINO = False 42 | """Simulate an Arduino in software?""" 43 | if sys.argv[-1] == "simulate": 44 | SIMULATE_ARDUINO = True 45 | 46 | DEBUG = False 47 | """Show debug info in terminal? Warning: Slow! Do not leave on unintentionally. 48 | """ 49 | 50 | print( 51 | f"{qtpy.API_NAME:9s} " 52 | f"{qtpy.QT_VERSION}" # pyright: ignore[reportPrivateImportUsage] 53 | ) 54 | print(f"PyQtGraph {pg.__version__}") 55 | 56 | if TRY_USING_OPENGL: 57 | try: 58 | import OpenGL.GL as gl # pylint: disable=unused-import 59 | from OpenGL.version import __version__ as gl_version 60 | except Exception: # pylint: disable=broad-except 61 | print("PyOpenGL not found") 62 | print("To install: `conda install pyopengl` or `pip install pyopengl`") 63 | else: 64 | print(f"PyOpenGL {gl_version}") 65 | pg.setConfigOptions(useOpenGL=True) 66 | pg.setConfigOptions(antialias=True) 67 | pg.setConfigOptions(enableExperimental=True) 68 | else: 69 | print("PyOpenGL disabled") 70 | 71 | # Global pyqtgraph configuration 72 | # pg.setConfigOptions(leftButtonPan=False) 73 | pg.setConfigOption("foreground", "#EEE") 74 | 75 | # ------------------------------------------------------------------------------ 76 | # MainWindow 77 | # ------------------------------------------------------------------------------ 78 | 79 | 80 | class MainWindow(QtWid.QWidget): 81 | def __init__(self, parent=None, **kwargs): 82 | super().__init__(parent, **kwargs) 83 | 84 | self.setWindowTitle("Arduino & PyQt multithread demo") 85 | self.setGeometry(40, 60, 800, 660) 86 | 87 | # GraphicsLayoutWidget 88 | self.gw = pg.GraphicsLayoutWidget() 89 | self.plot = self.gw.addPlot() 90 | 91 | p = {"color": "#EEE", "font-size": "10pt"} 92 | self.plot.setClipToView(True) 93 | self.plot.showGrid(x=1, y=1) 94 | self.plot.setLabel("bottom", text="history (sec)", **p) 95 | self.plot.setLabel("left", text="amplitude", **p) 96 | self.plot.setRange( 97 | xRange=[-1.04 * CHART_HISTORY_TIME, CHART_HISTORY_TIME * 0.04], 98 | yRange=[-1.1, 1.1], 99 | disableAutoRange=True, 100 | ) 101 | 102 | self.history_chart_curve = HistoryChartCurve( 103 | capacity=round(CHART_HISTORY_TIME * 1e3 / DAQ_INTERVAL_MS), 104 | linked_curve=self.plot.plot( 105 | pen=pg.mkPen(color=[255, 255, 0], width=3) 106 | ), 107 | ) 108 | 109 | vbox = QtWid.QVBoxLayout(self) 110 | vbox.addWidget(self.gw, 1) 111 | 112 | # Chart refresh timer 113 | self.timer_chart = QtCore.QTimer() 114 | self.timer_chart.setTimerType(QtCore.Qt.TimerType.PreciseTimer) 115 | self.timer_chart.timeout.connect(self.history_chart_curve.update) 116 | 117 | 118 | # ------------------------------------------------------------------------------ 119 | # Main 120 | # ------------------------------------------------------------------------------ 121 | 122 | if __name__ == "__main__": 123 | # Set priority of this process to maximum in the operating system 124 | print(f"PID: {os.getpid()}\n") 125 | try: 126 | proc = psutil.Process(os.getpid()) 127 | if os.name == "nt": 128 | proc.nice(psutil.REALTIME_PRIORITY_CLASS) # Windows 129 | else: 130 | proc.nice(-20) # Other 131 | except Exception: # pylint: disable=broad-except 132 | print("Warning: Could not set process to maximum priority.\n") 133 | 134 | # -------------------------------------------------------------------------- 135 | # Connect to Arduino 136 | # -------------------------------------------------------------------------- 137 | 138 | if SIMULATE_ARDUINO: 139 | ard = FakeWaveGeneratorArduino() 140 | else: 141 | ard = WaveGeneratorArduino() 142 | 143 | ard.serial_settings["baudrate"] = 115200 144 | ard.auto_connect() 145 | 146 | if not ard.is_alive: 147 | print("\nCheck connection and try resetting the Arduino.") 148 | print("Exiting...\n") 149 | sys.exit(0) 150 | 151 | # -------------------------------------------------------------------------- 152 | # Create application 153 | # -------------------------------------------------------------------------- 154 | 155 | main_thread = QtCore.QThread.currentThread() 156 | if isinstance(main_thread, QtCore.QThread): 157 | main_thread.setObjectName("MAIN") # For DEBUG info 158 | 159 | app = QtWid.QApplication(sys.argv) 160 | 161 | # -------------------------------------------------------------------------- 162 | # Set up multithreaded communication with the Arduino 163 | # -------------------------------------------------------------------------- 164 | 165 | def DAQ_function() -> bool: 166 | """Perform a single data acquisition and append this data to the chart. 167 | 168 | Returns: True if successful, False otherwise. 169 | """ 170 | # Query the Arduino for new readings, parse them and update the 171 | # corresponding variables of its `state` member. 172 | if not ard.perform_DAQ(): 173 | return False 174 | 175 | # Use Arduino time or PC time? 176 | now = time.perf_counter() if USE_PC_TIME else ard.state.time 177 | if ard_qdev.update_counter_DAQ == 1: 178 | ard.state.time_0 = now 179 | ard.state.time = 0 180 | else: 181 | ard.state.time = now - ard.state.time_0 182 | 183 | # Add readings to chart history 184 | window.history_chart_curve.appendData( 185 | ard.state.time, ard.state.reading_1 186 | ) 187 | 188 | return True 189 | 190 | ard_qdev = WaveGeneratorArduino_qdev( 191 | dev=ard, 192 | DAQ_function=DAQ_function, 193 | DAQ_interval_ms=DAQ_INTERVAL_MS, 194 | debug=DEBUG, 195 | ) 196 | 197 | # -------------------------------------------------------------------------- 198 | # Program termination routines 199 | # -------------------------------------------------------------------------- 200 | 201 | @Slot() 202 | def about_to_quit(): 203 | print("\nAbout to quit") 204 | app.processEvents() 205 | ard_qdev.quit() 206 | ard.close() 207 | 208 | print("Stopping timers: ", end="") 209 | window.timer_chart.stop() 210 | print("done.") 211 | 212 | # -------------------------------------------------------------------------- 213 | # Start the main GUI event loop 214 | # -------------------------------------------------------------------------- 215 | 216 | window = MainWindow() 217 | window.timer_chart.start(CHART_INTERVAL_MS) 218 | window.show() 219 | 220 | ard_qdev.start(DAQ_priority=QtCore.QThread.Priority.TimeCriticalPriority) 221 | 222 | app.aboutToQuit.connect(about_to_quit) 223 | sys.exit(app.exec()) 224 | -------------------------------------------------------------------------------- /demo_C_singlethread_for_comparison.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Demonstration of singlethreaded real-time plotting and logging of live 4 | Arduino data using PyQt/PySide and PyQtGraph. 5 | 6 | NOTE: This demonstrates the bad case of what happens when both the acquisition 7 | and the plotting happen on the same thread. You should observe a drop in the 8 | acquisition rate (DAQ rate) when you rapidly resize the window, for instance. 9 | And you should notice the timestamps in the recorded data file being all over 10 | the place. 11 | """ 12 | __author__ = "Dennis van Gils" 13 | __authoremail__ = "vangils.dennis@gmail.com" 14 | __url__ = "https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo" 15 | __date__ = "11-06-2024" 16 | __version__ = "9.0" 17 | # pylint: disable=missing-function-docstring, unnecessary-lambda 18 | # pylint: disable=global-statement 19 | 20 | import os 21 | import sys 22 | import time 23 | import datetime 24 | from typing import Union 25 | 26 | import qtpy 27 | from qtpy import QtCore, QtGui, QtWidgets as QtWid 28 | from qtpy.QtCore import Slot # type: ignore 29 | 30 | import psutil 31 | import numpy as np 32 | import pyqtgraph as pg 33 | 34 | from dvg_debug_functions import tprint 35 | from dvg_pyqtgraph_threadsafe import HistoryChartCurve, PlotManager 36 | from dvg_pyqt_filelogger import FileLogger 37 | import dvg_pyqt_controls as controls 38 | 39 | from WaveGeneratorArduino import WaveGeneratorArduino, FakeWaveGeneratorArduino 40 | 41 | # Constants 42 | DAQ_INTERVAL_MS = 10 43 | """[ms] Update interval for the data acquisition (DAQ)""" 44 | CHART_INTERVAL_MS = 20 45 | """[ms] Update interval for the chart""" 46 | CHART_HISTORY_TIME = 10 47 | """[s] History length of the chart""" 48 | 49 | # Global flags 50 | TRY_USING_OPENGL = True 51 | USE_PC_TIME = True 52 | """Use Arduino time or PC time?""" 53 | SIMULATE_ARDUINO = False 54 | """Simulate an Arduino in software?""" 55 | if sys.argv[-1] == "simulate": 56 | SIMULATE_ARDUINO = True 57 | 58 | DEBUG = False 59 | """Show debug info in terminal? Warning: Slow! Do not leave on unintentionally. 60 | """ 61 | 62 | print( 63 | f"{qtpy.API_NAME:9s} " 64 | f"{qtpy.QT_VERSION}" # pyright: ignore[reportPrivateImportUsage] 65 | ) 66 | print(f"PyQtGraph {pg.__version__}") 67 | 68 | if TRY_USING_OPENGL: 69 | try: 70 | import OpenGL.GL as gl # pylint: disable=unused-import 71 | from OpenGL.version import __version__ as gl_version 72 | except Exception: # pylint: disable=broad-except 73 | print("PyOpenGL not found") 74 | print("To install: `conda install pyopengl` or `pip install pyopengl`") 75 | else: 76 | print(f"PyOpenGL {gl_version}") 77 | pg.setConfigOptions(useOpenGL=True) 78 | pg.setConfigOptions(antialias=True) 79 | pg.setConfigOptions(enableExperimental=True) 80 | else: 81 | print("PyOpenGL disabled") 82 | 83 | # Global pyqtgraph configuration 84 | # pg.setConfigOptions(leftButtonPan=False) 85 | pg.setConfigOption("foreground", "#EEE") 86 | 87 | # ------------------------------------------------------------------------------ 88 | # Globals for keeping track of the obtained DAQ rate in singlethreaded mode 89 | # ------------------------------------------------------------------------------ 90 | 91 | # The obtained DAQ rate is normally being tracked via the multithreaded 92 | # `QDeviceIO` instance, but because we are demoing singlethreaded performance we 93 | # use these globals instead. 94 | update_counter_DAQ = 0 95 | obtained_DAQ_rate_Hz = np.nan 96 | QET_rate = QtCore.QElapsedTimer() 97 | rate_accumulator = 0 98 | 99 | # ------------------------------------------------------------------------------ 100 | # MainWindow 101 | # ------------------------------------------------------------------------------ 102 | 103 | 104 | class MainWindow(QtWid.QWidget): 105 | def __init__( 106 | self, 107 | dev: Union[WaveGeneratorArduino, FakeWaveGeneratorArduino], 108 | qlog: FileLogger, 109 | parent=None, 110 | **kwargs, 111 | ): 112 | super().__init__(parent, **kwargs) 113 | 114 | self.dev = dev 115 | self.qlog = qlog 116 | 117 | # Run/pause mechanism on updating the GUI and graphs 118 | self.allow_GUI_update_of_readings = True 119 | 120 | self.setWindowTitle("Arduino & PyQt singlethread demo") 121 | self.setGeometry(40, 60, 960, 660) 122 | self.setStyleSheet(controls.SS_TEXTBOX_READ_ONLY + controls.SS_GROUP) 123 | 124 | # ------------------------- 125 | # Chart refresh timer 126 | # ------------------------- 127 | 128 | self.timer_chart = QtCore.QTimer() 129 | self.timer_chart.setTimerType(QtCore.Qt.TimerType.PreciseTimer) 130 | self.timer_chart.timeout.connect(self.update_chart) 131 | 132 | # ------------------------- 133 | # Top frame 134 | # ------------------------- 135 | 136 | # Left box 137 | self.qlbl_update_counter = QtWid.QLabel("0") 138 | self.qlbl_DAQ_rate = QtWid.QLabel("DAQ: nan Hz") 139 | self.qlbl_DAQ_rate.setStyleSheet("QLabel {min-width: 7em}") 140 | self.qlbl_recording_time = QtWid.QLabel() 141 | 142 | vbox_left = QtWid.QVBoxLayout() 143 | vbox_left.addWidget(self.qlbl_update_counter, stretch=0) 144 | vbox_left.addStretch(1) 145 | vbox_left.addWidget(self.qlbl_recording_time, stretch=0) 146 | vbox_left.addWidget(self.qlbl_DAQ_rate, stretch=0) 147 | 148 | # Middle box 149 | self.qlbl_title = QtWid.QLabel("Arduino & PyQt singlethread demo") 150 | self.qlbl_title.setFont( 151 | QtGui.QFont("Palatino", 14, weight=QtGui.QFont.Weight.Bold), 152 | ) 153 | self.qlbl_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 154 | 155 | self.qlbl_cur_date_time = QtWid.QLabel("00-00-0000 00:00:00") 156 | self.qlbl_cur_date_time.setAlignment( 157 | QtCore.Qt.AlignmentFlag.AlignCenter 158 | ) 159 | 160 | self.qpbt_record = controls.create_Toggle_button( 161 | "Click to start recording to file", minimumHeight=40 162 | ) 163 | self.qpbt_record.setMinimumWidth(400) 164 | self.qpbt_record.clicked.connect(lambda state: qlog.record(state)) 165 | 166 | vbox_middle = QtWid.QVBoxLayout() 167 | vbox_middle.addWidget(self.qlbl_title) 168 | vbox_middle.addWidget(self.qlbl_cur_date_time) 169 | vbox_middle.addWidget(self.qpbt_record) 170 | 171 | # Right box 172 | p = { 173 | "alignment": QtCore.Qt.AlignmentFlag.AlignRight 174 | | QtCore.Qt.AlignmentFlag.AlignVCenter, 175 | "parent": self, 176 | } 177 | 178 | self.qpbt_exit = QtWid.QPushButton("Exit") 179 | self.qpbt_exit.clicked.connect(self.close) 180 | self.qpbt_exit.setMinimumHeight(30) 181 | 182 | self.qlbl_GitHub = QtWid.QLabel( 183 | f'GitHub source', **p 184 | ) 185 | self.qlbl_GitHub.setTextFormat(QtCore.Qt.TextFormat.RichText) 186 | self.qlbl_GitHub.setTextInteractionFlags( 187 | QtCore.Qt.TextInteractionFlag.TextBrowserInteraction 188 | ) 189 | self.qlbl_GitHub.setOpenExternalLinks(True) 190 | 191 | vbox_right = QtWid.QVBoxLayout() 192 | vbox_right.setSpacing(4) 193 | vbox_right.addWidget(self.qpbt_exit, stretch=0) 194 | vbox_right.addStretch(1) 195 | vbox_right.addWidget(QtWid.QLabel(__author__, **p)) 196 | vbox_right.addWidget(self.qlbl_GitHub) 197 | vbox_right.addWidget(QtWid.QLabel(f"v{__version__}", **p)) 198 | 199 | # Round up top frame 200 | hbox_top = QtWid.QHBoxLayout() 201 | hbox_top.addLayout(vbox_left, stretch=0) 202 | hbox_top.addStretch(1) 203 | hbox_top.addLayout(vbox_middle, stretch=0) 204 | hbox_top.addStretch(1) 205 | hbox_top.addLayout(vbox_right, stretch=0) 206 | 207 | # ------------------------- 208 | # Bottom frame 209 | # ------------------------- 210 | 211 | # GraphicsLayoutWidget 212 | self.gw = pg.GraphicsLayoutWidget() 213 | self.plot = self.gw.addPlot() 214 | 215 | p = {"color": "#EEE", "font-size": "10pt"} 216 | self.plot.setClipToView(True) 217 | self.plot.showGrid(x=1, y=1) 218 | self.plot.setLabel("bottom", text="history (sec)", **p) 219 | self.plot.setLabel("left", text="amplitude", **p) 220 | self.plot.setRange( 221 | xRange=[-1.04 * CHART_HISTORY_TIME, CHART_HISTORY_TIME * 0.04], 222 | yRange=[-1.1, 1.1], 223 | disableAutoRange=True, 224 | ) 225 | 226 | self.history_chart_curve = HistoryChartCurve( 227 | capacity=round(CHART_HISTORY_TIME * 1e3 / DAQ_INTERVAL_MS), 228 | linked_curve=self.plot.plot( 229 | pen=pg.mkPen(color=[255, 255, 0], width=3) 230 | ), 231 | ) 232 | 233 | # 'Readings' 234 | p = {"readOnly": True, "maximumWidth": 63} 235 | self.qlin_reading_t = QtWid.QLineEdit(**p) 236 | self.qlin_reading_1 = QtWid.QLineEdit(**p) 237 | self.qpbt_running = controls.create_Toggle_button( 238 | "Running", checked=True 239 | ) 240 | self.qpbt_running.clicked.connect( 241 | lambda state: self.process_qpbt_running(state) 242 | ) 243 | 244 | # fmt: off 245 | grid = QtWid.QGridLayout() 246 | grid.addWidget(self.qpbt_running , 0, 0, 1, 2) 247 | grid.addWidget(QtWid.QLabel("time"), 1, 0) 248 | grid.addWidget(self.qlin_reading_t , 1, 1) 249 | grid.addWidget(QtWid.QLabel("#01") , 2, 0) 250 | grid.addWidget(self.qlin_reading_1 , 2, 1) 251 | grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) 252 | # fmt: on 253 | 254 | qgrp_readings = QtWid.QGroupBox("Readings") 255 | qgrp_readings.setLayout(grid) 256 | 257 | # 'Wave type' 258 | self.qpbt_wave_sine = QtWid.QPushButton("Sine") 259 | self.qpbt_wave_sine.clicked.connect(self.dev.set_waveform_to_sine) 260 | self.qpbt_wave_square = QtWid.QPushButton("Square") 261 | self.qpbt_wave_square.clicked.connect(self.dev.set_waveform_to_square) 262 | self.qpbt_wave_sawtooth = QtWid.QPushButton("Sawtooth") 263 | self.qpbt_wave_sawtooth.clicked.connect( 264 | self.dev.set_waveform_to_sawtooth 265 | ) 266 | 267 | # fmt: off 268 | grid = QtWid.QGridLayout() 269 | grid.addWidget(self.qpbt_wave_sine , 0, 0) 270 | grid.addWidget(self.qpbt_wave_square , 1, 0) 271 | grid.addWidget(self.qpbt_wave_sawtooth, 2, 0) 272 | grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) 273 | # fmt: on 274 | 275 | qgrp_wave_type = QtWid.QGroupBox("Wave type") 276 | qgrp_wave_type.setLayout(grid) 277 | 278 | # ------------------------- 279 | # PlotManager 280 | # ------------------------- 281 | 282 | self.plot_manager = PlotManager(parent=self) 283 | self.plot_manager.add_autorange_buttons(linked_plots=self.plot) 284 | self.plot_manager.add_preset_buttons( 285 | linked_plots=self.plot, 286 | linked_curves=[self.history_chart_curve], 287 | presets=[ 288 | { 289 | "button_label": "0.100", 290 | "x_axis_label": "history (msec)", 291 | "x_axis_divisor": 1e-3, 292 | "x_axis_range": (-101, 0), 293 | }, 294 | { 295 | "button_label": "0:05", 296 | "x_axis_label": "history (sec)", 297 | "x_axis_divisor": 1, 298 | "x_axis_range": (-5.05, 0), 299 | }, 300 | { 301 | "button_label": "0:10", 302 | "x_axis_label": "history (sec)", 303 | "x_axis_divisor": 1, 304 | "x_axis_range": (-10.1, 0), 305 | }, 306 | ], 307 | ) 308 | self.plot_manager.add_clear_button( 309 | linked_curves=[self.history_chart_curve] 310 | ) 311 | self.plot_manager.perform_preset(1) 312 | 313 | qgrp_chart = QtWid.QGroupBox("Chart") 314 | qgrp_chart.setLayout(self.plot_manager.grid) 315 | 316 | vbox = QtWid.QVBoxLayout() 317 | vbox.addWidget(qgrp_readings) 318 | vbox.addWidget(qgrp_wave_type) 319 | vbox.addWidget(qgrp_chart) 320 | vbox.addStretch() 321 | 322 | # Round up bottom frame 323 | hbox_bot = QtWid.QHBoxLayout() 324 | hbox_bot.addWidget(self.gw, 1) 325 | hbox_bot.addLayout(vbox, 0) 326 | 327 | # ------------------------- 328 | # Round up full window 329 | # ------------------------- 330 | 331 | vbox = QtWid.QVBoxLayout(self) 332 | vbox.addLayout(hbox_top, stretch=0) 333 | vbox.addSpacerItem(QtWid.QSpacerItem(0, 10)) 334 | vbox.addLayout(hbox_bot, stretch=1) 335 | 336 | # -------------------------------------------------------------------------- 337 | # Handle controls 338 | # -------------------------------------------------------------------------- 339 | 340 | @Slot(bool) 341 | def process_qpbt_running(self, state: bool): 342 | self.qpbt_running.setText("Running" if state else "Paused") 343 | self.allow_GUI_update_of_readings = state 344 | 345 | @Slot() 346 | def update_GUI(self): 347 | state = self.dev.state # Shorthand 348 | 349 | self.qlbl_cur_date_time.setText( 350 | f"{datetime.datetime.now().strftime('%d-%m-%Y %H:%M:%S')}" 351 | ) 352 | self.qlbl_update_counter.setText(f"{update_counter_DAQ}") 353 | self.qlbl_DAQ_rate.setText(f"DAQ: {obtained_DAQ_rate_Hz:.1f} Hz") 354 | self.qlbl_recording_time.setText( 355 | f"REC: {self.qlog.pretty_elapsed()}" 356 | if self.qlog.is_recording() 357 | else "" 358 | ) 359 | 360 | if not self.allow_GUI_update_of_readings: 361 | return 362 | 363 | self.qlin_reading_t.setText(f"{state.time:.3f}") 364 | self.qlin_reading_1.setText(f"{state.reading_1:.4f}") 365 | 366 | @Slot() 367 | def update_chart(self): 368 | if not self.allow_GUI_update_of_readings: 369 | return 370 | 371 | if DEBUG: 372 | tprint("update_curve") 373 | 374 | self.history_chart_curve.update() 375 | 376 | 377 | # ------------------------------------------------------------------------------ 378 | # Main 379 | # ------------------------------------------------------------------------------ 380 | 381 | if __name__ == "__main__": 382 | # Set priority of this process to maximum in the operating system 383 | print(f"PID: {os.getpid()}\n") 384 | try: 385 | proc = psutil.Process(os.getpid()) 386 | if os.name == "nt": 387 | proc.nice(psutil.REALTIME_PRIORITY_CLASS) # Windows 388 | else: 389 | proc.nice(-20) # Other 390 | except Exception: # pylint: disable=broad-except 391 | print("Warning: Could not set process to maximum priority.\n") 392 | 393 | # -------------------------------------------------------------------------- 394 | # Connect to Arduino 395 | # -------------------------------------------------------------------------- 396 | 397 | if SIMULATE_ARDUINO: 398 | ard = FakeWaveGeneratorArduino() 399 | else: 400 | ard = WaveGeneratorArduino() 401 | 402 | ard.serial_settings["baudrate"] = 115200 403 | ard.auto_connect() 404 | 405 | if not ard.is_alive: 406 | print("\nCheck connection and try resetting the Arduino.") 407 | print("Exiting...\n") 408 | sys.exit(0) 409 | 410 | # -------------------------------------------------------------------------- 411 | # Create application 412 | # -------------------------------------------------------------------------- 413 | 414 | main_thread = QtCore.QThread.currentThread() 415 | if isinstance(main_thread, QtCore.QThread): 416 | main_thread.setObjectName("MAIN") # For DEBUG info 417 | 418 | app = QtWid.QApplication(sys.argv) 419 | 420 | # -------------------------------------------------------------------------- 421 | # File logger 422 | # -------------------------------------------------------------------------- 423 | 424 | def write_header_to_log(): 425 | log.write("elapsed [s]\treading_1\n") 426 | 427 | def write_data_to_log(): 428 | if USE_PC_TIME: 429 | timestamp = log.elapsed() # Starts at 0 s every recording 430 | else: 431 | timestamp = ard.state.time 432 | 433 | log.write(f"{timestamp:.3f}\t{ard.state.reading_1:.4f}\n") 434 | 435 | log = FileLogger( 436 | write_header_function=write_header_to_log, 437 | write_data_function=write_data_to_log, 438 | ) 439 | log.signal_recording_started.connect( 440 | lambda filepath: window.qpbt_record.setText( 441 | f"Recording to file: {filepath}" 442 | ) 443 | ) 444 | log.signal_recording_stopped.connect( 445 | lambda: window.qpbt_record.setText("Click to start recording to file") 446 | ) 447 | 448 | # -------------------------------------------------------------------------- 449 | # Singlethreaded DAQ function 450 | # -------------------------------------------------------------------------- 451 | 452 | @Slot() 453 | def DAQ_function(): 454 | """Perform a single data acquisition and append this data to the chart 455 | and log. 456 | """ 457 | # Keep track of the obtained DAQ rate 458 | global update_counter_DAQ, rate_accumulator, obtained_DAQ_rate_Hz 459 | 460 | update_counter_DAQ += 1 461 | 462 | if not QET_rate.isValid(): 463 | QET_rate.start() 464 | else: 465 | # Obtained DAQ rate 466 | rate_accumulator += 1 467 | dT = QET_rate.elapsed() 468 | 469 | if dT >= 1000: # Evaluate every N elapsed milliseconds. Hard-coded. 470 | QET_rate.restart() 471 | try: 472 | obtained_DAQ_rate_Hz = rate_accumulator / dT * 1e3 473 | except ZeroDivisionError: 474 | obtained_DAQ_rate_Hz = np.nan 475 | 476 | rate_accumulator = 0 477 | 478 | # Query the Arduino for new readings, parse them and update the 479 | # corresponding variables of its `state` member. 480 | if not ard.perform_DAQ(): 481 | sys.exit(0) 482 | 483 | # Use Arduino time or PC time? 484 | now = time.perf_counter() if USE_PC_TIME else ard.state.time 485 | if update_counter_DAQ == 1: 486 | ard.state.time_0 = now 487 | ard.state.time = 0 488 | else: 489 | ard.state.time = now - ard.state.time_0 490 | 491 | # Add readings to chart history 492 | window.history_chart_curve.appendData( 493 | ard.state.time, ard.state.reading_1 494 | ) 495 | 496 | # Create and add readings to the log 497 | log.update() 498 | 499 | # We update the GUI right now because this is a singlethread demo 500 | window.update_GUI() 501 | 502 | timer_state = QtCore.QTimer() 503 | timer_state.timeout.connect(DAQ_function) 504 | timer_state.setTimerType(QtCore.Qt.TimerType.PreciseTimer) 505 | timer_state.start(DAQ_INTERVAL_MS) 506 | 507 | # -------------------------------------------------------------------------- 508 | # Program termination routines 509 | # -------------------------------------------------------------------------- 510 | 511 | @Slot() 512 | def about_to_quit(): 513 | print("\nAbout to quit") 514 | app.processEvents() 515 | log.close() 516 | ard.close() 517 | 518 | print("Stopping timers: ", end="") 519 | window.timer_chart.stop() 520 | print("done.") 521 | 522 | # -------------------------------------------------------------------------- 523 | # Start the main GUI event loop 524 | # -------------------------------------------------------------------------- 525 | 526 | window = MainWindow(dev=ard, qlog=log) 527 | window.timer_chart.start(CHART_INTERVAL_MS) 528 | window.show() 529 | 530 | app.aboutToQuit.connect(about_to_quit) 531 | sys.exit(app.exec()) 532 | -------------------------------------------------------------------------------- /demo_D_no_GUI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Demonstration of multithreaded live Arduino data 4 | - Terminal output only 5 | - Mode: INTERNAL_TIMER 6 | """ 7 | __author__ = "Dennis van Gils" 8 | __authoremail__ = "vangils.dennis@gmail.com" 9 | __url__ = "https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo" 10 | __date__ = "11-06-2024" 11 | __version__ = "9.0" 12 | # pylint: disable=missing-function-docstring 13 | 14 | import os 15 | import sys 16 | import time 17 | import datetime 18 | import signal # To catch CTRL+C and quit 19 | 20 | import qtpy 21 | from qtpy import QtCore 22 | from qtpy.QtCore import Slot # type: ignore 23 | 24 | import psutil 25 | 26 | from WaveGeneratorArduino import WaveGeneratorArduino, FakeWaveGeneratorArduino 27 | from WaveGeneratorArduino_qdev import WaveGeneratorArduino_qdev 28 | 29 | # Constants 30 | DAQ_INTERVAL_MS = 10 31 | """[ms] Update interval for the data acquisition (DAQ)""" 32 | 33 | # Global flags 34 | USE_PC_TIME = True 35 | """Use Arduino time or PC time?""" 36 | SIMULATE_ARDUINO = False 37 | """Simulate an Arduino in software?""" 38 | if sys.argv[-1] == "simulate": 39 | SIMULATE_ARDUINO = True 40 | 41 | DEBUG = False 42 | """Show debug info in terminal? Warning: Slow! Do not leave on unintentionally. 43 | """ 44 | 45 | print( 46 | f"{qtpy.API_NAME:9s} " 47 | f"{qtpy.QT_VERSION}" # pyright: ignore[reportPrivateImportUsage] 48 | ) 49 | 50 | # ------------------------------------------------------------------------------ 51 | # Main 52 | # ------------------------------------------------------------------------------ 53 | 54 | if __name__ == "__main__": 55 | # Set priority of this process to maximum in the operating system 56 | print(f"PID: {os.getpid()}\n") 57 | try: 58 | proc = psutil.Process(os.getpid()) 59 | if os.name == "nt": 60 | proc.nice(psutil.REALTIME_PRIORITY_CLASS) # Windows 61 | else: 62 | proc.nice(-20) # Other 63 | except Exception: # pylint: disable=broad-except 64 | print("Warning: Could not set process to maximum priority.\n") 65 | 66 | # -------------------------------------------------------------------------- 67 | # Connect to Arduino 68 | # -------------------------------------------------------------------------- 69 | 70 | if SIMULATE_ARDUINO: 71 | ard = FakeWaveGeneratorArduino() 72 | else: 73 | ard = WaveGeneratorArduino() 74 | 75 | ard.serial_settings["baudrate"] = 115200 76 | ard.auto_connect() 77 | 78 | if not ard.is_alive: 79 | print("\nCheck connection and try resetting the Arduino.") 80 | print("Exiting...\n") 81 | sys.exit(0) 82 | 83 | # -------------------------------------------------------------------------- 84 | # Create application 85 | # -------------------------------------------------------------------------- 86 | 87 | app = QtCore.QCoreApplication(sys.argv) 88 | 89 | # -------------------------------------------------------------------------- 90 | # Set up multithreaded communication with the Arduino 91 | # -------------------------------------------------------------------------- 92 | 93 | def DAQ_function() -> bool: 94 | """Perform a single data acquisition. 95 | 96 | Returns: True if successful, False otherwise. 97 | """ 98 | # Query the Arduino for new readings, parse them and update the 99 | # corresponding variables of its `state` member. 100 | if not ard.perform_DAQ(): 101 | return False 102 | 103 | # Use Arduino time or PC time? 104 | now = time.perf_counter() if USE_PC_TIME else ard.state.time 105 | if ard_qdev.update_counter_DAQ == 1: 106 | ard.state.time_0 = now 107 | ard.state.time = 0 108 | else: 109 | ard.state.time = now - ard.state.time_0 110 | 111 | # For demo purposes: Quit automatically after N updates 112 | if ard_qdev.update_counter_DAQ > 1000: 113 | app.quit() 114 | 115 | return True 116 | 117 | ard_qdev = WaveGeneratorArduino_qdev( 118 | dev=ard, 119 | DAQ_function=DAQ_function, 120 | DAQ_interval_ms=DAQ_INTERVAL_MS, 121 | debug=DEBUG, 122 | ) 123 | 124 | # -------------------------------------------------------------------------- 125 | # update_terminal 126 | # -------------------------------------------------------------------------- 127 | 128 | @Slot() 129 | def update_terminal(): 130 | print( 131 | f"{ard_qdev.update_counter_DAQ - 1}\t" 132 | f"{ard.state.time:.3f}\t" 133 | f"{ard.state.reading_1:.4f}", 134 | # end="\r", 135 | # flush=True, 136 | ) 137 | 138 | # -------------------------------------------------------------------------- 139 | # Program termination routines 140 | # -------------------------------------------------------------------------- 141 | 142 | def keyboardInterruptHandler( 143 | keysig, frame 144 | ): # pylint: disable=unused-argument 145 | app.quit() 146 | 147 | # Catch CTRL+C 148 | signal.signal(signal.SIGINT, keyboardInterruptHandler) 149 | 150 | @Slot() 151 | def notify_connection_lost(): 152 | str_msg = ( 153 | f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" 154 | "Lost connection to Arduino." 155 | ) 156 | print(f"\nCRITICAL ERROR @ {str_msg}") 157 | app.quit() 158 | 159 | @Slot() 160 | def about_to_quit(): 161 | app.processEvents() 162 | print("\nAbout to quit") 163 | ard_qdev.quit() 164 | ard.close() 165 | 166 | # -------------------------------------------------------------------------- 167 | # Start the main event loop 168 | # -------------------------------------------------------------------------- 169 | 170 | ard_qdev.signal_DAQ_updated.connect(update_terminal) 171 | ard_qdev.signal_connection_lost.connect(notify_connection_lost) 172 | ard_qdev.start(DAQ_priority=QtCore.QThread.Priority.TimeCriticalPriority) 173 | 174 | app.aboutToQuit.connect(about_to_quit) 175 | sys.exit(app.exec()) 176 | -------------------------------------------------------------------------------- /demo_E_no_GUI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Demonstration of multithreaded live Arduino data 4 | - Terminal output only 5 | - Mode: Device `MasterSync` acts as a master device running on a 6 | `DAQ_TRIGGER.INTERNAL_TIMER` to which another device running on a 7 | `DAQ_TRIGGER.SINGLE_SHOT_WAKE_UP` is slaved to. 8 | """ 9 | __author__ = "Dennis van Gils" 10 | __authoremail__ = "vangils.dennis@gmail.com" 11 | __url__ = "https://github.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo" 12 | __date__ = "11-06-2024" 13 | __version__ = "9.0" 14 | # pylint: disable=missing-function-docstring 15 | 16 | import os 17 | import sys 18 | import time 19 | import datetime 20 | from typing import Union, Callable 21 | import signal # To catch CTRL+C and quit 22 | 23 | import qtpy 24 | from qtpy import QtCore 25 | from qtpy.QtCore import Slot # type: ignore 26 | 27 | import psutil 28 | 29 | from dvg_qdeviceio import QDeviceIO, DAQ_TRIGGER 30 | 31 | from WaveGeneratorArduino import WaveGeneratorArduino, FakeWaveGeneratorArduino 32 | 33 | # Constants 34 | DAQ_INTERVAL_MS = 10 35 | """[ms] Update interval for the data acquisition (DAQ)""" 36 | 37 | # Global flags 38 | USE_PC_TIME = True 39 | """Use Arduino time or PC time?""" 40 | SIMULATE_ARDUINO = False 41 | """Simulate an Arduino in software?""" 42 | if sys.argv[-1] == "simulate": 43 | SIMULATE_ARDUINO = True 44 | 45 | DEBUG = False 46 | """Show debug info in terminal? Warning: Slow! Do not leave on unintentionally. 47 | """ 48 | 49 | print( 50 | f"{qtpy.API_NAME:9s} " 51 | f"{qtpy.QT_VERSION}" # pyright: ignore[reportPrivateImportUsage] 52 | ) 53 | 54 | # ------------------------------------------------------------------------------ 55 | # WaveGeneratorArduino_qdev 56 | # ------------------------------------------------------------------------------ 57 | 58 | 59 | class WaveGeneratorArduino_qdev(QDeviceIO): 60 | """Manages multithreaded communication and periodical data acquisition 61 | for an Arduino that is programmed as a wave generator, referred to as the 62 | 'device'.""" 63 | 64 | def __init__( 65 | self, 66 | dev: Union[WaveGeneratorArduino, FakeWaveGeneratorArduino], 67 | DAQ_function: Union[Callable[[], bool], None] = None, 68 | DAQ_interval_ms=10, 69 | DAQ_timer_type=QtCore.Qt.TimerType.PreciseTimer, 70 | critical_not_alive_count=1, 71 | debug=False, 72 | **kwargs, 73 | ): 74 | super().__init__(dev, **kwargs) # Pass kwargs onto QtCore.QObject() 75 | self.dev: WaveGeneratorArduino # Enforce type: removes `_NoDevice()` 76 | 77 | self.create_worker_DAQ( 78 | DAQ_trigger=DAQ_TRIGGER.SINGLE_SHOT_WAKE_UP, 79 | DAQ_function=DAQ_function, 80 | DAQ_interval_ms=DAQ_interval_ms, 81 | DAQ_timer_type=DAQ_timer_type, 82 | critical_not_alive_count=critical_not_alive_count, 83 | debug=debug, 84 | ) 85 | self.create_worker_jobs(debug=debug) 86 | 87 | def request_set_waveform_to_sine(self): 88 | """Request sending out a new instruction to the Arduino to change to a 89 | sine wave.""" 90 | self.send(self.dev.set_waveform_to_sine) 91 | 92 | def request_set_waveform_to_square(self): 93 | """Request sending out a new instruction to the Arduino to change to a 94 | square wave.""" 95 | self.send(self.dev.set_waveform_to_square) 96 | 97 | def request_set_waveform_to_sawtooth(self): 98 | """Request sending out a new instruction to the Arduino to change to a 99 | sawtooth wave.""" 100 | self.send(self.dev.set_waveform_to_sawtooth) 101 | 102 | 103 | # ------------------------------------------------------------------------------ 104 | # Main 105 | # ------------------------------------------------------------------------------ 106 | 107 | if __name__ == "__main__": 108 | # Set priority of this process to maximum in the operating system 109 | print(f"PID: {os.getpid()}\n") 110 | try: 111 | proc = psutil.Process(os.getpid()) 112 | if os.name == "nt": 113 | proc.nice(psutil.REALTIME_PRIORITY_CLASS) # Windows 114 | else: 115 | proc.nice(-20) # Other 116 | except Exception: # pylint: disable=broad-except 117 | print("Warning: Could not set process to maximum priority.\n") 118 | 119 | # -------------------------------------------------------------------------- 120 | # Connect to Arduino 121 | # -------------------------------------------------------------------------- 122 | 123 | if SIMULATE_ARDUINO: 124 | ard = FakeWaveGeneratorArduino() 125 | else: 126 | ard = WaveGeneratorArduino() 127 | 128 | ard.serial_settings["baudrate"] = 115200 129 | ard.auto_connect() 130 | 131 | if not ard.is_alive: 132 | print("\nCheck connection and try resetting the Arduino.") 133 | print("Exiting...\n") 134 | sys.exit(0) 135 | 136 | # -------------------------------------------------------------------------- 137 | # Create application 138 | # -------------------------------------------------------------------------- 139 | 140 | app = QtCore.QCoreApplication(sys.argv) 141 | 142 | # -------------------------------------------------------------------------- 143 | # Set up multithreaded communication with the Arduino 144 | # -------------------------------------------------------------------------- 145 | 146 | def my_DAQ_function() -> bool: 147 | """Perform a single data acquisition. 148 | 149 | Returns: True if successful, False otherwise. 150 | """ 151 | # Query the Arduino for new readings, parse them and update the 152 | # corresponding variables of its `state` member. 153 | if not ard.perform_DAQ(): 154 | return False 155 | 156 | # Use Arduino time or PC time? 157 | now = time.perf_counter() if USE_PC_TIME else ard.state.time 158 | if ard_qdev.update_counter_DAQ == 1: 159 | ard.state.time_0 = now 160 | ard.state.time = 0 161 | else: 162 | ard.state.time = now - ard.state.time_0 163 | 164 | # For demo purposes: Quit automatically after N updates 165 | if ard_qdev.update_counter_DAQ > 1000: 166 | app.quit() 167 | 168 | return True 169 | 170 | # Here, `WaveGeneratorArduino_qdev` uses `DAQ_TRIGGER.SINGLE_SHOT_WAKE_UP` 171 | # acting as a slave device. 172 | ard_qdev = WaveGeneratorArduino_qdev( 173 | dev=ard, 174 | DAQ_function=my_DAQ_function, 175 | DAQ_interval_ms=DAQ_INTERVAL_MS, 176 | debug=DEBUG, 177 | ) 178 | 179 | # `MasterSync` will generate the clock to which `WaveGeneratorArduino_qdev` 180 | # will be slaved to. 181 | class MasterSync: 182 | def __init__(self, slave: QDeviceIO): 183 | self.slave_qdev = slave 184 | self.name = "Sync" 185 | 186 | def tick(self) -> bool: 187 | self.slave_qdev.wake_up_DAQ() 188 | return True 189 | 190 | sync = MasterSync(slave=ard_qdev) 191 | sync_qdev = QDeviceIO(sync) 192 | 193 | # Create worker 194 | sync_qdev.create_worker_DAQ( 195 | DAQ_trigger=DAQ_TRIGGER.INTERNAL_TIMER, 196 | DAQ_function=sync.tick, 197 | DAQ_interval_ms=DAQ_INTERVAL_MS, 198 | critical_not_alive_count=1, 199 | debug=DEBUG, 200 | ) 201 | 202 | # -------------------------------------------------------------------------- 203 | # update_terminal 204 | # -------------------------------------------------------------------------- 205 | 206 | @Slot() 207 | def update_terminal(): 208 | print( 209 | f"{ard_qdev.update_counter_DAQ - 1}\t" 210 | f"{ard.state.time:.3f}\t" 211 | f"{ard.state.reading_1:.4f}", 212 | # end="\r", 213 | # flush=True, 214 | ) 215 | 216 | # -------------------------------------------------------------------------- 217 | # Program termination routines 218 | # -------------------------------------------------------------------------- 219 | 220 | def keyboardInterruptHandler( 221 | keysig, frame 222 | ): # pylint: disable=unused-argument 223 | app.quit() 224 | 225 | # Catch CTRL+C 226 | signal.signal(signal.SIGINT, keyboardInterruptHandler) 227 | 228 | @Slot() 229 | def notify_connection_lost(): 230 | str_msg = ( 231 | f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" 232 | "Lost connection to Arduino." 233 | ) 234 | print(f"\nCRITICAL ERROR @ {str_msg}") 235 | app.quit() 236 | 237 | @Slot() 238 | def about_to_quit(): 239 | app.processEvents() 240 | print("\nAbout to quit") 241 | sync_qdev.quit() 242 | ard_qdev.quit() 243 | ard.close() 244 | 245 | # -------------------------------------------------------------------------- 246 | # Start the main event loop 247 | # -------------------------------------------------------------------------- 248 | 249 | ard_qdev.signal_DAQ_updated.connect(update_terminal) 250 | ard_qdev.signal_connection_lost.connect(notify_connection_lost) 251 | ard_qdev.start(DAQ_priority=QtCore.QThread.Priority.TimeCriticalPriority) 252 | sync_qdev.start(DAQ_priority=QtCore.QThread.Priority.TimeCriticalPriority) 253 | 254 | app.aboutToQuit.connect(about_to_quit) 255 | sys.exit(app.exec()) 256 | -------------------------------------------------------------------------------- /images/Arduino_PyQt_demo_with_multithreading.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dennis-van-Gils/DvG_Arduino_PyQt_multithread_demo/f29fe0a628e5e951da168a038fe75d247860b125/images/Arduino_PyQt_demo_with_multithreading.PNG -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy~=1.15 2 | psutil~=5.6 3 | pyopengl~=3.1 4 | pyqtgraph~=0.11 5 | qtpy 6 | 7 | dvg-debug-functions~=2.4 8 | dvg-devices~=1.4 9 | dvg-pyqt-controls~=1.4 10 | dvg-pyqt-filelogger~=1.3 11 | dvg-pyqtgraph-threadsafe~=3.3 12 | dvg-qdeviceio~=1.3 13 | --------------------------------------------------------------------------------