Skip to content

Commit

Permalink
Merge pull request #1510 from benoit-pierre/improve_serial_port_handling
Browse files Browse the repository at this point in the history
Improve serial port handling
  • Loading branch information
benoit-pierre authored May 15, 2022
2 parents 6e29929 + 0e12688 commit 900c04b
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 4 deletions.
1 change: 1 addition & 0 deletions news.d/feature/1510.linux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use `/dev/serial/by-id/xxxx` links for each available serial port in the machine configuration dialog.
1 change: 1 addition & 0 deletions news.d/feature/1510.ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Show detailed information about each available serial port in the machine configuration dialog.
108 changes: 104 additions & 4 deletions plover/gui_qt/machine_options.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,123 @@
from copy import copy

from PyQt5.QtCore import QVariant, pyqtSignal
from PyQt5.QtWidgets import QGroupBox
from pathlib import Path

from PyQt5.QtCore import Qt, QVariant, pyqtSignal
from PyQt5.QtGui import (
QTextCharFormat,
QTextFrameFormat,
QTextListFormat,
QTextCursor,
QTextDocument,
)
from PyQt5.QtWidgets import (
QGroupBox,
QStyledItemDelegate,
QStyle,
QToolTip,
)

from serial import Serial
from serial.tools.list_ports import comports

from plover import _
from plover.oslayer.serial import patch_ports_info

from plover.gui_qt.config_keyboard_widget_ui import Ui_KeyboardWidget
from plover.gui_qt.config_serial_widget_ui import Ui_SerialWidget


def serial_port_details(port_info):
parts = []
global_ignore = {None, 'n/a', Path(port_info.device).name}
local_ignore = set(global_ignore)
for attr, fmt in (
('product', _('product: {value}')),
('manufacturer', _('manufacturer: {value}')),
('serial_number', _('serial number: {value}')),
):
value = getattr(port_info, attr)
if value not in global_ignore:
parts.append(fmt.format(value=value))
local_ignore.add(value)
description = getattr(port_info, 'description')
if description not in local_ignore:
parts.insert(0, _('description: {value}').format(value=description))
if not parts:
return None
return parts


class SerialOption(QGroupBox, Ui_SerialWidget):

class PortDelegate(QStyledItemDelegate):

def __init__(self):
super().__init__()
self._doc = QTextDocument()
doc_margin = self._doc.documentMargin()
self._doc.setIndentWidth(doc_margin * 3)
background = QToolTip.palette().toolTipBase()
foreground = QToolTip.palette().toolTipText()
self._device_format = QTextCharFormat()
self._details_char_format = QTextCharFormat()
self._details_char_format.setFont(QToolTip.font())
self._details_char_format.setBackground(background)
self._details_char_format.setForeground(foreground)
self._details_frame_format = QTextFrameFormat()
self._details_frame_format.setBackground(background)
self._details_frame_format.setForeground(foreground)
self._details_frame_format.setTopMargin(doc_margin)
self._details_frame_format.setBottomMargin(-3 * doc_margin)
self._details_frame_format.setBorderStyle(QTextFrameFormat.BorderStyle_Solid)
self._details_frame_format.setBorder(doc_margin / 2)
self._details_frame_format.setPadding(doc_margin)
self._details_list_format = QTextListFormat()
self._details_list_format.setStyle(QTextListFormat.ListSquare)

def _format_port(self, index):
self._doc.clear()
cursor = QTextCursor(self._doc)
cursor.setCharFormat(self._device_format)
port_info = index.data(Qt.UserRole)
if port_info is None:
cursor.insertText(index.data(Qt.DisplayRole))
return
cursor.insertText(port_info.device)
details = serial_port_details(port_info)
if not details:
return
cursor.insertFrame(self._details_frame_format)
details_list = cursor.createList(self._details_list_format)
for n, part in enumerate(details):
if n:
cursor.insertBlock()
cursor.insertText(part, self._details_char_format)

def paint(self, painter, option, index):
painter.save()
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
text_color = option.palette.highlightedText()
else:
text_color = option.palette.text()
self._device_format.setForeground(text_color)
doc_margin = self._doc.documentMargin()
self._details_frame_format.setWidth(option.rect.width() - doc_margin * 2)
self._format_port(index)
painter.translate(option.rect.topLeft())
self._doc.drawContents(painter)
painter.restore()

def sizeHint(self, option, index):
self._format_port(index)
return self._doc.size().toSize()

valueChanged = pyqtSignal(QVariant)

def __init__(self):
super().__init__()
self.setupUi(self)
self.port.setItemDelegate(self.PortDelegate())
self._value = {}

def setValue(self, value):
Expand Down Expand Up @@ -61,7 +160,8 @@ def _update(self, field, value):

def on_scan(self):
self.port.clear()
self.port.addItems(sorted(x[0] for x in comports()))
for port_info in sorted(patch_ports_info(comports())):
self.port.addItem(port_info.device, port_info)

def on_port_changed(self, value):
self._update('port', value)
Expand Down
15 changes: 15 additions & 0 deletions plover/oslayer/linux/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pathlib import Path


def patch_ports_info(port_list):
'''Patch serial ports info to use device-by-id links.'''
try:
device_by_id = {
str(device.resolve()): str(device)
for device in Path('/dev/serial/by-id').iterdir()
}
except FileNotFoundError:
device_by_id = {}
for port_info in port_list:
port_info.device = device_by_id.get(port_info.device, port_info.device)
return port_list
3 changes: 3 additions & 0 deletions plover/oslayer/osx/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def patch_ports_info(port_list):
'''NOP…'''
return port_list
16 changes: 16 additions & 0 deletions plover/oslayer/windows/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# “Microsoft Corp”.
MICROSOFT_VID = 0x045e


def patch_ports_info(port_list):
'''Patch serial ports info to remove erroneous manufacturer.
Because on Windows 10 most USB serial devices will use the generic
CDC/ACM driver, their manufacturer is reported as Microsoft. Strip
that information if the vendor ID does not match.
'''
for port_info in port_list:
if port_info.manufacturer == 'Microsoft' \
and port_info.vid != MICROSOFT_VID:
port_info.manufacturer = None
return port_list

0 comments on commit 900c04b

Please sign in to comment.