My main use case is [...] small PC programs for microcontroller boards tethered to a Linux PC (most often by a serial port, sometimes by LAN or USB + some GUI to print/plot on the PC whatever the MCU board is doing). Maybe controlling a few SCPI instruments, too (over LXI). Small programs like these, hobby level, at home. Typically a small GUI to plot something, plus a few buttons. Plotting data is a must.
I've done plenty of such, and in my experience, the language matters less than using a native
termios library for serial ports instead of some random serial library (
libserial etc. are all utter crap) and using a separate thread or subprocess for large bulk data transfers, communicating with the GUI thread or process via suitable messages and UI events.
With Python, I do recommend using Qt5 or Qt6 (using
PySide2/
PySide6 or
PyQt5/
PyQt6), and writing any bulk data management in C, compiled to and loaded as a dynamic library via Python built-in
ctypes module. I do prefer specifying Qt user interfaces using Qt Widgets (not Qt Quick nor QML) in Qt Widgets Designer (
pyside6-designer)
.ui format, and instantiating it at run time using QtUiTools module (
Qt 5/
Qt 6). I do use QtSVG, but not QtSerialPort (I use
termios instead), nor do I use any of the Qt Quick modules.
QtCharts (Qt 5) and
QtGraphs (Qt 6) should make it easy to display any results as graphs (as long as you remember to use the Widget interface and ignore the QML stuff), although for realtime/continuous stuff I'd use OpenGL (
Qt 5/
Qt 6) to offload the rendering to the GPU (even on Linux ARM SBCs with OpenGL ES support).
I haven't used Ruby with GUI toolkits at all, so don't feel comfortable recommending one over the other. All I know is that Python + Qt works extremely well on Linux hosts, even on SBCs with OpenGL ES, for my very similar needs. (I also prefer this way, because then the native code loaded dynamically to that same process can use a proprietary license, and Qt used via LGPL, making low-cost commercial development possible while still allowing end users to meddle (safely) with the UI. It is extremely common for many end user complaints to be handled by suitable small changes to the UI; the hard part is actually finding out the underlying issue, instead of what the end user
believes the issue to be.)
The exact language used for the UI does not really matter that much, because almost all of the UI is done by the widget toolkit (Qt, FLTK, GTK, WxWindows, etc.), and the handlers/scripted code comprises only a tiny fraction of the total CPU time taken by the application. It is the application
design that is crucial.
The best example of this and its importance I've mentioned before, is when a button press changes some state –– say, a relay, or maybe just a LED light –– on the microcontroller.
Typical implementations of the host-side applications decouples the UI button from the actual state on the microcontroller: the button is just a graphical element that triggers a command to be sent to the microcontroller, and that's all. I consider this a fatal design error: the UI should always
reflect the state, or it is not as useful to the human users as it could be.
Consider, instead, a design where the user pressing the UI button causes the UI button to become depressed, and a corresponding event sent to the microcontroller. The microcontroller does whatever it does, and responds with the related new state. When the UI receives the new state message, it updates the UI button visual state also. This way the user interface reflects the microcontroller state at all times, even between UI button press and UI button release! This is
responsive design.
I personally prefer to do this in Python by having two worker threads. One thread is responsible for receiving UI events from the UI thread and sending them as commands to the microcontroller. The other is responsible for receiving microcontroller responses, and sending them as UI events for the UI thread. I prefer to do asynchronous command-responses, with each command and response having a monotonically increasing ID (number), and a shared set of pending commands.
If you use Python + Qt + Python or Qt threads, then you can create your own derivative QObject class and use native Qt signals safely across threads for passing events and data between threads (including a worker thread and the UI thread). Just remember that the messages are asynchronous: not "immediately" acted upon, but some time in the near future, usually by another thread, possibly even by another CPU core.