Pythonic solution for accessing class properties - Page 1
Forum section: electronics / general

author: rx8pilot
author_rank: Super Contributor
timestamp: November 13, 2020, 08:31:32 pm
content: [Coming from a C world, please forgive if my nomenclature is not perfect]In the context of trying to build a GUI with PyQt5. I have developed a dynamic layout and need to update a variable number of properties.Code:def prgsUpdate(self, barNumber, minVal, maxVal, remainVal):''' Update progress bar specified by barNumber 0-6 '''self.ui.cycleRemain0.setMinimum(minVal)self.ui.cycleRemain0.setMaximum(maxVal)self.ui.cycleRemain0.setProperty("value", remainVal)print (f"CHANGE: ", remainVal)The goal here is to use the input variable 'barNumber' to address the 'cycleRemain0' property which there can by any number of - 'cycleRemain1, cycleRemain2, cycleRemain3' etc, etc. The number is updated dynamically.What is the most 'Pythonic' approach to keep a function like this simple so I can call this and update 'self.ui.cycleRemain4......'. 'barNumber = 4'Code:prgsUpdate(4, 0, 100, 50)

author: gmb42
author_rank: Frequent Contributor
timestamp: November 14, 2020, 12:57:28 pm
content: Why are your progress bar instances held as named classes other than some form of collection, e.g. in an array?

author: rx8pilot
author_rank: Super Contributor
timestamp: November 14, 2020, 08:24:26 pm
content: Quote from: gmb42 on November 14, 2020, 12:57:28 pmFor this application - the class that defines the progress bars is automatically generated based on information in a config file or database. Not so sure that is a great way to accomplish the dynamic functionality where the application can deal with all sorts of GUI layouts that are chosen based on the data available or the user preference. The class is built just before the objects are instantiated. Seems clunky and wrong - which is why I am here. My brain is struggling to escape the clutches of C.The broad goal here specifically is to have a GUI with any number of progress bars, limited by the display resolution available. After those progress bars are in place, having a clean and concise function that can update them based on the name of the progress bar object.I hope this makes sense, struggling to explain myself

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 14, 2020, 10:33:08 pm
content: You can usebar = self.ui.__getattribute__("cycleRemain%d" %index). If there is no such member, it'll raise an AttributeError exception.You can also defineself.bars = [ self.ui.cycleRemain0, self.ui.cycleRemain1, self.ui.cycleRemain2 ]and then usebar = self.barsto refer to one of the three properties. Theself.barsis then an array of references to those objects, it does not create a deep copy, or copy the objects themselves.In Qt, use thenameattributes (you already use cycleRemain0 etc.), andbar = self.ui.findChild(QtWidgets.QProgressBar, "cycleRemain%d" %index). It evaluates to a reference to the named QtWidget. All you need to know are the widget names (which is, unsurprisingly, provided by the.objectName()member function). If you use logical widget names, you can discover them dynamically too; seeQObjectdocumentation for details.

author: rx8pilot
author_rank: Super Contributor
timestamp: November 15, 2020, 05:50:52 am
content: Quote from: Nominal Animal on November 14, 2020, 10:33:08 pmInteresting!Just recently learning about the magic methods....trying this now

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 15, 2020, 07:25:41 am
content: Quote from: Nominal Animal on November 14, 2020, 10:33:08 pmI am just curious. Is there a reason why you get the instance attribute at such a low level? Does Qt sometimes do unhelpful things using __getattr__()?

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 15, 2020, 11:08:13 am
content: Quote from: jfiresto on November 15, 2020, 07:25:41 amNo, I just listed the choices from lowest level to highest level. I don't use the low level methods with Qt, only.findChild().

author: bd139
author_rank: Super Contributor
timestamp: November 15, 2020, 11:20:17 am
content: Code:class Thing(object):def __init__(self):self.prop1 = 1self.prop2 = 2self.prop3 = 3def setProp(self, n, value):setattr(self, "prop" + str(n), value)x = Thing()print (x.prop2)x.setProp(2, 5)print (x.prop2)

author: janoc
author_rank: Super Contributor
timestamp: November 15, 2020, 01:55:58 pm
content: Why not just use Python's property support instead?https://www.programiz.com/python-programming/property(scroll down for the final code using the @property decorators).Wrap your Qt UI element access in a pair of setters/getters and make them into a property. Much better than messing with the low level attribute access.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 15, 2020, 04:24:25 pm
content: To repeat: Use PyQt5ui.findChild()to find the widget whose properties you are modifying, supplying it the samename(as a string) as you used in the UI (XML file if using Qt Designer, QML if using Qt Quick – I prefer the Qt Designer XML/loadUi()approach).Do not try to do it Pythonically. When using PyQt5, use Python Qt5 facilities. I recommend against usingpyuicto compile the UI files to Python, and instead build the interface dynamically at application startup from the XML or QML description directly. If you do wish to create the layout in Python, write the code yourself, instead of using user interface generators, and make the layout adapt to different window sizes and font sizes gracefully.Name the widgets systematically. If they vary (because you have multiple UIs for the same engine, or the number of properties controlled by the UI varies for some other reason), traverse the Python UI object hierarchy: all widgets inherit from QObject, which provides both the traversal functions and the name properties.(I'm particularly fond of allowing end users write their own variant .ui files, with the application having a menu to choose between existing UIs. The "save all widget data properties then restore them to the new UI" is so annoying to implement I wouldn't bother; treat switching UIs as basically an app restart.)PyQt5 and PySide2 interfaces differminimally: enough to trip you if you aren't careful of how you import the Qt modules, but if you do it right, your code will work with either just fine. The two do the same thing, exposing Qt5 to Python, but PyQt5 has a longer history, being developed by Riverbank Computing Ltd, whereas PySide2 is relatively recent but provided by the Qt project itself.QtPyprovides just such a compatibility layer – includinguic,QComboBox, andQHeaderViewcompatibility objects (the only "real" differences between PyQt5 and PySide2) – but you can do your own. A typical application (using uic to build UIs dynamically) only really needsCode:# -*- coding: utf-8 -*-import syswhile True:# Prefer PySide2, if installed.try:from PySide2 import QtGui, QtWidgets, QtCoreimport uic.loadUifrom PySide2.QtCore import Signal, Slotbreakexcept:pass# Fall back to PyQt5, if installed.try:from PyQt5 import QtGui, QtWidgets, QtCorefrom PyQt5.uic import loadUifrom PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slotbreakexcept:pass# No Qt5 support. Fail.sys.stderr.write("No Qt5 support!\n")sys.exit(1)noting that the decorators are then@Signaland@Slot, and the modules are prefixed byQtGui,QtWidgets,andQtCore; withloadUi()in the root namespace (no module prefix).The fallback PySide2uic.pymodule (in the same directory) is then (ripped off from QtPy uic.py),Code:# -*- coding: utf-8 -*-__all__ = ['loadUi']# In PySide, loadUi does not exist, so we define it using QUiLoader, and# then make sure we expose that function. This is adapted from qt-helpers# which was released under a 3-clause BSD license:# qt-helpers - a common front-end to various Qt modules## Copyright (c) 2015, Chris Beaumont and Thomas Robitaille## All rights reserved.## Redistribution and use in source and binary forms, with or without# modification, are permitted provided that the following conditions are# met:## * Redistributions of source code must retain the above copyright# notice, this list of conditions and the following disclaimer.# * Redistributions in binary form must reproduce the above copyright# notice, this list of conditions and the following disclaimer in the# documentation and/or other materials provided with the# distribution.# * Neither the name of the Glue project nor the names of its contributors# may be used to endorse or promote products derived from this software# without specific prior written permission.## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.## Which itself was based on the solution at## https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8## which was released under the MIT license:## Copyright (c) 2011 Sebastian Wiesner <lunaryorn@gmail.com># Modifications by Charl Botha <cpbotha@vxlabs.com>## Permission is hereby granted, free of charge, to any person obtaining a# copy of this software and associated documentation files (the "Software"),# to deal in the Software without restriction, including without limitation# the rights to use, copy, modify, merge, publish, distribute, sublicense,# and/or sell copies of the Software, and to permit persons to whom the# Software is furnished to do so, subject to the following conditions:## The above copyright notice and this permission notice shall be included in# all copies or substantial portions of the Software.## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER# DEALINGS IN THE SOFTWARE.from PySide2.QtCore import QMetaObjectfrom PySide2.QtUiTools import QUiLoaderclass UiLoader(QUiLoader):"""Subclass of :class:`~PySide.QtUiTools.QUiLoader` to create the userinterface in a base instance.Unlike :class:`~PySide.QtUiTools.QUiLoader` itself this class does notcreate a new instance of the top-level widget, but creates the userinterface in an existing instance of the top-level class if needed.This mimics the behaviour of :func:`PyQt4.uic.loadUi`."""def __init__(self, baseinstance, customWidgets=None):"""Create a loader for the given ``baseinstance``.The user interface is created in ``baseinstance``, which must be aninstance of the top-level class in the user interface to load, or asubclass thereof.``customWidgets`` is a dictionary mapping from class name to classobject for custom widgets. Usually, this should be done by callingregisterCustomWidget on the QUiLoader, but with PySide 1.1.2 onUbuntu 12.04 x86_64 this causes a segfault.``parent`` is the parent object of this loader."""QUiLoader.__init__(self, baseinstance)self.baseinstance = baseinstanceif customWidgets is None:self.customWidgets = {}else:self.customWidgets = customWidgetsdef createWidget(self, class_name, parent=None, name=''):"""Function that is called for each widget defined in ui file,overridden here to populate baseinstance instead."""if parent is None and self.baseinstance:# supposed to create the top-level widget, return the base# instance insteadreturn self.baseinstanceelse:# For some reason, Line is not in the list of available# widgets, but works fine, so we have to special case it here.if class_name in self.availableWidgets() or class_name == 'Line':# create a new widget for child widgetswidget = QUiLoader.createWidget(self, class_name, parent, name)else:# If not in the list of availableWidgets, must be a custom# widget. This will raise KeyError if the user has not# supplied the relevant class_name in the dictionary or if# customWidgets is empty.try:widget = self.customWidgets(parent)except KeyError:raise Exception('No custom widget ' + class_name + ' ''found in customWidgets')if self.baseinstance:# set an attribute for the new child widget on the base# instance, just like PyQt4.uic.loadUi does.setattr(self.baseinstance, name, widget)return widgetdef _get_custom_widgets(ui_file):"""This function is used to parse a ui file and look for the <customwidgets>section, then automatically load all the custom widget classes."""import sysimport importlibfrom xml.etree.ElementTree import ElementTree# Parse the UI fileetree = ElementTree()ui = etree.parse(ui_file)# Get the customwidgets sectioncustom_widgets = ui.find('customwidgets')if custom_widgets is None:return {}custom_widget_classes = {}for custom_widget in custom_widgets.getchildren():cw_class = custom_widget.find('class').textcw_header = custom_widget.find('header').textmodule = importlib.import_module(cw_header)custom_widget_classes = getattr(module, cw_class)return custom_widget_classesdef loadUi(uifile, baseinstance=None, workingDirectory=None):"""Dynamically load a user interface from the given ``uifile``.``uifile`` is a string containing a file name of the UI file to load.If ``baseinstance`` is ``None``, the a new instance of the top-levelwidget will be created. Otherwise, the user interface is created withinthe given ``baseinstance``. In this case ``baseinstance`` must be aninstance of the top-level widget class in the UI file to load, or asubclass thereof. In other words, if you've created a ``QMainWindow``interface in the designer, ``baseinstance`` must be a ``QMainWindow``or a subclass thereof, too. You cannot load a ``QMainWindow`` UI filewith a plain :class:`~PySide.QtGui.QWidget` as ``baseinstance``.:method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called onthe created user interface, so you can implemented your slots accordingto its conventions in your widget class.Return ``baseinstance``, if ``baseinstance`` is not ``None``. Otherwisereturn the newly created instance of the user interface."""# We parse the UI file and import any required custom widgetscustomWidgets = _get_custom_widgets(uifile)loader = UiLoader(baseinstance, customWidgets)if workingDirectory is not None:loader.setWorkingDirectory(workingDirectory)widget = loader.load(uifile)QMetaObject.connectSlotsByName(widget)return widgetThis way, your Python3 Qt5 application will Just Work™. The QtPy is even better compatibility layer implementation than the above snippet, as it implements various version checks and QComboBox/QHeaderView compatibility objects, but it introduces Yet Another Library Dependency, so I tend to recommend this crude-and-simple method instead.Do not do any intensive calculation or communication in the main thread. If possible, use themultiprocessingmodule, so that multiple CPU cores can be used. The standard Python interpreter only executes one thread of Python code at a time within each process. If you do e.g. serial or socket communications, feel free to usethreadingfor those; it is okay to have many Python threads blocking/waiting in IO (they won't hinder the Python-executing thread, and the interpreter will switch when something happens; essentially, think of it as running on a single-CPU-core hardware). It is only intensive, heavy CPU computation that you want to offload to separate processes. You often want to use a thread in the main Qt5 Python process, running either a Qt5 event queue, or a Python Queue to communicate with the UI thread in a safe, robust manner. (Use Qt5 event queue if you want to pass work using Qt5signals and slots, Python Queue if you want to pass Python objects or raw data.)This ensures that your UI remains interactive and snappy at all times even on small SBCs. The overhead of threads and multiprocessing.shared_memory (to pass data via shared memory across multiple CPU-intensive Python processes) is insignificant in context; the Python interpreter itself isn't "perfect".For best results, write any really CPU-intensive work in C (I prefer POSIX C, with pthreads; this works in Linux, BSDs, and Mac OS – I don't use Windows, so for Windows you might need to add an abstraction layer). The built-inctypesPython module makes this interfacing rather simple. Compile such C libraries statically, without dynamic dependencies (other than what are built in to standard C library itself), and your Python + C application is portable across all Linux distributions using that single library for that hardware architecture. For other hardware architectures, and other OSes (including Windows), you only need to recompile the library. If you have the time, cross-compile the library to all architectures you can think of. Determine the library to be used fromsys.platform(OS type) and if'linux', the hardware architecture fromos.uname().machine. (Naming the C librariesfoo-linux-x86_64.soorfoo-win32.dllorfoo-win64.dllwill make the code particularly easy and straightforward.)As I've mentioned before, it is not difficult to write completely portable applications with Python3 and Qt5 user interfaces. (You can, but do not have to, even compile them into Windows etc. exececutables using PyInstaller.) With the above suggestions, you can make a package/archive/CD/USB stick, that you can stick to basically any common OS (on any hardware architecture you have compiled the C library for), and execute the Python + C library application directly. True MultiArch applications, yo.

author: rx8pilot
author_rank: Super Contributor
timestamp: November 15, 2020, 07:20:41 pm
content: So, my first lesson in this thread is that PyQt5 is where a full and complete understanding of classes and inheritance is a per-requisite .The information here helps in so many more ways than just the immediate problem I am trying to solve.This approach is what has been kicking around in my mind. Since my Python-Fu is still rather limited, I keep trying to figure out how to best mesh good software design with the strengths and weaknesses of Python.Quote

author: rx8pilot
author_rank: Super Contributor
timestamp: November 15, 2020, 07:26:04 pm
content: Quote from: Nominal Animal on November 15, 2020, 04:24:25 pmYes - indeed. My current self-education efforts are focused on threading in QT. On paper it seems straightforward, but in practice I need to clunk my way through some experiments before it will sink in. My goal is to create UI's that are almost always waiting on some data to arrive from slow sources. Without threading, the UI would be locked up almost all the time.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 15, 2020, 10:33:43 pm
content: Indeedlydeedly. If you have an Arduino microcontroller, you might find the following sketch useful:Code:/* Delayed echo. This is in Public Domain.** Waits for a non-empty input message (terminating with any newline encoding),* which is echoed back after a fixed delay.* (Only the first MAX_MESSAGE_LEN bytes of the message* are recorded and sent back, but the incoming message* can be of any length.)*/// Maximum message length#define MAX_MESSAGE_LEN 255// Message echo delay in milliseconds#define MESSAGE_DELAY_MS 750// End-of-message character used in the responses#define MESSAGE_NEWLINE '\n'// LED pin to light while keeping a message; can leave undefined#define MESSAGE_LED_PIN 13unsigned char message;void setup() {Serial.begin(115200);#ifdef MESSAGE_LED_PINpinMode(MESSAGE_LED_PIN, OUTPUT);#endif}void loop() {size_t n;int c;#ifdef MESSAGE_LED_PINdigitalWrite(MESSAGE_LED_PIN, LOW);#endif// For microcontrollers with native USB, wait for USB connection.while (!Serial) /* nothing */ ;// Receive messagen = 0;while (1) {c = Serial.read();if (!Serial) return;// Newline or NUL?if (c == '\n' || c == '\r' || c == '\0') {if (n) {// Message terminatorbreak;} else {// Garbage from a previous multi-character terminator, ignorecontinue;}}if (c > 0 && c < 256 && n < MAX_MESSAGE_LEN) {message[n++] = c;#ifdef MESSAGE_LED_PINdigitalWrite(MESSAGE_LED_PIN, (n & 1) ? HIGH : LOW);delayMicroseconds(4);#endif}}// USB disconnect?if (!Serial) return;#ifdef MESSAGE_LED_PINdigitalWrite(MESSAGE_LED_PIN, HIGH);delay(MESSAGE_DELAY_MS);digitalWrite(MESSAGE_LED_PIN, LOW);#elsedelay(MESSAGE_DELAY_MS);#endifif (!Serial) return;Serial.write(message, n);if (!Serial) return;Serial.write(MESSAGE_NEWLINE);if (!Serial) return;Serial.flush();}The idea is that you can experiment with slow serial-type communicators with this. The above is configured for Teensy 3.x (which have the LED on pin 13), but should work on any Arduino, if you change the MESSAGE_LED pin (or omit it). It echoes the message back after a delay, lighting the LED (if you define MESSAGE_LED_PIN) so you can see when it is active, making debugging much, much easier. It seems to have an excess of !Serial checks, but those just ensure that for native-USB microcontrollers like Teensies, if your program closes the serial device early, it "aborts" the echo and won't send the trailing garbage to whatever process opens the serial device next.You can test it in the Arduino serial monitor. To test it on the command line, first find out the character device corresponding to the microcontroller (usually /dev/ttyACM0), and then in a Bash shell, run( exec 3<>/dev/ttyACM0 ; stty raw <&3 ; printf 'your C-like string with escapes goes here\n' >&3 ; read -u 3 FOO ; echo "$FOO" )If you use Linux or Mac, I recommend using the Python termios module. (I haven't actually usedQSerialPort, because it is a separately-installed module, and I'm not sure PySide2 support for it is complete yet.) Remember to set the serial port to raw, or at minimum disable echo, because otherwise the microcontroller will get stuck in a loop processing its own output. This seems to catch a lot of people playing with microcontrollers!(Most of the Python serial libraries I have looked at are utter crap, and lack basic error checking; they contain stuff like assuming POSIX Cwrite()never fails, or never returns a short count... hyurgh. Leaves a really bad taste in my mouth. At least with termios, you can do it correctly in plain Python.)

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 16, 2020, 07:35:44 am
content: Quote from: Nominal Animal on November 15, 2020, 10:33:43 pmWould you includepyserial?I ended up using that for a simple DMM logging utility that would also work under Windows. Here are the utility's bugs through no fault of its own:Code:BUGSThis program ignores the DMM for the first 100 ms -- and thepossible, stale serial data in the que when the program started.Pyserial 2.7 ignores OSX and Windows serial data parity errors.A Control-C under Windows may wait until the DMM sends a line.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 16, 2020, 05:40:02 pm
content: Quote from: jfiresto on November 16, 2020, 07:35:44 amWhich version? Theissue listis pretty long..pyserial tries to be everything for everybody. It never ends well.Partially the problem is that we still treat binary stream devices like they were terminals, and they're really not; terminals have all sorts of baggage from the last few decades... The other problem is that the different OSes really implement different rules, and trying to shoehorn them all under one shim is just .. not going to work well.As to stale garbage data, I habitually discard the kernel buffer when changing terminal settings (viatermios.tcflush(descriptor, termios.TCIOFLUSH)) to avoid getting already received but unread stale data. My Arduino sketches have those!Serialtests for the same reason:Serialturns true whenever an application has the tty device open, and when the last application closes the device,Serialturns false.If you use Python termios, set PARMRK and clear IGNPAR in the first attribute. Then, input byte 255 will be read as 255 255, and byteXthat had a parity error will read as 255 0X. (You do still need to check each read byte in sequence, as the first 255 in a sequence of 255s and 0s is the key.)Just for fun, I wrote a stupid (and probably buggy!) but simple example of how to use Qt5 QThread and Qt signals and slots to pass serial device commands, requests, and responses between the main thread and the serial worker thread. Now, the serial worker itself is not very good, because it does not use nonblocking I/O with the interruption pipe (that allows a secondary helper thread to impose operation-specific timeouts that termios cannot – VTIME is interbyte timeout, not message timeout), but this way the example is only a bit over 300 lines long. (It does expect theuic.pyfrom my previous post, and is intended to be used with the Arduino delayed echo sketch.)Code:#!/usr/bin/python3# -*- coding: utf-8 -*-import osimport sysimport termiostry:from PySide2 import QtCore, QtWidgetsfrom PySide2.QtCore import Slot, Signalfrom uic import loadUiexcept:try:from PyQt5 import QtCore, QtWidgetsfrom PyQt5.QtCore import pyqtSlot as Slot, pyqtSignal as Signalfrom PyQt5.uic import loadUiexcept:raise NameError("No Python Qt5 support found")class SerialWorker(QtCore.QObject):success = Signal(int)failure = Signal(int, str)response = Signal(int, bytes)def __init__(self, parent=None):super().__init__(parent)self.fd = -1self.tty = Nonedef __del__(self):if self.fd != -1:if self.tty is not None:try:termios.tcflush(self.fd, TCIOFLUSH)termios.tcsetattr(self.fd, TCSANOW, self.tty)except:passself.tty = Nonetry:os.close(self.fd)except:passself.fd = -1def _abort(self):if self.fd != -1:if self.tty is not None:try:termios.tcflush(self.fd, TCIOFLUSH)termios.tcsetattr(self.fd, TCSANOW, self.tty)except:passself.tty = Nonetry:os.close(self.fd)except:passself.fd = -1@Slot(int, str)def open(self, ident, path):if self.fd != -1:self.failure.emit(ident, "Already open")return# Open the named devicetry:fd = os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_CLOEXEC)except OSError as err:self.failure.emit(ident, err.strerror)return# Obtain termios settingstry:oldtty = termios.tcgetattr(fd)newtty = termios.tcgetattr(fd)except:os.close(fd)self.failure.emit(ident, "Not a serial device")return# Raw mode, blocking readsnewtty &= ~(termios.ISTRIP | termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IUCLC)newtty |= termios.IGNBRKnewtty &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL | termios.ONOCR | termios.ONLRET)newtty &= ~(termios.CBAUD | termios.CSIZE | termios.CSTOPB | termios.PARENB | termios.PARODD)newtty |= termios.B115200 | termios.CS8 | termios.CREAD | termios.HUPCLnewtty &= ~(termios.ISIG | termios.ICANON | termios.ECHO | termios.IEXTEN)newtty = termios.B115200newtty = termios.B115200newtty[termios.VTIME] = 0newtty[termios.VMIN] = 1try:termios.tcflush(fd, termios.TCIOFLUSH)termios.tcsetattr(fd, termios.TCSANOW, newtty)except:os.close(fd)self.failure.emit(ident, "Not a serial device")# Opened successfully.self.fd = fdself.tty = oldttyself.success.emit(ident)@Slot(int)def close(self, ident):if self.fd == -1:self.failure.emit(ident, "Not opened yet")returnif self.tty is not None:try:termios.tcflush(self.fd, termios.TCIOFLUSH)termios.tcsetattr(self.fd, termios.TCSANOW, self.tty)except:passself.tty = Noneos.close(self.fd)self.success.emit(ident)@Slot(int, bytes)def request(self, ident, data):if self.fd == -1 or self.tty is None:self.failure.emit(ident, "Not open")returnif len(data) < 1:self.failure.emit(ident, "Empty request")return# Append newlinedata += b'\n'# Write all of the data.while len(data) > 0:try:n = os.write(self.fd, data)except OSError as err:self._abort()self.failure.emit(ident, err.strerror)returnif n > 0:data = data[n:]# Read reply, until a newline or NULreply = b''while (b'\0' not in reply) and (b'\r' not in reply) and (b'\n' not in reply):try:more = os.read(self.fd, 4095) # 4095 is termios buffer sizeexcept OSError as err:self._abort()self.failure.emit(ident, err.strerror)returnreply += morereply.lstrip(b'\0\r\n')# Assume we got a single reply. Remove any duplicate separators.data = reply.replace(b'\0', b'').replace(b'\r', b'').replace(b'\n', b'')self.response.emit(ident, data)returnclass SerialDevice(QtCore.QObject):"""Serial device management class.This uses a separate QThread to manage the actual serial port communications."""success = Signal(int)failure = Signal(int, str)response = Signal(int, bytes)_open = Signal(int, str)_close = Signal(int)_request = Signal(int, bytes)def __init__(self, parent=None):super().__init__(parent)self.ident = 0self.thread = QtCore.QThread()self.thread.setStackSize(131072)self.thread.setObjectName('serialThread')self.thread.setTerminationEnabled(True)self.worker = SerialWorker()self.worker.moveToThread(self.thread)self.thread.finished.connect(self.worker.deleteLater)self._open.connect(self.worker.open)self._close.connect(self.worker.close)self._request.connect(self.worker.request)self.worker.success.connect(self.success)self.worker.failure.connect(self.failure)self.worker.response.connect(self.response)self.thread.start()def __del__(self):self.thread.quit()if not self.thread.wait(5000):self.thread.terminate()self.thread.wait()del self.threaddel self.workerdef open(self, path: str) -> int:if not isinstance(path, str) or len(path) < 1:return 0try:self._open.emit(self.ident + 1, path)except:return 0self.ident += 1return self.identdef close(self) -> int:try:self._close.emit(self.ident + 1)except:return 0self.ident += 1return self.identdef request(self, data: bytes) -> int:if not isinstance(data, bytes) or len(data) < 1 or b'\0' in data or b'\r' in data or b'\n' in data:return 0try:self._request.emit(self.ident + 1, data)except:return 0self.ident += 1return self.identclass Ui(QtCore.QObject):def __init__(self, uifile, parent=None):super().__init__(parent)self.serial = SerialDevice(self)self.serial.success.connect(self.got_success)self.serial.failure.connect(self.got_failure)self.serial.response.connect(self.got_response)self.ui = loadUi(uifile)self.ui.findChild(QtWidgets.QWidget, 'connectButton').clicked.connect(self.do_connect)self.ui.findChild(QtWidgets.QWidget, 'deviceEdit').returnPressed.connect(self.do_connect)self.ui.findChild(QtWidgets.QWidget, 'disconnectButton').clicked.connect(self.do_disconnect)self.ui.findChild(QtWidgets.QWidget, 'commandEdit').returnPressed.connect(self.do_submit)# Shorthand propertiesself.widget = {}for name in ('deviceEdit', 'commandEdit', 'responseText'):self.widget = self.ui.findChild(QtWidgets.QWidget, name)def do_connect(self):ident = self.serial.open(self.widget['deviceEdit'].text())if ident:self.widget['responseText'].append("%d < Opening device .." % ident)def do_disconnect(self):ident = self.serial.close()if ident:self.widget['responseText'].append("%d < Closing device .." % ident)def do_submit(self):text = str(self.widget['commandEdit'].text())ident = self.serial.request(text.encode(encoding='utf-8', errors='replace'))if ident:self.widget['responseText'].append("%d < Sending '%s' .." % (ident, text))self.widget['commandEdit'].setText("")@Slot(int)def got_success(self, ident):self.widget['responseText'].append("%d > Done." % ident)@Slot(int, str)def got_failure(self, ident, message):self.widget['responseText'].append("%d > Failed: %s." % (ident, message))pass@Slot(int, bytes)def got_response(self, ident, data):self.widget['responseText'].append("%d > Received '%s' (%d bytes)." % (ident, data.decode(encoding='utf-8', errors='ignore'), len(data)))def show(self):self.ui.show()if __name__ == '__main__':QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)app = QtWidgets.QApplication(sys.argv)win = Ui("main.ui")win.show()status = app.exec_()del win, appsys.exit(status)and themain.uiI used isCode:<?xml version="1.0" encoding="UTF-8"?><ui version="4.0"><class>Ui</class><widget class="QWidget" name="Ui"><property name="geometry"><rect><x>0</x><y>0</y><width>368</width><height>172</height></rect></property><property name="windowTitle"><string>Serial Example</string></property><layout class="QGridLayout" name="layout" rowstretch="0,0,1" columnstretch="0,1,0,0"><item row="0" column="0" alignment="Qt::AlignRight"><widget class="QLabel" name="deviceLabel"><property name="text"><string>Device:</string></property></widget></item><item row="2" column="0" alignment="Qt::AlignRight|Qt::AlignTop"><widget class="QLabel" name="responseLabel"><property name="text"><string>Response:</string></property></widget></item><item row="0" column="1"><widget class="QLineEdit" name="deviceEdit"><property name="text"><string>/dev/ttyACM0</string></property></widget></item><item row="1" column="0" alignment="Qt::AlignRight"><widget class="QLabel" name="commandLabel"><property name="text"><string>Command:</string></property></widget></item><item row="0" column="2"><widget class="QPushButton" name="connectButton"><property name="text"><string>Connect</string></property></widget></item><item row="0" column="3"><widget class="QPushButton" name="disconnectButton"><property name="text"><string>Disconnect</string></property></widget></item><item row="1" column="1" colspan="3"><widget class="QLineEdit" name="commandEdit"/></item><item row="2" column="1" colspan="3"><widget class="QTextEdit" name="responseText"><property name="lineWrapMode"><enum>QTextEdit::NoWrap</enum></property><property name="readOnly"><bool>true</bool></property></widget></item></layout></widget><resources/><connections/></ui>which you can open in Qt Designer if you like.If I use one of my Teensies, connect, and then type garbage fast, and finally disconnect, here's what it shows in the text area:Code:1 < Opening device ..1 > Done.2 < Sending 'a' ..3 < Sending 's' ..4 < Sending 'd' ..5 < Sending 'f' ..2 > Received 'a' (1 bytes).6 < Sending 'e' ..3 > Received 's' (1 bytes).7 < Sending 'd' ..4 > Received 'd' (1 bytes).8 < Sending 'djsdkg' ..5 > Received 'f' (1 bytes).6 > Received 'e' (1 bytes).9 < Sending 'dshsdgdud' ..7 > Received 'd' (1 bytes).8 > Received 'djsdkg' (6 bytes).10 < Sending 'sldhduhsd' ..9 > Received 'dshsdgdud' (9 bytes).10 > Received 'sldhduhsd' (9 bytes).11 < Sending 'sodhsdhd' ..11 > Received 'sodhsdhd' (8 bytes).12 < Sending 'a' ..13 < Sending 's' ..12 > Received 'a' (1 bytes).14 < Sending 'dd' ..15 < Sending 'e' ..13 > Received 's' (1 bytes).16 < Sending 'ef' ..17 < Sending 'f' ..14 > Received 'dd' (2 bytes).15 > Received 'e' (1 bytes).16 > Received 'ef' (2 bytes).18 < Closing device ..17 > Received 'f' (1 bytes).18 > Done.While the UI stays responsive at all times, the 0.75 second delay before the MCU responds is obvious.<are reported when the UI has submitted a command/request, and>are the responses from the MCU via the worker thread. To keep which response/success/error is related to which request/command, the SerialDevice class keeps an autoincrementing ident number, which is passed to the worker, and returned to the caller (if the request/command was submitted); and the responses have the corresponding ident.Note that the microcontroller and the worker thread are strictly half-duplex: there is only one request in flight at any time, and the response is received before the next request is submitted. The reason for the garbled order above is that the above order reflects real time order (top was earliest event, bottom latest event), and the Qt signals are queued (between the main thread and the helper thread).The four signals (pressing enter at the text boxes, or clicking either of the two buttons) are connected separately in the code, but it is quite possible to use "magic" method names in the Ui class to automagically connect named widget events to their handlers. For example, if you like to useon_widgetName_signalNameas the magic handler names, all you need to add in the Ui class constructor is something likeCode:for methodName in dir(self):if methodName.startswith('on_'):namePart = methodName.split('_')if len(namePart) < 3:continuesignalName = namePart[-1]del namePart, namePart[-1]widgetName = '_'.join(namePart)widget = self.ui.findChild(QtWidgets.QWidget, widgetName)if widget is None:sys.stderr.write("%s: There is no widget '%s'!" % (uifile, widgetName))continuetry:getattr(widget, signalName).connect(getattr(self, methodName))except:sys.stderr.write("%s: Cannot connect widget '%s' signal '%s' to method '%s'!" % (uifile, widgetName, signalName, methodName))continueNormally uiLoader does that, but because the ui is not our root object (where the methods are), I think this additional mapping is needed.

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 16, 2020, 06:10:23 pm
content: Quote from: Nominal Animal on November 16, 2020, 05:40:02 pmI lost track of the versions. I have been slowly polishing the utility over the last 6 1/2 years and periodically updatepyserial. For a while,pyserialcould flush a serial input buffer, but then it stopped working, at least under OSX.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 17, 2020, 07:22:54 pm
content: Yup. But consider this: What would be the optimum interface for your use case? (My point is, it varies a LOT.)For example, if the output from the serial device is a stream with reliable separators (the most typical case; with newline as the separator), then I would suggest that the best Python interface would be something like (as a static method)readResponse(device,timeout,maxbytes=4096)wheretimeoutis the number of seconds (or milliseconds) to getting a full response; and the result is a tuple, say(bytes,terminator,parity-errors), whereterminatoris either the end separator or empty sequence if the command timed out, andparity-errorsis either a boolean indicator whether this response had parity errors, or a list or tuple of indices to the bytes which had parity errors.I know that in Linux this can be implemented easily in POSIX C (select/poll for nonblocking data, and a per-process timer with a SIGEV_THREAD_ID signal for the timeout), and doesn't even need pthreads. On BSDs and Macs one can use a helper thread in interruptible sleep (nanosleep()); which delivers a signal changing a volatile atomic flag (volatile sig_atomic_t) or writes a single byte to a pipe also monitored by the serial reading thread. That also works in Linux, making it a portable approach, but like I said, in Linux-only there is even a simpler option. For Windows, I dunno, because I don't use it or write any code for it.)For a pure binary datastream, I'd prefer slightly different interface:readStream(device,timeout,minBytes,maxBytes)which blocks until timeout expires, or at leastminBytesis received. If at that time there are more thanminBytesreceived (which is common, because USB transfers blocks of data, and not individual bytes, and USB is the most common serial interface nowadays), the call returns up tomaxBytes.But note how non-ordinary this interface is: each call returns three different pieces of information, and even when it times out, the caller gets both the notification (of the call timing out), but also any data received thus far. I bet such an interface would seriously weird out typical Python programmers, and make them wonder.. asking for a simpler interface:"I just want to read the serial data, and none of that other stuff. Can't you make it simpler?"You see, most humans are utterly stupid. And most programmers are just human. (Me, I'm an utter uncle bumblefuck, but at least I'm aware of my deficiencies.)

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 17, 2020, 08:12:47 pm
content: Fortunately, the digital multimeter my utility is reading, outputs CRLF terminated data blocks thatpyserialcan iterate over. Beyond that, I try to ask as little functionality frompyserialas possible, starting with the very first calls (here, in simplified form):Code:import serial # Download from http://pyserial.sourceforge.net/def main(argv):...# Flush any stale input.# BUG: pyserial 2.7 will not flush the receive buffer of the v2.2.18 FTDI# Chip USB serial driver, meaning its flushInput() doesn't. Try to# flush the port by opening it with a 100 ms timeout, reading and# discarding any input, and closing the port. A read with a much# shorter timeout or a non-blocking read may cause pyserial 2.7/3.1# to either lose qued characters or defer reading them. :(# Then (re)open the port with no timeout and repeatedly read# measurement line-blocks from the DMM....#def open_port(timeout=None):return serial.Serial(path, baudrate=2400, bytesize=serial.SEVENBITS,parity=serial.PARITY_ODD,stopbits=serial.STOPBITS_ONE,timeout=timeout)try:with open_port(timeout=0.1) as dmm:stale_data = dmm.read(32768//2) # BUG: Using a large int or None# fails or reads 1 byte.with open_port() as dmm:for block in dmm:...except EnvironmentError as exc:abort(exc)...if __name__ == '__main__':main(sys.argv)

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 17, 2020, 11:02:47 pm
content: Well, how about using the following? It should be portable across Linux, BSDs, and Macs:Code:# -*- coding: utf-8 -*-# This is in Public Domainimport sysimport osimport termiosimport timeimport selectimport errnoclass SerialMM:"""Serial multimetermm = SerialMM(device_path, default_sendtimeout, default_receivetimeout)Open the serial device at 2400 baud, odd parity, 7 bits per byte.Parity checking is enabled, and receive() will set the most significantbit for any received bytes with parity errors."""def __init__(self, devpath, sendtimeout=1.0, receivetimeout=1.0):self.fd = -1self.tty = Noneif isinstance(sendtimeout, (int, float)) and sendtimeout >= 0:self.sendtimeout = sendtimeoutelse:self.sendtimeout = -1if isinstance(receivetimeout, (int, float)) and receivetimeout >= 0:self.receivetimeout = receivetimeoutelse:self.receivetimeout = -1fd = os.open(devpath, os.O_RDWR | os.O_NONBLOCK | os.O_NOCTTY | os.O_CLOEXEC)try:oldtty = termios.tcgetattr(fd)newtty = termios.tcgetattr(fd)except Exception:os.close(fd)raise# c_iflags: ignore BREAK, enable input parity checking, no CR/LF translationnewtty &= ~(termios.ISTRIP | termios.IGNPAR | termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IUCLC)newtty |= termios.IGNBRK | termios.PARMRK | termios.INPCK# c_oflag: no post processing, no CR/LF translationnewtty &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL | termios.ONOCR | termios.ONLRET)# c_cflga: 2400o7: clear baud rate mask, odd parity, 2400 baud, 7 bits per characternewtty &= ~(termios.CBAUD | termios.CSIZE | termios.CSTOPB | termios.PARENB | termios.PARODD)newtty |= termios.B2400 | termios.CS7 | termios.CREAD | termios.HUPCL# c_lflag: raw mode, no signals, disable echonewtty &= ~(termios.ISIG | termios.ICANON | termios.ECHO | termios.IEXTEN)# 2400 baud ratenewtty = termios.B2400newtty = termios.B2400# Never blocknewtty[termios.VTIME] = 0newtty[termios.VMIN] = 0try:termios.tcflush(fd, termios.TCIOFLUSH)termios.tcsetattr(fd, termios.TCSANOW, newtty)except Exception:os.close(fd)raise# Device opened successfully.self.fd = fdself.tty = oldttyself.path = devpathself.buffer = bytes()def __del__(self):"""Close serial device, reverting to original termios settings."""fd = self.fdtty = self.ttyself.fd = -1self.tty = Noneif fd != -1:if tty is not None:try:termios.tcflush(fd, termios.TCIOFLUSH)except:passtry:termios.tcsetattr(fd, termios.TCSANOW, tty)except:passtry:os.close(fd)except:passdef send(self, data, timeout=None):"""Send data (bytes) to serial device, spending at most timeout.Returns the number of bytes sent.Zero timeout is allowed (nonblocking operation),negative timeouts blocks until all data is sent."""if isinstance(timeout, (int, float)):usetimeout = timeoutelse:usetimeout = self.sendtimeoutif self.fd == -1:# Device not open.return 0data_len = len(data)data_sent = 0started = time.monotonic()while data_sent < data_len:# We have nonblocking I/Otry:n = os.write(self.fd, data[data_sent:])except InterruptedError:# Interrupted by signal delivery; ignorecontinueexcept OSError as err:# In some cases, n==0, and in other cases we have EAGAIN or EWOULDBLOCK error.if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK:n = 0else:raiseif n > 0:data_sent += ncontinue# We need to wait until we can write again.if usetimeout > 0:maxwait = started + usetimeout - time.monotonic()if maxwait <= 0.0:# Timeout. Return the number of bytes already sent.return data_sentelif usetimeout < 0:maxwait = 1.0else:# Nonblocking operationreturn data_sentreadfds, writefds, exceptfds = select.select((), (self.fd,), (), maxwait)# Because we do nonblocking writes, we don't actually care about the result.return data_sentdef receive(self, timeout=None):"""Receive newline-delimited sata from serial device, spending at mosttimeout seconds for a complete response. CR and LF characters are omitted,so empty lines will never be received.Returns an empty bytes object if the timeout is exceeded before a full response is received."""if isinstance(timeout, (int, float)):usetimeout = timeoutelse:usetimeout = self.receivetimeoutstarted = time.monotonic()while True:# Check if buffer contains a complete response yet.cr_index = self.buffer.find(b'\r')lf_index = self.buffer.find(b'\n')if cr_index > 0 or lf_index > 0:index = min(cr_index, lf_index)chunk = self.buffer[0:index]self.buffer = (self.buffer[index:]).lstrip(b'\r\n')return chunkelif cr_index > 0:chunk = self.buffer[0:cr_index]self.buffer = (self.buffer[cr_index:]).lstrip(b'\r\n')return chunkelif lf_index > 0:chunk = self.buffer[0:lf_index]self.buffer = (self.buffer[lf_index:]).lstrip(b'\r\n')return chunk# Nonblocking?if usetimeout == 0:return bytes()# Wait for more data to become available.while True:if self.fd == -1:# Connection to device lost. Could raise an Exception instead.return bytes()if usetimeout > 0:maxwait = started + usetimeout - time.monotonic()if maxwait <= 0:# Timeout exceeded. We could raise an exception instead.return bytes()else:maxwait = 1.0try:readfds, writefds, exceptfds = select.select((self.fd,), (), (), maxwait)except InterruptedError:# Interrupted by signal delivery; ignorecontinueif self.fd in readfds:break# Receive more data.try:more = os.read(self.fd, 4096) # TTY buffer size = 4095except InterruptedError:# Interrupted by signal delivery; ignorecontinue# Parity checks. b'\377\0z' is byte z with a parity error,# and b'\377\377' is a non-error 8-bit byte value 255.if more is not None and len(more) > 0:self.buffer = (self.buffer + more).lstrip(b'\r\n')i = self.buffer.find(b'\377')while i >= 0 and i <= len(self.buffer) - 3:if self.buffer[i+1] == 0:# Parity error. Mark by setting highest bit.self.buffer = self.buffer[:i] + bytes((self.buffer[i+2] | 128,)) + self.buffer[i+3:]i = self.buffer.find(b'\377', i+3)elif self.buffer[i+1] == 255:# Not a parity error, just 8-bit value 255.self.buffer = self.buffer[:i] + bytes((255,)) + self.buffer[i+2:]i = self.buffer.find(b'\377', i+2)else:# This should never occur. We just keep the 255 byte.i = self.buffer.find(b'\377', i+1)if __name__ == '__main__':if len(sys.argv) < 2 or sys.argv in ('-h', '--help'):if len(sys.argv) > 0:this = sys.argvelse:this = '(this)'sys.stderr.write("\n")sys.stderr.write("Usage: %s [ -h | --help ]\n" % this)sys.stderr.write(" %s SERIAL-DEVICE\n" % this)sys.stderr.write("\n")sys.stderr.write("This program will echo everything received from serial device,\n");sys.stderr.write("using 2400 baud, odd parity, 7 bits per byte. Parity errors\n")sys.stderr.write("are marked by setting the high bit.\n")sys.stderr.write("\n")sys.stderr.write("Press Ctrl+C to stop the output.\n")sys.stderr.write("\n")sys.exit(0)mm = SerialMM(sys.argv)# To send something to the multineter, use mm.send().while True:try:line = mm.receive()except KeyboardInterrupt:breakif len(line) < 1:try:sys.stderr.write("(No output)\n")sys.stderr.flush()except KeyboardInterrupt:breakcontinuetry:sys.stdout.write('"%s" (%d bytes)\n' % (line.decode(encoding='utf-8', errors='ignore'), len(line)))sys.stdout.flush()except KeyboardInterrupt:breakdel mmSince the data is 7-bit, I marked parity errors by setting the most significant bit.send()takes a bytes object, andreceive()returns a bytes object (but with CR and LF omitted).receive()never returns an empty line, because it consumes newlines; it does return an empty bytes object if the timeout is exceeded.(I don't particularly like that way of noting the parity errors, but it can be changed if you know how you'd like to deal with parity errors – ignore that line? Or something else? – but this was the simplest option, only 12 lines of Python code.)As usual, the normal timeout modes are supported: positive for a timeout, zero for nonblocking operation, and negative for infinite timeout.I wrote it just now, so it probably contains bugs, and I only tested it with the Arduino sketch earlier (adding somemm.send(b'Foofah!\n')to the above code, so it'll respond). But the idea behind the code is sound.When the program opens the serial port, any already received data (and buffered but not sent data) at the tty level is discarded. The initial line may therefore be partial one, simply because the multimeter might be in the middle of sending a line when the program opens the port.There is a way to work around that, however: instead of flushing the input, we could do a nonblocking read of 4096 bytes (termios buffers are only 4095 bytes long), and discard everything up to and including the final newline (CR, LF, CRLF, or LFCR), and if anything is left, putting that part inself.buffer. Want me to add that into this public domain example code? (It would then make sense to refactor the buffering mechanism, and make it more robust by doing the parity marking on the chunk returned byreceive().)Again, this is just a first write, so don't think this is "finished" or quality code in any way. It is just the first version, and requires some testing and especially using (in an actual application) to see what works, and what needs changing, which usually results in big changes in such "library" code. But it isn't a big deal at all, comfy slippers stuff for me... Happy to help, if this helps!

author: rx8pilot
author_rank: Super Contributor
timestamp: November 18, 2020, 12:43:35 am
content: Quote from: janoc on November 15, 2020, 01:55:58 pmStill trying to understand the setters/getters.......Dimming the lights, taking a deep breath and waiting for the 'click' in my brain.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 18, 2020, 03:08:13 am
content: Let's define a simple class for stand-ins for the Qt objects:Code:class Stuff:def __init__(self, **kwargs):for name, value in kwargs.items():setattr(self, name, value)Basically,foo = Stuff(bar=1, baz="two")defines an objectfoowhich has attributesfoo.bar(== 1) andfoo.baz(== "two").If we havefoo, and the name of an attribute as a string, sayaname, we can get and set the attribute usingCode:value = getattr(foo, aname)setattr(foo, aname, value)If we havefoo, and a member function name in sayfname, we can call that member function usingCode:(getattr(foo, fname))(parameters, to, the, function)or, equivalently,Code:memberfunc = getattr(foo, fname)memberfunc(parameters, to, the, function)Let's say you create some widget in Qt, and name them. If you define them in an .ui file, just make sure the widgets have a name (a<property name="text"><string>nameGoesHere</string></property>block). If you define them in Python, callwidget.setObjectName("nameGoesHere"). If your root widget is sayself.ui, you can find any named widget usingCode:widget = self.ui.findChild(QtWidgets.QWidget, "nameGoesHere")With this, you can implement the function in the first message in this thread:Code:def prgsUpdate(self, barNumber, minVal, maxVal, remainVal):''' Update progress bar specified by barNumber 0-6 '''widget = self.ui.findChild(QtWidgets.QWidget, "cycleRemain" + barNumber)widget.setMinimum(minVal)widget.setMaximum(maxVal)widget.setProperty("value", remainVal)Let's consider something more interesting: lets assume we have a dictionary with Qt widget names, where the value is a dictionary of method names with values being method parameters. This means thatprgsUpdate(self, 0, 100, 200, 150)andprgsUpdate(self, 1, 25, 75, 50)are described usingCode:settings = { 'cycleRemain0': { 'minimum': 100, 'maximum': 200, '=value': 150 },'cycleRemain1': { 'minimum': 25, 'maximum': 75, '=value': 50 } }Note that properties that start with a '=' name QObject properties. The function that applies such settings can then be written asCode:def setAll(self, settings):for widgetName, widgetSettings in settings.items():widget = self.ui.findChild(QtWidgets.QWidget, widgetName)for itemName, value in widgetSettings.items():if itemName == '=':widget.setProperty(itemName[1:], value)else:(getattr(widget, "set" + itemName.upper() + itemName[1:]))(value)This (assuming the above has no typos) works for all Qt widgets and their properties.The inverse, or updating such a dictionary structure with the current values, is something likeCode:def updateAll(self, settings):for widgetName, widgetSettings in settings.items():widget = self.ui.findChild(QtWidgets.QWidget, widgetName)for itemName, value in widgetSettings.items():if itemName == '=':newValue = widget.getProperty(itemName[1:])else:newValue = (getattr(widget, itemName))()widgetSettings = newValuereturn settingsThis is suitable when you know exactly what you wish to save from an user interface. When you don't, you instead need to traverse through the widget structure, and determine the methods to use from the widget type. This is not hard, but because of the large number of widgets, involves a lot of testing.So, the thing I mentioned earlier, allowing an user to switch UIs, means we'll want to store just one thing per widget, ending up with a plain unordered dictionary. For numerical values, you'll want to store the normalized or logical value, not a physical value (say, pixel counts or pixel coordinates). Such dictionaries can be trivially stored to .INI -like files using the built-inconfigparsermodule; meaning, you can even let the user "snapshot" their user interface, including all text content et cetera, saving to simple ini files.(If your application has multiple windows, or configurable user interface, saving the widget geometries (instead of their "contents") in a similar way allows the user to "snapshot" and restore their UI to different work flows.)

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 18, 2020, 03:31:05 am
content: When creating widgets in Python, you end up calling many of its property functions to set them. This can be shortened with e.g.Code:def applyWidget(self, widgetName, **kwargs):widget = self.ui.findChild(QtWidgets.QWidget, widgetName)for name, value in kwargs.items():method = getattr(widget, name)if isinstance(value, tuple):method(*value)elif isinstance(value, dict):method(**value)else:method(value)For example,Code:label = QtWidgets.QLabel("Some text label:")label.setObjectName("someLabel")label.setWordWrap(False)label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTop)label.setProperty("myOwn", 5)simplifies toCode:label = QtWidgets.QLabel("Some text label:")applyWidget(label, setObjectName="someLabel", setWordWrap=False, setAlignment=QtCore.Qt.AlignRight|QtCore.Qt.AlignTop, setProperty=('myOwn':5))If you have many of them that share calls, save them in a dictionary:Code:labelCommon = { 'setWordWrap': False, 'setAlignment': QtCore.Qt.AlignRight|QtCore.Qt.AlignTop }and then insert the dictionary as named parameters:Code:label = QtWidgets.QLabel("Some text label:")applyWidget(label, setObjectName="someLabel", **labelCommon)other = QtWidgets.QLabel("Some other label:")applyWidget(label, setObjectName="otherLabel", **labelCommon)

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 18, 2020, 09:13:36 am
content: Quote from: Nominal Animal on November 17, 2020, 11:02:47 pmWell, thank you very much for that, although you really did not need to. That is a lot of typing! I have been using Python fairly intensely since 2007. Being the slow creature that I am, my code was at first pretty much C and friends, but became less and less so until I had fully unlearned them after roughly seven years. I can guess where you might be on that journey.Just a couple quick suggestions – to use or ignore – that I feel obliged to offer in appreciation of your efforts.I do not think I have ever trapped failed writes tosys.stderr. If it has stopped working, I expect a kernel panic will shortly follow.Rather thanCode:if isinstance(sendtimeout, (int, float)) and sendtimeout >= 0:self.sendtimeout = sendtimeoutelse:self.sendtimeout = -1I might writeCode:try:self.sendtimeout = sendtimeout if (sendtimeout + 0 >= 0) else -1except TypeError:self.sendtimeout = -1with the "+ 0" if the code will run under Python 2. Whileisinstance()can sometimes make life much simpler, rewriting code without it is one of those useful exercises I always try.EDIT: Or on further thought, might it be better to raise a ValueError ifsendtimeoutis neither None nor a number?EDITx2: Fish think, but too slowly. On third thought, since the function is for developers and I gatherself.sendtimeoutneed only be numeric, I would write:Code:self.sendtimeout = -1 if (sendtimeout is None) else sendtimeout + 0and let the addition and exception detect and identify the type error, before the error becomes costly.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 18, 2020, 04:11:12 pm
content: Quote from: jfiresto on November 18, 2020, 09:13:36 amOh no, there is no failure in sys.stderr itself; they are to just to trap the KeyboardInterrupt exception that occurs when the user presses Ctrl+C.If you run the code, you'll see that pressing Ctrl+C does not cause the program to abort, it exits the main loop cleanly.Quote from: jfiresto on November 18, 2020, 09:13:36 amSure, why not? That works perfectly well too.The reason I happen to prefer isinstance checks is that I like to subclass tuples (since they're immutable; otherwise I'd use plain objects) for various message types (used in e.g. queue.Queue). For example:Code:class Message(tuple):"""Abstract message parent class"""def __new__(cls):return tuple.__new__(cls, ())class SuccessMessage(Message):def __new__(cls, status):return tuple.__new__(cls, (status,))@propertydef status(self):return selfclass FailureMessage(Message):def __new__(cls, cause):return tuple.__new__(cls, (cause,))@propertydef cause(self):return selfNow, if you had a perfectly flat type hierarchy, you could just use a class or instance method returning a type identifier. However,isinstance(msg, Message)is True for both SuccessMessage and FailureMessage instances. This means that code maintenance (adding new message types) is robust, because you only need to subclass the relevant class, and add specific handling at the point – no need to worry about adding the message type to various checks in between. Simples.Quote from: jfiresto on November 18, 2020, 09:13:36 amNow that I think of it, I agree: raising a ValueError makes most sense.

author: jfiresto
author_rank: Frequent Contributor
timestamp: November 18, 2020, 04:52:17 pm
content: Quote from: Nominal Animal on November 18, 2020, 04:11:12 pmYou are right, I scanned past the KeyboardInterrupt arguments, probably because I was not expecting them that late. I catch them more globally:Code:if __name__ == '__main__':try:main(sys.argv)except KeyboardInterrupt:debug(1, lambda: ['Exiting on KeyboardInterrupt (SIGINT)'])It is harder to mess that one up, and of course it is less to type.QuoteDistinguishing different type messages by instantiating them from different subclasses is cool. Beyond that, I like to duck type: it means less for me to remember and compose.QuoteOr provoking a illuminating TypeError with three more characters. (It was my 2nd edit while you were composing your message.)

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 18, 2020, 06:01:21 pm
content: Quote from: jfiresto on November 18, 2020, 04:52:17 pmMakes sense. (If anything in main() needs cleanup, it can catch KeyboardInterrupt and just re-raise it viaraise, without any parameters.)Quote from: jfiresto on November 18, 2020, 04:52:17 pmIt is relevant to this thread, too, because when one gets a widget, one can useisinstance()to check which kind of widget it is, and use the Qt type hierarchy to avoid having to check for all types, just those that matter.For example, when saving widget contents, layout widgets don't matter (because they themselves don't have any user-editable content, it is in child widgets). So, one can checkisinstance(widget, QtWidgets.QLayout)and it will be true for QLayouts, QBoxLayouts, QHBoxLayouts, QVBoxLayouts, QStackedLayouts, QGridLayouts, and QFormLayouts, and any instances of their subclasses too.An another example: QAbstractButton is the parent type for all sorts of buttons, but of these, really only QCheckBox and QRadioButton have state that makes sense to record. The others, QPushButton, QToolButton, and QCommandLinkButton, are used for immediate interaction, so they don't normally stay "pressed" like checkboxes and radiobuttons do.Quote from: jfiresto on November 18, 2020, 04:52:17 pmYuh,raise TypeError("timeout must be a real number or None, not " + type(timeout))or something along those lines. (That's the exception math.sqrt(None) raises.)I'm not actually spending much time with these, just hoping to provide enough information to help others in the right direction. For me, writing the above example code snippets was about as much effort as playing a couple of rounds of Sudoku or Solitaire; and while I try to make sure they're helpful, I don't polish them like I would if I were to e.g. recommend them as a pattern; I personally always need to sleep and review the code I've written with a new pair of eyes, before I can trust it at all.(Sometimes my mind gets stuck on one particular flow, and cannot see the alternatives. Having a good nights sleep, letting my subconscious sort it out, always helps.)

author: rx8pilot
author_rank: Super Contributor
timestamp: November 19, 2020, 01:31:27 am
content: Quote from: Nominal Animal on November 16, 2020, 05:40:02 pmI am VERY grateful for the details on this topic!!!!! The various resources I have been scouring tell me 'what' to do and not much 'why' to do it. All of the effort you put into this thread speaks to building an intuition of how the problems are solved instead of just just a simple copy-paste code snippet that would be over my head.So much to chew on, thank you!

author: rx8pilot
author_rank: Super Contributor
timestamp: November 19, 2020, 04:38:50 am
content: I am getting stuck here:Code:def prgsUpdate(self, barNumber, minVal, maxVal, remainVal):''' Update progress bar specified by barNumber 0-6 '''widget = self.ui.findChild("cycleRemain" + barNumber)widget.setMinimum(minVal)widget.setMaximum(maxVal)widget.setProperty("value", remainVal)findChild() is not an attribute of the object 'widget'.I tried a few variants but have not been able to figure it out. It looks like it take 2 arguments - which I tried but since findChild is not found at all, it doesn't really matter.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 19, 2020, 04:57:43 am
content: Sorry, my fault. The first parameter is the desired widget type, which for all GUI widgets isQtWidgets.QWidget, or for all possible Qt objectsQtCore.QObject.I fixed my post, but here it is again:Code:def prgsUpdate(self, barNumber, minVal, maxVal, remainVal):''' Update progress bar specified by barNumber 0-6 '''widget = self.ui.findChild(QtWidgets.QWidget, "cycleRemain" + barNumber)widget.setMinimum(minVal)widget.setMaximum(maxVal)widget.setProperty("value", remainVal)

author: rx8pilot
author_rank: Super Contributor
timestamp: November 19, 2020, 05:11:05 am
content: Ok, I had tried that but the error remains.....Code:from PyQt5 import QtWidgets as qtw.........Ui_shopDash, baseClass = uic.loadUiType('ui\cncDash_A-v2.ui')..........prgsBar = self.ui.findChild(qtw.QWidget, "cycleRemain" + barNumber).........error:Code:prgsBar = self.ui.findChild(qtw.QWidget, "cycleRemain" + str(barNumber))AttributeError: 'Ui_shopDash' object has no attribute 'findChild'

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 19, 2020, 05:54:19 am
content: Quote from: rx8pilot on November 19, 2020, 05:11:05 amBecause you are usingloadUiType(), you need to usebaseClass.findChild(qtw.QWidget, "cycleRemain" + barNumber).I am usingself.ui = uic.loadUi('ui-file-path'), which returns an instance (actually, the widget hierarchy itself). Because the self.ui instance is necessarily some subclass of QtCore.QObject, it does have the .findChild() method.

author: Nominal Animal
author_rank: Super Contributor
timestamp: November 19, 2020, 06:15:00 am
content: Consider the following Python2/Python3 example program,example.py:Code:#!/usr/bin/env python3# -*- coding: utf-8 -*-import systry:from PyQt5 import QtCore, QtGui, QtWidgetsfrom PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slotfrom PyQt5.uic import loadUiexcept:try:from PySide2 import QtCore, QtGui, QtWidgetsfrom PySide2.QtCore import Signal, Slotfrom uic import loadUiexcept:raise ModuleNotFoundError("No Qt5 support.")def _iterate_all_qobjects(obj):yield objfor subobj in obj.children():for childobj in _iterate_all_qobjects(subobj):yield childobjclass Ui(QtCore.QObject):"""Graphical User interface class"""def __init__(self, uifile, parent=None):QtCore.QObject.__init__(self, parent)self.ui = loadUi(uifile)def show(self):self.ui.show()def allTextWidgets(self):for widget in _iterate_all_qobjects(self.ui):if len(widget.objectName()) < 1:continueif isinstance(widget, QtWidgets.QLineEdit):yield (widget.objectName(), widget.text())elif isinstance(widget, QtWidgets.QPlainTextEdit):yield (widget.objectName(), widget.toPlainText())elif isinstance(widget, QtWidgets.QTextEdit):yield (widget.objectName(), widget.toPlainText())if __name__ == '__main__':if len(sys.argv) < 2 or '-h' in sys.argv[1:] or '--help' in sys.argv[1:]:if len(sys.argv) > 0 and len(sys.argv) > 0:this = sys.argvelse:this = '(this)'sys.stderr.write('\n')sys.stderr.write('Usage: %s [ -h | --help ]\n' % this)sys.stderr.write(' %s UI-FILE [ textwidgetname=contents ]\n' % this)sys.stderr.write('\n')sys.stderr.write('When you close the window, the contents of all named text widgets are shown.\n')sys.stderr.write('\n')sys.exit(0)QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts)app = QtWidgets.QApplication(sys.argv)win = Ui(sys.argv)win.show()# Set text widget contentsif len(sys.argv) > 2:for arg in sys.argv[2:]:if '=' in arg:name, value = arg.split('=', 2)widget = win.ui.findChild(QtCore.QObject, name)if widget is None:sys.stderr.write("%s: No such widget in %s." % (name, sys.argv))elif isinstance(widget, (QtWidgets.QLineEdit, QtWidgets.QLabel)):widget.setText(value)elif isinstance(widget, (QtWidgets.QPlainTextEdit, QtWidgets.QTextEdit)):widget.setDocument(QtGui.QTextDocument(value))else:sys.stderr.write("%s: Widget is %s, not a text widget.\n" % (name, type(widget)))status = app.exec_()for pair in win.allTextWidgets():print('%s: "%s"' % pair)del win, appsys.exit(status)If you use PySide2, put theuic.pyI showed earlier in this thread into the same directory.When you run it, give it the path to an .ui file, and optionally one or morename=textpairs.Here is again the simplemain.uiI used for testing:Code:<?xml version="1.0" encoding="UTF-8"?><ui version="4.0"><class>Ui</class><widget class="QWidget" name="Ui"><property name="geometry"><rect><x>0</x><y>0</y><width>368</width><height>172</height></rect></property><property name="windowTitle"><string>Serial Example</string></property><layout class="QGridLayout" name="layout" rowstretch="0,0,1" columnstretch="0,1,0,0"><item row="0" column="0" alignment="Qt::AlignRight"><widget class="QLabel" name="deviceLabel"><property name="text"><string>Device:</string></property></widget></item><item row="2" column="0" alignment="Qt::AlignRight|Qt::AlignTop"><widget class="QLabel" name="responseLabel"><property name="text"><string>Response:</string></property></widget></item><item row="0" column="1"><widget class="QLineEdit" name="deviceEdit"><property name="text"><string>/dev/ttyACM0</string></property></widget></item><item row="1" column="0" alignment="Qt::AlignRight"><widget class="QLabel" name="commandLabel"><property name="text"><string>Command:</string></property></widget></item><item row="0" column="2"><widget class="QPushButton" name="connectButton"><property name="text"><string>Connect</string></property></widget></item><item row="0" column="3"><widget class="QPushButton" name="disconnectButton"><property name="text"><string>Disconnect</string></property></widget></item><item row="1" column="1" colspan="3"><widget class="QLineEdit" name="commandEdit"/></item><item row="2" column="1" colspan="3"><widget class="QTextEdit" name="responseText"><property name="lineWrapMode"><enum>QTextEdit::NoWrap</enum></property><property name="readOnly"><bool>true</bool></property></widget></item></layout></widget><resources/><connections/></ui>Run it using e.g.python example.py main.ui. Note the initial contents of the fields.Inexample.py, function_iterate_all_qobjects(obj)is a generator function which yields each object in the qobject tree recursively.When you close the window, theallTextWidgets()generator method of the Ui class iterates over all widgets in the Ui hierarchy, skipping widgets without names, and generates tuples of (text widget name, editable text widget contents). In the main, these are simply printed to standard output.When the command line also containsname=textpairs, the interesting magic happens. For example, try runningpython example.py main.ui deviceEdit=/dev/null commandEdit=Nothing responseText=HeyThis is done in the snippet beginning with comment# Set text widget contents.The first four lines split the command-line parameter to a name=value pair.The fifth line finds the QObject in the UI hierarchy having that name.QLineEdit and QLabel widgets provide a setText() method that we use to set the contents.QPlainTextEdit and QTextEdit widgets provide a setDocument() method that we use, first constructing a suitable QtGui.QTextDocument for it (its constructor takes the plain text contents as a string).So, as you can see, withuic.loadUi()this works just fine. Note that PySide2 nor the uic.py shim that I've shown in this thread, *does not* provideloadUiType(), onlyloadUi().

author: rx8pilot
author_rank: Super Contributor
timestamp: November 20, 2020, 11:49:59 pm
content: I got my code to work as I hoped in the original post! Very exciting.My goal with this thread (and all my other threads) is not to have a solution handed to me, but rather gain the understanding necessary for me to build the intuition necessary to solve this problem and the ones in the future.This discussion gave me a lot to dig into and the end result was something that hopefully others can benefit from. Thanks Nominal Animal for the time it took to post your knowledge and the examples. It kept me busy for the past couple of days. Still have a long journey toward expert level Python but this is a much needed confidence booster.Awesome.