Initial commit
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Byte-compiled / cache files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Editor / OS files
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Python packaging artifacts
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
91
deps/serial/__init__.py
vendored
Normal file
91
deps/serial/__init__.py
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# This is a wrapper module for different platform implementations
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from serial.serialutil import *
|
||||||
|
#~ SerialBase, SerialException, to_bytes, iterbytes
|
||||||
|
|
||||||
|
__version__ = '3.5'
|
||||||
|
|
||||||
|
VERSION = __version__
|
||||||
|
|
||||||
|
# pylint: disable=wrong-import-position
|
||||||
|
if sys.platform == 'cli':
|
||||||
|
from serial.serialcli import Serial
|
||||||
|
else:
|
||||||
|
import os
|
||||||
|
# chose an implementation, depending on os
|
||||||
|
if os.name == 'nt': # sys.platform == 'win32':
|
||||||
|
from serial.serialwin32 import Serial
|
||||||
|
elif os.name == 'posix':
|
||||||
|
from serial.serialposix import Serial, PosixPollSerial, VTIMESerial # noqa
|
||||||
|
elif os.name == 'java':
|
||||||
|
from serial.serialjava import Serial
|
||||||
|
else:
|
||||||
|
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||||
|
|
||||||
|
|
||||||
|
protocol_handler_packages = [
|
||||||
|
'serial.urlhandler',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def serial_for_url(url, *args, **kwargs):
|
||||||
|
"""\
|
||||||
|
Get an instance of the Serial class, depending on port/url. The port is not
|
||||||
|
opened when the keyword parameter 'do_not_open' is true, by default it
|
||||||
|
is. All other parameters are directly passed to the __init__ method when
|
||||||
|
the port is instantiated.
|
||||||
|
|
||||||
|
The list of package names that is searched for protocol handlers is kept in
|
||||||
|
``protocol_handler_packages``.
|
||||||
|
|
||||||
|
e.g. we want to support a URL ``foobar://``. A module
|
||||||
|
``my_handlers.protocol_foobar`` is provided by the user. Then
|
||||||
|
``protocol_handler_packages.append("my_handlers")`` would extend the search
|
||||||
|
path so that ``serial_for_url("foobar://"))`` would work.
|
||||||
|
"""
|
||||||
|
# check and remove extra parameter to not confuse the Serial class
|
||||||
|
do_open = not kwargs.pop('do_not_open', False)
|
||||||
|
# the default is to use the native implementation
|
||||||
|
klass = Serial
|
||||||
|
try:
|
||||||
|
url_lowercase = url.lower()
|
||||||
|
except AttributeError:
|
||||||
|
# it's not a string, use default
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# if it is an URL, try to import the handler module from the list of possible packages
|
||||||
|
if '://' in url_lowercase:
|
||||||
|
protocol = url_lowercase.split('://', 1)[0]
|
||||||
|
module_name = '.protocol_{}'.format(protocol)
|
||||||
|
for package_name in protocol_handler_packages:
|
||||||
|
try:
|
||||||
|
importlib.import_module(package_name)
|
||||||
|
handler_module = importlib.import_module(module_name, package_name)
|
||||||
|
except ImportError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if hasattr(handler_module, 'serial_class_for_url'):
|
||||||
|
url, klass = handler_module.serial_class_for_url(url)
|
||||||
|
else:
|
||||||
|
klass = handler_module.Serial
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError('invalid URL, protocol {!r} not known'.format(protocol))
|
||||||
|
# instantiate and open when desired
|
||||||
|
instance = klass(None, *args, **kwargs)
|
||||||
|
instance.port = url
|
||||||
|
if do_open:
|
||||||
|
instance.open()
|
||||||
|
return instance
|
||||||
3
deps/serial/__main__.py
vendored
Normal file
3
deps/serial/__main__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .tools import miniterm
|
||||||
|
|
||||||
|
miniterm.main()
|
||||||
1351
deps/serial/rfc2217.py
vendored
Normal file
1351
deps/serial/rfc2217.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
94
deps/serial/rs485.py
vendored
Normal file
94
deps/serial/rs485.py
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# RS485 support
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
"""\
|
||||||
|
The settings for RS485 are stored in a dedicated object that can be applied to
|
||||||
|
serial ports (where supported).
|
||||||
|
NOTE: Some implementations may only support a subset of the settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import time
|
||||||
|
import serial
|
||||||
|
|
||||||
|
|
||||||
|
class RS485Settings(object):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
rts_level_for_tx=True,
|
||||||
|
rts_level_for_rx=False,
|
||||||
|
loopback=False,
|
||||||
|
delay_before_tx=None,
|
||||||
|
delay_before_rx=None):
|
||||||
|
self.rts_level_for_tx = rts_level_for_tx
|
||||||
|
self.rts_level_for_rx = rts_level_for_rx
|
||||||
|
self.loopback = loopback
|
||||||
|
self.delay_before_tx = delay_before_tx
|
||||||
|
self.delay_before_rx = delay_before_rx
|
||||||
|
|
||||||
|
|
||||||
|
class RS485(serial.Serial):
|
||||||
|
"""\
|
||||||
|
A subclass that replaces the write method with one that toggles RTS
|
||||||
|
according to the RS485 settings.
|
||||||
|
|
||||||
|
NOTE: This may work unreliably on some serial ports (control signals not
|
||||||
|
synchronized or delayed compared to data). Using delays may be
|
||||||
|
unreliable (varying times, larger than expected) as the OS may not
|
||||||
|
support very fine grained delays (no smaller than in the order of
|
||||||
|
tens of milliseconds).
|
||||||
|
|
||||||
|
NOTE: Some implementations support this natively. Better performance
|
||||||
|
can be expected when the native version is used.
|
||||||
|
|
||||||
|
NOTE: The loopback property is ignored by this implementation. The actual
|
||||||
|
behavior depends on the used hardware.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
ser = RS485(...)
|
||||||
|
ser.rs485_mode = RS485Settings(...)
|
||||||
|
ser.write(b'hello')
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(RS485, self).__init__(*args, **kwargs)
|
||||||
|
self._alternate_rs485_settings = None
|
||||||
|
|
||||||
|
def write(self, b):
|
||||||
|
"""Write to port, controlling RTS before and after transmitting."""
|
||||||
|
if self._alternate_rs485_settings is not None:
|
||||||
|
# apply level for TX and optional delay
|
||||||
|
self.setRTS(self._alternate_rs485_settings.rts_level_for_tx)
|
||||||
|
if self._alternate_rs485_settings.delay_before_tx is not None:
|
||||||
|
time.sleep(self._alternate_rs485_settings.delay_before_tx)
|
||||||
|
# write and wait for data to be written
|
||||||
|
super(RS485, self).write(b)
|
||||||
|
super(RS485, self).flush()
|
||||||
|
# optional delay and apply level for RX
|
||||||
|
if self._alternate_rs485_settings.delay_before_rx is not None:
|
||||||
|
time.sleep(self._alternate_rs485_settings.delay_before_rx)
|
||||||
|
self.setRTS(self._alternate_rs485_settings.rts_level_for_rx)
|
||||||
|
else:
|
||||||
|
super(RS485, self).write(b)
|
||||||
|
|
||||||
|
# redirect where the property stores the settings so that underlying Serial
|
||||||
|
# instance does not see them
|
||||||
|
@property
|
||||||
|
def rs485_mode(self):
|
||||||
|
"""\
|
||||||
|
Enable RS485 mode and apply new settings, set to None to disable.
|
||||||
|
See serial.rs485.RS485Settings for more info about the value.
|
||||||
|
"""
|
||||||
|
return self._alternate_rs485_settings
|
||||||
|
|
||||||
|
@rs485_mode.setter
|
||||||
|
def rs485_mode(self, rs485_settings):
|
||||||
|
self._alternate_rs485_settings = rs485_settings
|
||||||
253
deps/serial/serialcli.py
vendored
Normal file
253
deps/serial/serialcli.py
vendored
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# Backend for .NET/Mono (IronPython), .NET >= 2
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2008-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import System
|
||||||
|
import System.IO.Ports
|
||||||
|
from serial.serialutil import *
|
||||||
|
|
||||||
|
# must invoke function with byte array, make a helper to convert strings
|
||||||
|
# to byte arrays
|
||||||
|
sab = System.Array[System.Byte]
|
||||||
|
|
||||||
|
|
||||||
|
def as_byte_array(string):
|
||||||
|
return sab([ord(x) for x in string]) # XXX will require adaption when run with a 3.x compatible IronPython
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(SerialBase):
|
||||||
|
"""Serial port implementation for .NET/Mono."""
|
||||||
|
|
||||||
|
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||||
|
9600, 19200, 38400, 57600, 115200)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""\
|
||||||
|
Open port with current settings. This may throw a SerialException
|
||||||
|
if the port cannot be opened.
|
||||||
|
"""
|
||||||
|
if self._port is None:
|
||||||
|
raise SerialException("Port must be configured before it can be used.")
|
||||||
|
if self.is_open:
|
||||||
|
raise SerialException("Port is already open.")
|
||||||
|
try:
|
||||||
|
self._port_handle = System.IO.Ports.SerialPort(self.portstr)
|
||||||
|
except Exception as msg:
|
||||||
|
self._port_handle = None
|
||||||
|
raise SerialException("could not open port %s: %s" % (self.portstr, msg))
|
||||||
|
|
||||||
|
# if RTS and/or DTR are not set before open, they default to True
|
||||||
|
if self._rts_state is None:
|
||||||
|
self._rts_state = True
|
||||||
|
if self._dtr_state is None:
|
||||||
|
self._dtr_state = True
|
||||||
|
|
||||||
|
self._reconfigure_port()
|
||||||
|
self._port_handle.Open()
|
||||||
|
self.is_open = True
|
||||||
|
if not self._dsrdtr:
|
||||||
|
self._update_dtr_state()
|
||||||
|
if not self._rtscts:
|
||||||
|
self._update_rts_state()
|
||||||
|
self.reset_input_buffer()
|
||||||
|
|
||||||
|
def _reconfigure_port(self):
|
||||||
|
"""Set communication parameters on opened port."""
|
||||||
|
if not self._port_handle:
|
||||||
|
raise SerialException("Can only operate on a valid port handle")
|
||||||
|
|
||||||
|
#~ self._port_handle.ReceivedBytesThreshold = 1
|
||||||
|
|
||||||
|
if self._timeout is None:
|
||||||
|
self._port_handle.ReadTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
|
||||||
|
else:
|
||||||
|
self._port_handle.ReadTimeout = int(self._timeout * 1000)
|
||||||
|
|
||||||
|
# if self._timeout != 0 and self._interCharTimeout is not None:
|
||||||
|
# timeouts = (int(self._interCharTimeout * 1000),) + timeouts[1:]
|
||||||
|
|
||||||
|
if self._write_timeout is None:
|
||||||
|
self._port_handle.WriteTimeout = System.IO.Ports.SerialPort.InfiniteTimeout
|
||||||
|
else:
|
||||||
|
self._port_handle.WriteTimeout = int(self._write_timeout * 1000)
|
||||||
|
|
||||||
|
# Setup the connection info.
|
||||||
|
try:
|
||||||
|
self._port_handle.BaudRate = self._baudrate
|
||||||
|
except IOError as e:
|
||||||
|
# catch errors from illegal baudrate settings
|
||||||
|
raise ValueError(str(e))
|
||||||
|
|
||||||
|
if self._bytesize == FIVEBITS:
|
||||||
|
self._port_handle.DataBits = 5
|
||||||
|
elif self._bytesize == SIXBITS:
|
||||||
|
self._port_handle.DataBits = 6
|
||||||
|
elif self._bytesize == SEVENBITS:
|
||||||
|
self._port_handle.DataBits = 7
|
||||||
|
elif self._bytesize == EIGHTBITS:
|
||||||
|
self._port_handle.DataBits = 8
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported number of data bits: %r" % self._bytesize)
|
||||||
|
|
||||||
|
if self._parity == PARITY_NONE:
|
||||||
|
self._port_handle.Parity = getattr(System.IO.Ports.Parity, 'None') # reserved keyword in Py3k
|
||||||
|
elif self._parity == PARITY_EVEN:
|
||||||
|
self._port_handle.Parity = System.IO.Ports.Parity.Even
|
||||||
|
elif self._parity == PARITY_ODD:
|
||||||
|
self._port_handle.Parity = System.IO.Ports.Parity.Odd
|
||||||
|
elif self._parity == PARITY_MARK:
|
||||||
|
self._port_handle.Parity = System.IO.Ports.Parity.Mark
|
||||||
|
elif self._parity == PARITY_SPACE:
|
||||||
|
self._port_handle.Parity = System.IO.Ports.Parity.Space
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported parity mode: %r" % self._parity)
|
||||||
|
|
||||||
|
if self._stopbits == STOPBITS_ONE:
|
||||||
|
self._port_handle.StopBits = System.IO.Ports.StopBits.One
|
||||||
|
elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
|
||||||
|
self._port_handle.StopBits = System.IO.Ports.StopBits.OnePointFive
|
||||||
|
elif self._stopbits == STOPBITS_TWO:
|
||||||
|
self._port_handle.StopBits = System.IO.Ports.StopBits.Two
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported number of stop bits: %r" % self._stopbits)
|
||||||
|
|
||||||
|
if self._rtscts and self._xonxoff:
|
||||||
|
self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSendXOnXOff
|
||||||
|
elif self._rtscts:
|
||||||
|
self._port_handle.Handshake = System.IO.Ports.Handshake.RequestToSend
|
||||||
|
elif self._xonxoff:
|
||||||
|
self._port_handle.Handshake = System.IO.Ports.Handshake.XOnXOff
|
||||||
|
else:
|
||||||
|
self._port_handle.Handshake = getattr(System.IO.Ports.Handshake, 'None') # reserved keyword in Py3k
|
||||||
|
|
||||||
|
#~ def __del__(self):
|
||||||
|
#~ self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close port"""
|
||||||
|
if self.is_open:
|
||||||
|
if self._port_handle:
|
||||||
|
try:
|
||||||
|
self._port_handle.Close()
|
||||||
|
except System.IO.Ports.InvalidOperationException:
|
||||||
|
# ignore errors. can happen for unplugged USB serial devices
|
||||||
|
pass
|
||||||
|
self._port_handle = None
|
||||||
|
self.is_open = False
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
"""Return the number of characters currently in the input buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
return self._port_handle.BytesToRead
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
# must use single byte reads as this is the only way to read
|
||||||
|
# without applying encodings
|
||||||
|
data = bytearray()
|
||||||
|
while size:
|
||||||
|
try:
|
||||||
|
data.append(self._port_handle.ReadByte())
|
||||||
|
except System.TimeoutException:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
size -= 1
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""Output the given string over the serial port."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
#~ if not isinstance(data, (bytes, bytearray)):
|
||||||
|
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||||
|
try:
|
||||||
|
# must call overloaded method with byte array argument
|
||||||
|
# as this is the only one not applying encodings
|
||||||
|
self._port_handle.Write(as_byte_array(data), 0, len(data))
|
||||||
|
except System.TimeoutException:
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
"""Clear input buffer, discarding all that is in the buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._port_handle.DiscardInBuffer()
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
"""\
|
||||||
|
Clear output buffer, aborting the current output and
|
||||||
|
discarding all that is in the buffer.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._port_handle.DiscardOutBuffer()
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""
|
||||||
|
Set break: Controls TXD. When active, to transmitting is possible.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._port_handle.BreakState = bool(self._break_state)
|
||||||
|
|
||||||
|
def _update_rts_state(self):
|
||||||
|
"""Set terminal status line: Request To Send"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._port_handle.RtsEnable = bool(self._rts_state)
|
||||||
|
|
||||||
|
def _update_dtr_state(self):
|
||||||
|
"""Set terminal status line: Data Terminal Ready"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._port_handle.DtrEnable = bool(self._dtr_state)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cts(self):
|
||||||
|
"""Read terminal status line: Clear To Send"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
return self._port_handle.CtsHolding
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsr(self):
|
||||||
|
"""Read terminal status line: Data Set Ready"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
return self._port_handle.DsrHolding
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ri(self):
|
||||||
|
"""Read terminal status line: Ring Indicator"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
#~ return self._port_handle.XXX
|
||||||
|
return False # XXX an error would be better
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cd(self):
|
||||||
|
"""Read terminal status line: Carrier Detect"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
return self._port_handle.CDHolding
|
||||||
|
|
||||||
|
# - - platform specific - - - -
|
||||||
|
# none
|
||||||
251
deps/serial/serialjava.py
vendored
Normal file
251
deps/serial/serialjava.py
vendored
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
#!jython
|
||||||
|
#
|
||||||
|
# Backend Jython with JavaComm
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2002-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from serial.serialutil import *
|
||||||
|
|
||||||
|
|
||||||
|
def my_import(name):
|
||||||
|
mod = __import__(name)
|
||||||
|
components = name.split('.')
|
||||||
|
for comp in components[1:]:
|
||||||
|
mod = getattr(mod, comp)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def detect_java_comm(names):
|
||||||
|
"""try given list of modules and return that imports"""
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
mod = my_import(name)
|
||||||
|
mod.SerialPort
|
||||||
|
return mod
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
pass
|
||||||
|
raise ImportError("No Java Communications API implementation found")
|
||||||
|
|
||||||
|
|
||||||
|
# Java Communications API implementations
|
||||||
|
# http://mho.republika.pl/java/comm/
|
||||||
|
|
||||||
|
comm = detect_java_comm([
|
||||||
|
'javax.comm', # Sun/IBM
|
||||||
|
'gnu.io', # RXTX
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def device(portnumber):
|
||||||
|
"""Turn a port number into a device name"""
|
||||||
|
enum = comm.CommPortIdentifier.getPortIdentifiers()
|
||||||
|
ports = []
|
||||||
|
while enum.hasMoreElements():
|
||||||
|
el = enum.nextElement()
|
||||||
|
if el.getPortType() == comm.CommPortIdentifier.PORT_SERIAL:
|
||||||
|
ports.append(el)
|
||||||
|
return ports[portnumber].getName()
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(SerialBase):
|
||||||
|
"""\
|
||||||
|
Serial port class, implemented with Java Communications API and
|
||||||
|
thus usable with jython and the appropriate java extension.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""\
|
||||||
|
Open port with current settings. This may throw a SerialException
|
||||||
|
if the port cannot be opened.
|
||||||
|
"""
|
||||||
|
if self._port is None:
|
||||||
|
raise SerialException("Port must be configured before it can be used.")
|
||||||
|
if self.is_open:
|
||||||
|
raise SerialException("Port is already open.")
|
||||||
|
if type(self._port) == type(''): # strings are taken directly
|
||||||
|
portId = comm.CommPortIdentifier.getPortIdentifier(self._port)
|
||||||
|
else:
|
||||||
|
portId = comm.CommPortIdentifier.getPortIdentifier(device(self._port)) # numbers are transformed to a comport id obj
|
||||||
|
try:
|
||||||
|
self.sPort = portId.open("python serial module", 10)
|
||||||
|
except Exception as msg:
|
||||||
|
self.sPort = None
|
||||||
|
raise SerialException("Could not open port: %s" % msg)
|
||||||
|
self._reconfigurePort()
|
||||||
|
self._instream = self.sPort.getInputStream()
|
||||||
|
self._outstream = self.sPort.getOutputStream()
|
||||||
|
self.is_open = True
|
||||||
|
|
||||||
|
def _reconfigurePort(self):
|
||||||
|
"""Set communication parameters on opened port."""
|
||||||
|
if not self.sPort:
|
||||||
|
raise SerialException("Can only operate on a valid port handle")
|
||||||
|
|
||||||
|
self.sPort.enableReceiveTimeout(30)
|
||||||
|
if self._bytesize == FIVEBITS:
|
||||||
|
jdatabits = comm.SerialPort.DATABITS_5
|
||||||
|
elif self._bytesize == SIXBITS:
|
||||||
|
jdatabits = comm.SerialPort.DATABITS_6
|
||||||
|
elif self._bytesize == SEVENBITS:
|
||||||
|
jdatabits = comm.SerialPort.DATABITS_7
|
||||||
|
elif self._bytesize == EIGHTBITS:
|
||||||
|
jdatabits = comm.SerialPort.DATABITS_8
|
||||||
|
else:
|
||||||
|
raise ValueError("unsupported bytesize: %r" % self._bytesize)
|
||||||
|
|
||||||
|
if self._stopbits == STOPBITS_ONE:
|
||||||
|
jstopbits = comm.SerialPort.STOPBITS_1
|
||||||
|
elif self._stopbits == STOPBITS_ONE_POINT_FIVE:
|
||||||
|
jstopbits = comm.SerialPort.STOPBITS_1_5
|
||||||
|
elif self._stopbits == STOPBITS_TWO:
|
||||||
|
jstopbits = comm.SerialPort.STOPBITS_2
|
||||||
|
else:
|
||||||
|
raise ValueError("unsupported number of stopbits: %r" % self._stopbits)
|
||||||
|
|
||||||
|
if self._parity == PARITY_NONE:
|
||||||
|
jparity = comm.SerialPort.PARITY_NONE
|
||||||
|
elif self._parity == PARITY_EVEN:
|
||||||
|
jparity = comm.SerialPort.PARITY_EVEN
|
||||||
|
elif self._parity == PARITY_ODD:
|
||||||
|
jparity = comm.SerialPort.PARITY_ODD
|
||||||
|
elif self._parity == PARITY_MARK:
|
||||||
|
jparity = comm.SerialPort.PARITY_MARK
|
||||||
|
elif self._parity == PARITY_SPACE:
|
||||||
|
jparity = comm.SerialPort.PARITY_SPACE
|
||||||
|
else:
|
||||||
|
raise ValueError("unsupported parity type: %r" % self._parity)
|
||||||
|
|
||||||
|
jflowin = jflowout = 0
|
||||||
|
if self._rtscts:
|
||||||
|
jflowin |= comm.SerialPort.FLOWCONTROL_RTSCTS_IN
|
||||||
|
jflowout |= comm.SerialPort.FLOWCONTROL_RTSCTS_OUT
|
||||||
|
if self._xonxoff:
|
||||||
|
jflowin |= comm.SerialPort.FLOWCONTROL_XONXOFF_IN
|
||||||
|
jflowout |= comm.SerialPort.FLOWCONTROL_XONXOFF_OUT
|
||||||
|
|
||||||
|
self.sPort.setSerialPortParams(self._baudrate, jdatabits, jstopbits, jparity)
|
||||||
|
self.sPort.setFlowControlMode(jflowin | jflowout)
|
||||||
|
|
||||||
|
if self._timeout >= 0:
|
||||||
|
self.sPort.enableReceiveTimeout(int(self._timeout*1000))
|
||||||
|
else:
|
||||||
|
self.sPort.disableReceiveTimeout()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close port"""
|
||||||
|
if self.is_open:
|
||||||
|
if self.sPort:
|
||||||
|
self._instream.close()
|
||||||
|
self._outstream.close()
|
||||||
|
self.sPort.close()
|
||||||
|
self.sPort = None
|
||||||
|
self.is_open = False
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
"""Return the number of characters currently in the input buffer."""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
return self._instream.available()
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
read = bytearray()
|
||||||
|
if size > 0:
|
||||||
|
while len(read) < size:
|
||||||
|
x = self._instream.read()
|
||||||
|
if x == -1:
|
||||||
|
if self.timeout >= 0:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
read.append(x)
|
||||||
|
return bytes(read)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""Output the given string over the serial port."""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if not isinstance(data, (bytes, bytearray)):
|
||||||
|
raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||||
|
self._outstream.write(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
"""Clear input buffer, discarding all that is in the buffer."""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._instream.skip(self._instream.available())
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
"""\
|
||||||
|
Clear output buffer, aborting the current output and
|
||||||
|
discarding all that is in the buffer.
|
||||||
|
"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._outstream.flush()
|
||||||
|
|
||||||
|
def send_break(self, duration=0.25):
|
||||||
|
"""Send break condition. Timed, returns to idle state after given duration."""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.sPort.sendBreak(duration*1000.0)
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""Set break: Controls TXD. When active, to transmitting is possible."""
|
||||||
|
if self.fd is None:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
raise SerialException("The _update_break_state function is not implemented in java.")
|
||||||
|
|
||||||
|
def _update_rts_state(self):
|
||||||
|
"""Set terminal status line: Request To Send"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.sPort.setRTS(self._rts_state)
|
||||||
|
|
||||||
|
def _update_dtr_state(self):
|
||||||
|
"""Set terminal status line: Data Terminal Ready"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.sPort.setDTR(self._dtr_state)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cts(self):
|
||||||
|
"""Read terminal status line: Clear To Send"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.sPort.isCTS()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsr(self):
|
||||||
|
"""Read terminal status line: Data Set Ready"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.sPort.isDSR()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ri(self):
|
||||||
|
"""Read terminal status line: Ring Indicator"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.sPort.isRI()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cd(self):
|
||||||
|
"""Read terminal status line: Carrier Detect"""
|
||||||
|
if not self.sPort:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.sPort.isCD()
|
||||||
900
deps/serial/serialposix.py
vendored
Normal file
900
deps/serial/serialposix.py
vendored
Normal file
@@ -0,0 +1,900 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# backend for serial IO for POSIX compatible systems, like Linux, OSX
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
#
|
||||||
|
# parts based on code from Grant B. Edwards <grante@visi.com>:
|
||||||
|
# ftp://ftp.visi.com/users/grante/python/PosixSerial.py
|
||||||
|
#
|
||||||
|
# references: http://www.easysw.com/~mike/serial/serial.html
|
||||||
|
|
||||||
|
# Collection of port names (was previously used by number_to_device which was
|
||||||
|
# removed.
|
||||||
|
# - Linux /dev/ttyS%d (confirmed)
|
||||||
|
# - cygwin/win32 /dev/com%d (confirmed)
|
||||||
|
# - openbsd (OpenBSD) /dev/cua%02d
|
||||||
|
# - bsd*, freebsd* /dev/cuad%d
|
||||||
|
# - darwin (OS X) /dev/cuad%d
|
||||||
|
# - netbsd /dev/dty%02d (NetBSD 1.6 testing by Erk)
|
||||||
|
# - irix (IRIX) /dev/ttyf%d (partially tested) names depending on flow control
|
||||||
|
# - hp (HP-UX) /dev/tty%dp0 (not tested)
|
||||||
|
# - sunos (Solaris/SunOS) /dev/tty%c (letters, 'a'..'z') (confirmed)
|
||||||
|
# - aix (AIX) /dev/tty%d
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
import errno
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
import select
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import termios
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.serialutil import SerialBase, SerialException, to_bytes, \
|
||||||
|
PortNotOpenError, SerialTimeoutException, Timeout
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformSpecificBase(object):
|
||||||
|
BAUDRATE_CONSTANTS = {}
|
||||||
|
|
||||||
|
def _set_special_baudrate(self, baudrate):
|
||||||
|
raise NotImplementedError('non-standard baudrates are not supported on this platform')
|
||||||
|
|
||||||
|
def _set_rs485_mode(self, rs485_settings):
|
||||||
|
raise NotImplementedError('RS485 not supported on this platform')
|
||||||
|
|
||||||
|
def set_low_latency_mode(self, low_latency_settings):
|
||||||
|
raise NotImplementedError('Low latency not supported on this platform')
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""\
|
||||||
|
Set break: Controls TXD. When active, no transmitting is possible.
|
||||||
|
"""
|
||||||
|
if self._break_state:
|
||||||
|
fcntl.ioctl(self.fd, TIOCSBRK)
|
||||||
|
else:
|
||||||
|
fcntl.ioctl(self.fd, TIOCCBRK)
|
||||||
|
|
||||||
|
|
||||||
|
# some systems support an extra flag to enable the two in POSIX unsupported
|
||||||
|
# paritiy settings for MARK and SPACE
|
||||||
|
CMSPAR = 0 # default, for unsupported platforms, override below
|
||||||
|
|
||||||
|
# try to detect the OS so that a device can be selected...
|
||||||
|
# this code block should supply a device() and set_special_baudrate() function
|
||||||
|
# for the platform
|
||||||
|
plat = sys.platform.lower()
|
||||||
|
|
||||||
|
if plat[:5] == 'linux': # Linux (confirmed) # noqa
|
||||||
|
import array
|
||||||
|
|
||||||
|
# extra termios flags
|
||||||
|
CMSPAR = 0o10000000000 # Use "stick" (mark/space) parity
|
||||||
|
|
||||||
|
# baudrate ioctls
|
||||||
|
TCGETS2 = 0x802C542A
|
||||||
|
TCSETS2 = 0x402C542B
|
||||||
|
BOTHER = 0o010000
|
||||||
|
|
||||||
|
# RS485 ioctls
|
||||||
|
TIOCGRS485 = 0x542E
|
||||||
|
TIOCSRS485 = 0x542F
|
||||||
|
SER_RS485_ENABLED = 0b00000001
|
||||||
|
SER_RS485_RTS_ON_SEND = 0b00000010
|
||||||
|
SER_RS485_RTS_AFTER_SEND = 0b00000100
|
||||||
|
SER_RS485_RX_DURING_TX = 0b00010000
|
||||||
|
|
||||||
|
class PlatformSpecific(PlatformSpecificBase):
|
||||||
|
BAUDRATE_CONSTANTS = {
|
||||||
|
0: 0o000000, # hang up
|
||||||
|
50: 0o000001,
|
||||||
|
75: 0o000002,
|
||||||
|
110: 0o000003,
|
||||||
|
134: 0o000004,
|
||||||
|
150: 0o000005,
|
||||||
|
200: 0o000006,
|
||||||
|
300: 0o000007,
|
||||||
|
600: 0o000010,
|
||||||
|
1200: 0o000011,
|
||||||
|
1800: 0o000012,
|
||||||
|
2400: 0o000013,
|
||||||
|
4800: 0o000014,
|
||||||
|
9600: 0o000015,
|
||||||
|
19200: 0o000016,
|
||||||
|
38400: 0o000017,
|
||||||
|
57600: 0o010001,
|
||||||
|
115200: 0o010002,
|
||||||
|
230400: 0o010003,
|
||||||
|
460800: 0o010004,
|
||||||
|
500000: 0o010005,
|
||||||
|
576000: 0o010006,
|
||||||
|
921600: 0o010007,
|
||||||
|
1000000: 0o010010,
|
||||||
|
1152000: 0o010011,
|
||||||
|
1500000: 0o010012,
|
||||||
|
2000000: 0o010013,
|
||||||
|
2500000: 0o010014,
|
||||||
|
3000000: 0o010015,
|
||||||
|
3500000: 0o010016,
|
||||||
|
4000000: 0o010017
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_low_latency_mode(self, low_latency_settings):
|
||||||
|
buf = array.array('i', [0] * 32)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# get serial_struct
|
||||||
|
fcntl.ioctl(self.fd, termios.TIOCGSERIAL, buf)
|
||||||
|
|
||||||
|
# set or unset ASYNC_LOW_LATENCY flag
|
||||||
|
if low_latency_settings:
|
||||||
|
buf[4] |= 0x2000
|
||||||
|
else:
|
||||||
|
buf[4] &= ~0x2000
|
||||||
|
|
||||||
|
# set serial_struct
|
||||||
|
fcntl.ioctl(self.fd, termios.TIOCSSERIAL, buf)
|
||||||
|
except IOError as e:
|
||||||
|
raise ValueError('Failed to update ASYNC_LOW_LATENCY flag to {}: {}'.format(low_latency_settings, e))
|
||||||
|
|
||||||
|
def _set_special_baudrate(self, baudrate):
|
||||||
|
# right size is 44 on x86_64, allow for some growth
|
||||||
|
buf = array.array('i', [0] * 64)
|
||||||
|
try:
|
||||||
|
# get serial_struct
|
||||||
|
fcntl.ioctl(self.fd, TCGETS2, buf)
|
||||||
|
# set custom speed
|
||||||
|
buf[2] &= ~termios.CBAUD
|
||||||
|
buf[2] |= BOTHER
|
||||||
|
buf[9] = buf[10] = baudrate
|
||||||
|
|
||||||
|
# set serial_struct
|
||||||
|
fcntl.ioctl(self.fd, TCSETS2, buf)
|
||||||
|
except IOError as e:
|
||||||
|
raise ValueError('Failed to set custom baud rate ({}): {}'.format(baudrate, e))
|
||||||
|
|
||||||
|
def _set_rs485_mode(self, rs485_settings):
|
||||||
|
buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
|
||||||
|
try:
|
||||||
|
fcntl.ioctl(self.fd, TIOCGRS485, buf)
|
||||||
|
buf[0] |= SER_RS485_ENABLED
|
||||||
|
if rs485_settings is not None:
|
||||||
|
if rs485_settings.loopback:
|
||||||
|
buf[0] |= SER_RS485_RX_DURING_TX
|
||||||
|
else:
|
||||||
|
buf[0] &= ~SER_RS485_RX_DURING_TX
|
||||||
|
if rs485_settings.rts_level_for_tx:
|
||||||
|
buf[0] |= SER_RS485_RTS_ON_SEND
|
||||||
|
else:
|
||||||
|
buf[0] &= ~SER_RS485_RTS_ON_SEND
|
||||||
|
if rs485_settings.rts_level_for_rx:
|
||||||
|
buf[0] |= SER_RS485_RTS_AFTER_SEND
|
||||||
|
else:
|
||||||
|
buf[0] &= ~SER_RS485_RTS_AFTER_SEND
|
||||||
|
if rs485_settings.delay_before_tx is not None:
|
||||||
|
buf[1] = int(rs485_settings.delay_before_tx * 1000)
|
||||||
|
if rs485_settings.delay_before_rx is not None:
|
||||||
|
buf[2] = int(rs485_settings.delay_before_rx * 1000)
|
||||||
|
else:
|
||||||
|
buf[0] = 0 # clear SER_RS485_ENABLED
|
||||||
|
fcntl.ioctl(self.fd, TIOCSRS485, buf)
|
||||||
|
except IOError as e:
|
||||||
|
raise ValueError('Failed to set RS485 mode: {}'.format(e))
|
||||||
|
|
||||||
|
|
||||||
|
elif plat == 'cygwin': # cygwin/win32 (confirmed)
|
||||||
|
|
||||||
|
class PlatformSpecific(PlatformSpecificBase):
|
||||||
|
BAUDRATE_CONSTANTS = {
|
||||||
|
128000: 0x01003,
|
||||||
|
256000: 0x01005,
|
||||||
|
500000: 0x01007,
|
||||||
|
576000: 0x01008,
|
||||||
|
921600: 0x01009,
|
||||||
|
1000000: 0x0100a,
|
||||||
|
1152000: 0x0100b,
|
||||||
|
1500000: 0x0100c,
|
||||||
|
2000000: 0x0100d,
|
||||||
|
2500000: 0x0100e,
|
||||||
|
3000000: 0x0100f
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
elif plat[:6] == 'darwin': # OS X
|
||||||
|
import array
|
||||||
|
IOSSIOSPEED = 0x80045402 # _IOW('T', 2, speed_t)
|
||||||
|
|
||||||
|
class PlatformSpecific(PlatformSpecificBase):
|
||||||
|
osx_version = os.uname()[2].split('.')
|
||||||
|
TIOCSBRK = 0x2000747B # _IO('t', 123)
|
||||||
|
TIOCCBRK = 0x2000747A # _IO('t', 122)
|
||||||
|
|
||||||
|
# Tiger or above can support arbitrary serial speeds
|
||||||
|
if int(osx_version[0]) >= 8:
|
||||||
|
def _set_special_baudrate(self, baudrate):
|
||||||
|
# use IOKit-specific call to set up high speeds
|
||||||
|
buf = array.array('i', [baudrate])
|
||||||
|
fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1)
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""\
|
||||||
|
Set break: Controls TXD. When active, no transmitting is possible.
|
||||||
|
"""
|
||||||
|
if self._break_state:
|
||||||
|
fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
|
||||||
|
else:
|
||||||
|
fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
|
||||||
|
|
||||||
|
elif plat[:3] == 'bsd' or \
|
||||||
|
plat[:7] == 'freebsd' or \
|
||||||
|
plat[:6] == 'netbsd' or \
|
||||||
|
plat[:7] == 'openbsd':
|
||||||
|
|
||||||
|
class ReturnBaudrate(object):
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return key
|
||||||
|
|
||||||
|
class PlatformSpecific(PlatformSpecificBase):
|
||||||
|
# Only tested on FreeBSD:
|
||||||
|
# The baud rate may be passed in as
|
||||||
|
# a literal value.
|
||||||
|
BAUDRATE_CONSTANTS = ReturnBaudrate()
|
||||||
|
|
||||||
|
TIOCSBRK = 0x2000747B # _IO('t', 123)
|
||||||
|
TIOCCBRK = 0x2000747A # _IO('t', 122)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""\
|
||||||
|
Set break: Controls TXD. When active, no transmitting is possible.
|
||||||
|
"""
|
||||||
|
if self._break_state:
|
||||||
|
fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
|
||||||
|
else:
|
||||||
|
fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
|
||||||
|
|
||||||
|
else:
|
||||||
|
class PlatformSpecific(PlatformSpecificBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# load some constants for later use.
|
||||||
|
# try to use values from termios, use defaults from linux otherwise
|
||||||
|
TIOCMGET = getattr(termios, 'TIOCMGET', 0x5415)
|
||||||
|
TIOCMBIS = getattr(termios, 'TIOCMBIS', 0x5416)
|
||||||
|
TIOCMBIC = getattr(termios, 'TIOCMBIC', 0x5417)
|
||||||
|
TIOCMSET = getattr(termios, 'TIOCMSET', 0x5418)
|
||||||
|
|
||||||
|
# TIOCM_LE = getattr(termios, 'TIOCM_LE', 0x001)
|
||||||
|
TIOCM_DTR = getattr(termios, 'TIOCM_DTR', 0x002)
|
||||||
|
TIOCM_RTS = getattr(termios, 'TIOCM_RTS', 0x004)
|
||||||
|
# TIOCM_ST = getattr(termios, 'TIOCM_ST', 0x008)
|
||||||
|
# TIOCM_SR = getattr(termios, 'TIOCM_SR', 0x010)
|
||||||
|
|
||||||
|
TIOCM_CTS = getattr(termios, 'TIOCM_CTS', 0x020)
|
||||||
|
TIOCM_CAR = getattr(termios, 'TIOCM_CAR', 0x040)
|
||||||
|
TIOCM_RNG = getattr(termios, 'TIOCM_RNG', 0x080)
|
||||||
|
TIOCM_DSR = getattr(termios, 'TIOCM_DSR', 0x100)
|
||||||
|
TIOCM_CD = getattr(termios, 'TIOCM_CD', TIOCM_CAR)
|
||||||
|
TIOCM_RI = getattr(termios, 'TIOCM_RI', TIOCM_RNG)
|
||||||
|
# TIOCM_OUT1 = getattr(termios, 'TIOCM_OUT1', 0x2000)
|
||||||
|
# TIOCM_OUT2 = getattr(termios, 'TIOCM_OUT2', 0x4000)
|
||||||
|
if hasattr(termios, 'TIOCINQ'):
|
||||||
|
TIOCINQ = termios.TIOCINQ
|
||||||
|
else:
|
||||||
|
TIOCINQ = getattr(termios, 'FIONREAD', 0x541B)
|
||||||
|
TIOCOUTQ = getattr(termios, 'TIOCOUTQ', 0x5411)
|
||||||
|
|
||||||
|
TIOCM_zero_str = struct.pack('I', 0)
|
||||||
|
TIOCM_RTS_str = struct.pack('I', TIOCM_RTS)
|
||||||
|
TIOCM_DTR_str = struct.pack('I', TIOCM_DTR)
|
||||||
|
|
||||||
|
TIOCSBRK = getattr(termios, 'TIOCSBRK', 0x5427)
|
||||||
|
TIOCCBRK = getattr(termios, 'TIOCCBRK', 0x5428)
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(SerialBase, PlatformSpecific):
|
||||||
|
"""\
|
||||||
|
Serial port class POSIX implementation. Serial port configuration is
|
||||||
|
done with termios and fcntl. Runs on Linux and many other Un*x like
|
||||||
|
systems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""\
|
||||||
|
Open port with current settings. This may throw a SerialException
|
||||||
|
if the port cannot be opened."""
|
||||||
|
if self._port is None:
|
||||||
|
raise SerialException("Port must be configured before it can be used.")
|
||||||
|
if self.is_open:
|
||||||
|
raise SerialException("Port is already open.")
|
||||||
|
self.fd = None
|
||||||
|
# open
|
||||||
|
try:
|
||||||
|
self.fd = os.open(self.portstr, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
||||||
|
except OSError as msg:
|
||||||
|
self.fd = None
|
||||||
|
raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
|
||||||
|
#~ fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # set blocking
|
||||||
|
|
||||||
|
self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
|
||||||
|
self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._reconfigure_port(force_update=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self._dsrdtr:
|
||||||
|
self._update_dtr_state()
|
||||||
|
if not self._rtscts:
|
||||||
|
self._update_rts_state()
|
||||||
|
except IOError as e:
|
||||||
|
# ignore Invalid argument and Inappropriate ioctl
|
||||||
|
if e.errno not in (errno.EINVAL, errno.ENOTTY):
|
||||||
|
raise
|
||||||
|
|
||||||
|
self._reset_input_buffer()
|
||||||
|
|
||||||
|
self.pipe_abort_read_r, self.pipe_abort_read_w = os.pipe()
|
||||||
|
self.pipe_abort_write_r, self.pipe_abort_write_w = os.pipe()
|
||||||
|
fcntl.fcntl(self.pipe_abort_read_r, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||||
|
fcntl.fcntl(self.pipe_abort_write_r, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
os.close(self.fd)
|
||||||
|
except Exception:
|
||||||
|
# ignore any exception when closing the port
|
||||||
|
# also to keep original exception that happened when setting up
|
||||||
|
pass
|
||||||
|
self.fd = None
|
||||||
|
|
||||||
|
if self.pipe_abort_read_w is not None:
|
||||||
|
os.close(self.pipe_abort_read_w)
|
||||||
|
self.pipe_abort_read_w = None
|
||||||
|
if self.pipe_abort_read_r is not None:
|
||||||
|
os.close(self.pipe_abort_read_r)
|
||||||
|
self.pipe_abort_read_r = None
|
||||||
|
if self.pipe_abort_write_w is not None:
|
||||||
|
os.close(self.pipe_abort_write_w)
|
||||||
|
self.pipe_abort_write_w = None
|
||||||
|
if self.pipe_abort_write_r is not None:
|
||||||
|
os.close(self.pipe_abort_write_r)
|
||||||
|
self.pipe_abort_write_r = None
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.is_open = True
|
||||||
|
|
||||||
|
def _reconfigure_port(self, force_update=False):
|
||||||
|
"""Set communication parameters on opened port."""
|
||||||
|
if self.fd is None:
|
||||||
|
raise SerialException("Can only operate on a valid file descriptor")
|
||||||
|
|
||||||
|
# if exclusive lock is requested, create it before we modify anything else
|
||||||
|
if self._exclusive is not None:
|
||||||
|
if self._exclusive:
|
||||||
|
try:
|
||||||
|
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
except IOError as msg:
|
||||||
|
raise SerialException(msg.errno, "Could not exclusively lock port {}: {}".format(self._port, msg))
|
||||||
|
else:
|
||||||
|
fcntl.flock(self.fd, fcntl.LOCK_UN)
|
||||||
|
|
||||||
|
custom_baud = None
|
||||||
|
|
||||||
|
vmin = vtime = 0 # timeout is done via select
|
||||||
|
if self._inter_byte_timeout is not None:
|
||||||
|
vmin = 1
|
||||||
|
vtime = int(self._inter_byte_timeout * 10)
|
||||||
|
try:
|
||||||
|
orig_attr = termios.tcgetattr(self.fd)
|
||||||
|
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
|
||||||
|
except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
|
||||||
|
raise SerialException("Could not configure port: {}".format(msg))
|
||||||
|
# set up raw mode / no echo / binary
|
||||||
|
cflag |= (termios.CLOCAL | termios.CREAD)
|
||||||
|
lflag &= ~(termios.ICANON | termios.ECHO | termios.ECHOE |
|
||||||
|
termios.ECHOK | termios.ECHONL |
|
||||||
|
termios.ISIG | termios.IEXTEN) # |termios.ECHOPRT
|
||||||
|
for flag in ('ECHOCTL', 'ECHOKE'): # netbsd workaround for Erk
|
||||||
|
if hasattr(termios, flag):
|
||||||
|
lflag &= ~getattr(termios, flag)
|
||||||
|
|
||||||
|
oflag &= ~(termios.OPOST | termios.ONLCR | termios.OCRNL)
|
||||||
|
iflag &= ~(termios.INLCR | termios.IGNCR | termios.ICRNL | termios.IGNBRK)
|
||||||
|
if hasattr(termios, 'IUCLC'):
|
||||||
|
iflag &= ~termios.IUCLC
|
||||||
|
if hasattr(termios, 'PARMRK'):
|
||||||
|
iflag &= ~termios.PARMRK
|
||||||
|
|
||||||
|
# setup baud rate
|
||||||
|
try:
|
||||||
|
ispeed = ospeed = getattr(termios, 'B{}'.format(self._baudrate))
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
ispeed = ospeed = self.BAUDRATE_CONSTANTS[self._baudrate]
|
||||||
|
except KeyError:
|
||||||
|
#~ raise ValueError('Invalid baud rate: %r' % self._baudrate)
|
||||||
|
|
||||||
|
# See if BOTHER is defined for this platform; if it is, use
|
||||||
|
# this for a speed not defined in the baudrate constants list.
|
||||||
|
try:
|
||||||
|
ispeed = ospeed = BOTHER
|
||||||
|
except NameError:
|
||||||
|
# may need custom baud rate, it isn't in our list.
|
||||||
|
ispeed = ospeed = getattr(termios, 'B38400')
|
||||||
|
|
||||||
|
try:
|
||||||
|
custom_baud = int(self._baudrate) # store for later
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
|
||||||
|
else:
|
||||||
|
if custom_baud < 0:
|
||||||
|
raise ValueError('Invalid baud rate: {!r}'.format(self._baudrate))
|
||||||
|
|
||||||
|
# setup char len
|
||||||
|
cflag &= ~termios.CSIZE
|
||||||
|
if self._bytesize == 8:
|
||||||
|
cflag |= termios.CS8
|
||||||
|
elif self._bytesize == 7:
|
||||||
|
cflag |= termios.CS7
|
||||||
|
elif self._bytesize == 6:
|
||||||
|
cflag |= termios.CS6
|
||||||
|
elif self._bytesize == 5:
|
||||||
|
cflag |= termios.CS5
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
|
||||||
|
# setup stop bits
|
||||||
|
if self._stopbits == serial.STOPBITS_ONE:
|
||||||
|
cflag &= ~(termios.CSTOPB)
|
||||||
|
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||||
|
cflag |= (termios.CSTOPB) # XXX same as TWO.. there is no POSIX support for 1.5
|
||||||
|
elif self._stopbits == serial.STOPBITS_TWO:
|
||||||
|
cflag |= (termios.CSTOPB)
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
|
||||||
|
# setup parity
|
||||||
|
iflag &= ~(termios.INPCK | termios.ISTRIP)
|
||||||
|
if self._parity == serial.PARITY_NONE:
|
||||||
|
cflag &= ~(termios.PARENB | termios.PARODD | CMSPAR)
|
||||||
|
elif self._parity == serial.PARITY_EVEN:
|
||||||
|
cflag &= ~(termios.PARODD | CMSPAR)
|
||||||
|
cflag |= (termios.PARENB)
|
||||||
|
elif self._parity == serial.PARITY_ODD:
|
||||||
|
cflag &= ~CMSPAR
|
||||||
|
cflag |= (termios.PARENB | termios.PARODD)
|
||||||
|
elif self._parity == serial.PARITY_MARK and CMSPAR:
|
||||||
|
cflag |= (termios.PARENB | CMSPAR | termios.PARODD)
|
||||||
|
elif self._parity == serial.PARITY_SPACE and CMSPAR:
|
||||||
|
cflag |= (termios.PARENB | CMSPAR)
|
||||||
|
cflag &= ~(termios.PARODD)
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid parity: {!r}'.format(self._parity))
|
||||||
|
# setup flow control
|
||||||
|
# xonxoff
|
||||||
|
if hasattr(termios, 'IXANY'):
|
||||||
|
if self._xonxoff:
|
||||||
|
iflag |= (termios.IXON | termios.IXOFF) # |termios.IXANY)
|
||||||
|
else:
|
||||||
|
iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY)
|
||||||
|
else:
|
||||||
|
if self._xonxoff:
|
||||||
|
iflag |= (termios.IXON | termios.IXOFF)
|
||||||
|
else:
|
||||||
|
iflag &= ~(termios.IXON | termios.IXOFF)
|
||||||
|
# rtscts
|
||||||
|
if hasattr(termios, 'CRTSCTS'):
|
||||||
|
if self._rtscts:
|
||||||
|
cflag |= (termios.CRTSCTS)
|
||||||
|
else:
|
||||||
|
cflag &= ~(termios.CRTSCTS)
|
||||||
|
elif hasattr(termios, 'CNEW_RTSCTS'): # try it with alternate constant name
|
||||||
|
if self._rtscts:
|
||||||
|
cflag |= (termios.CNEW_RTSCTS)
|
||||||
|
else:
|
||||||
|
cflag &= ~(termios.CNEW_RTSCTS)
|
||||||
|
# XXX should there be a warning if setting up rtscts (and xonxoff etc) fails??
|
||||||
|
|
||||||
|
# buffer
|
||||||
|
# vmin "minimal number of characters to be read. 0 for non blocking"
|
||||||
|
if vmin < 0 or vmin > 255:
|
||||||
|
raise ValueError('Invalid vmin: {!r}'.format(vmin))
|
||||||
|
cc[termios.VMIN] = vmin
|
||||||
|
# vtime
|
||||||
|
if vtime < 0 or vtime > 255:
|
||||||
|
raise ValueError('Invalid vtime: {!r}'.format(vtime))
|
||||||
|
cc[termios.VTIME] = vtime
|
||||||
|
# activate settings
|
||||||
|
if force_update or [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] != orig_attr:
|
||||||
|
termios.tcsetattr(
|
||||||
|
self.fd,
|
||||||
|
termios.TCSANOW,
|
||||||
|
[iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
|
||||||
|
|
||||||
|
# apply custom baud rate, if any
|
||||||
|
if custom_baud is not None:
|
||||||
|
self._set_special_baudrate(custom_baud)
|
||||||
|
|
||||||
|
if self._rs485_mode is not None:
|
||||||
|
self._set_rs485_mode(self._rs485_mode)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close port"""
|
||||||
|
if self.is_open:
|
||||||
|
if self.fd is not None:
|
||||||
|
os.close(self.fd)
|
||||||
|
self.fd = None
|
||||||
|
os.close(self.pipe_abort_read_w)
|
||||||
|
os.close(self.pipe_abort_read_r)
|
||||||
|
os.close(self.pipe_abort_write_w)
|
||||||
|
os.close(self.pipe_abort_write_r)
|
||||||
|
self.pipe_abort_read_r, self.pipe_abort_read_w = None, None
|
||||||
|
self.pipe_abort_write_r, self.pipe_abort_write_w = None, None
|
||||||
|
self.is_open = False
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
"""Return the number of bytes currently in the input buffer."""
|
||||||
|
#~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
|
||||||
|
s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str)
|
||||||
|
return struct.unpack('I', s)[0]
|
||||||
|
|
||||||
|
# select based implementation, proved to work on many systems
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
read = bytearray()
|
||||||
|
timeout = Timeout(self._timeout)
|
||||||
|
while len(read) < size:
|
||||||
|
try:
|
||||||
|
ready, _, _ = select.select([self.fd, self.pipe_abort_read_r], [], [], timeout.time_left())
|
||||||
|
if self.pipe_abort_read_r in ready:
|
||||||
|
os.read(self.pipe_abort_read_r, 1000)
|
||||||
|
break
|
||||||
|
# If select was used with a timeout, and the timeout occurs, it
|
||||||
|
# returns with empty lists -> thus abort read operation.
|
||||||
|
# For timeout == 0 (non-blocking operation) also abort when
|
||||||
|
# there is nothing to read.
|
||||||
|
if not ready:
|
||||||
|
break # timeout
|
||||||
|
buf = os.read(self.fd, size - len(read))
|
||||||
|
except OSError as e:
|
||||||
|
# this is for Python 3.x where select.error is a subclass of
|
||||||
|
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||||
|
# https://www.python.org/dev/peps/pep-0475.
|
||||||
|
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('read failed: {}'.format(e))
|
||||||
|
except select.error as e:
|
||||||
|
# this is for Python 2.x
|
||||||
|
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||||
|
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||||
|
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('read failed: {}'.format(e))
|
||||||
|
else:
|
||||||
|
# read should always return some data as select reported it was
|
||||||
|
# ready to read when we get to this point.
|
||||||
|
if not buf:
|
||||||
|
# Disconnected devices, at least on Linux, show the
|
||||||
|
# behavior that they are always ready to read immediately
|
||||||
|
# but reading returns nothing.
|
||||||
|
raise SerialException(
|
||||||
|
'device reports readiness to read but returned no data '
|
||||||
|
'(device disconnected or multiple access on port?)')
|
||||||
|
read.extend(buf)
|
||||||
|
|
||||||
|
if timeout.expired():
|
||||||
|
break
|
||||||
|
return bytes(read)
|
||||||
|
|
||||||
|
def cancel_read(self):
|
||||||
|
if self.is_open:
|
||||||
|
os.write(self.pipe_abort_read_w, b"x")
|
||||||
|
|
||||||
|
def cancel_write(self):
|
||||||
|
if self.is_open:
|
||||||
|
os.write(self.pipe_abort_write_w, b"x")
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""Output the given byte string over the serial port."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
d = to_bytes(data)
|
||||||
|
tx_len = length = len(d)
|
||||||
|
timeout = Timeout(self._write_timeout)
|
||||||
|
while tx_len > 0:
|
||||||
|
try:
|
||||||
|
n = os.write(self.fd, d)
|
||||||
|
if timeout.is_non_blocking:
|
||||||
|
# Zero timeout indicates non-blocking - simply return the
|
||||||
|
# number of bytes of data actually written
|
||||||
|
return n
|
||||||
|
elif not timeout.is_infinite:
|
||||||
|
# when timeout is set, use select to wait for being ready
|
||||||
|
# with the time left as timeout
|
||||||
|
if timeout.expired():
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], timeout.time_left())
|
||||||
|
if abort:
|
||||||
|
os.read(self.pipe_abort_write_r, 1000)
|
||||||
|
break
|
||||||
|
if not ready:
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
else:
|
||||||
|
assert timeout.time_left() is None
|
||||||
|
# wait for write operation
|
||||||
|
abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], None)
|
||||||
|
if abort:
|
||||||
|
os.read(self.pipe_abort_write_r, 1)
|
||||||
|
break
|
||||||
|
if not ready:
|
||||||
|
raise SerialException('write failed (select)')
|
||||||
|
d = d[n:]
|
||||||
|
tx_len -= n
|
||||||
|
except SerialException:
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
# this is for Python 3.x where select.error is a subclass of
|
||||||
|
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||||
|
# https://www.python.org/dev/peps/pep-0475.
|
||||||
|
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('write failed: {}'.format(e))
|
||||||
|
except select.error as e:
|
||||||
|
# this is for Python 2.x
|
||||||
|
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||||
|
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||||
|
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('write failed: {}'.format(e))
|
||||||
|
if not timeout.is_non_blocking and timeout.expired():
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
return length - len(d)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""\
|
||||||
|
Flush of file like objects. In this case, wait until all data
|
||||||
|
is written.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
termios.tcdrain(self.fd)
|
||||||
|
|
||||||
|
def _reset_input_buffer(self):
|
||||||
|
"""Clear input buffer, discarding all that is in the buffer."""
|
||||||
|
termios.tcflush(self.fd, termios.TCIFLUSH)
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
"""Clear input buffer, discarding all that is in the buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._reset_input_buffer()
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
"""\
|
||||||
|
Clear output buffer, aborting the current output and discarding all
|
||||||
|
that is in the buffer.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
termios.tcflush(self.fd, termios.TCOFLUSH)
|
||||||
|
|
||||||
|
def send_break(self, duration=0.25):
|
||||||
|
"""\
|
||||||
|
Send break condition. Timed, returns to idle state after given
|
||||||
|
duration.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
termios.tcsendbreak(self.fd, int(duration / 0.25))
|
||||||
|
|
||||||
|
def _update_rts_state(self):
|
||||||
|
"""Set terminal status line: Request To Send"""
|
||||||
|
if self._rts_state:
|
||||||
|
fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_RTS_str)
|
||||||
|
else:
|
||||||
|
fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_RTS_str)
|
||||||
|
|
||||||
|
def _update_dtr_state(self):
|
||||||
|
"""Set terminal status line: Data Terminal Ready"""
|
||||||
|
if self._dtr_state:
|
||||||
|
fcntl.ioctl(self.fd, TIOCMBIS, TIOCM_DTR_str)
|
||||||
|
else:
|
||||||
|
fcntl.ioctl(self.fd, TIOCMBIC, TIOCM_DTR_str)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cts(self):
|
||||||
|
"""Read terminal status line: Clear To Send"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||||
|
return struct.unpack('I', s)[0] & TIOCM_CTS != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsr(self):
|
||||||
|
"""Read terminal status line: Data Set Ready"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||||
|
return struct.unpack('I', s)[0] & TIOCM_DSR != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ri(self):
|
||||||
|
"""Read terminal status line: Ring Indicator"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||||
|
return struct.unpack('I', s)[0] & TIOCM_RI != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cd(self):
|
||||||
|
"""Read terminal status line: Carrier Detect"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
|
||||||
|
return struct.unpack('I', s)[0] & TIOCM_CD != 0
|
||||||
|
|
||||||
|
# - - platform specific - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def out_waiting(self):
|
||||||
|
"""Return the number of bytes currently in the output buffer."""
|
||||||
|
#~ s = fcntl.ioctl(self.fd, termios.FIONREAD, TIOCM_zero_str)
|
||||||
|
s = fcntl.ioctl(self.fd, TIOCOUTQ, TIOCM_zero_str)
|
||||||
|
return struct.unpack('I', s)[0]
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
"""\
|
||||||
|
For easier use of the serial port instance with select.
|
||||||
|
WARNING: this function is not portable to different platforms!
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
return self.fd
|
||||||
|
|
||||||
|
def set_input_flow_control(self, enable=True):
|
||||||
|
"""\
|
||||||
|
Manually control flow - when software flow control is enabled.
|
||||||
|
This will send XON (true) or XOFF (false) to the other device.
|
||||||
|
WARNING: this function is not portable to different platforms!
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if enable:
|
||||||
|
termios.tcflow(self.fd, termios.TCION)
|
||||||
|
else:
|
||||||
|
termios.tcflow(self.fd, termios.TCIOFF)
|
||||||
|
|
||||||
|
def set_output_flow_control(self, enable=True):
|
||||||
|
"""\
|
||||||
|
Manually control flow of outgoing data - when hardware or software flow
|
||||||
|
control is enabled.
|
||||||
|
WARNING: this function is not portable to different platforms!
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if enable:
|
||||||
|
termios.tcflow(self.fd, termios.TCOON)
|
||||||
|
else:
|
||||||
|
termios.tcflow(self.fd, termios.TCOOFF)
|
||||||
|
|
||||||
|
def nonblocking(self):
|
||||||
|
"""DEPRECATED - has no use"""
|
||||||
|
import warnings
|
||||||
|
warnings.warn("nonblocking() has no effect, already nonblocking", DeprecationWarning)
|
||||||
|
|
||||||
|
|
||||||
|
class PosixPollSerial(Serial):
|
||||||
|
"""\
|
||||||
|
Poll based read implementation. Not all systems support poll properly.
|
||||||
|
However this one has better handling of errors, such as a device
|
||||||
|
disconnecting while it's in use (e.g. USB-serial unplugged).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
read = bytearray()
|
||||||
|
timeout = Timeout(self._timeout)
|
||||||
|
poll = select.poll()
|
||||||
|
poll.register(self.fd, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
|
||||||
|
poll.register(self.pipe_abort_read_r, select.POLLIN | select.POLLERR | select.POLLHUP | select.POLLNVAL)
|
||||||
|
if size > 0:
|
||||||
|
while len(read) < size:
|
||||||
|
# print "\tread(): size",size, "have", len(read) #debug
|
||||||
|
# wait until device becomes ready to read (or something fails)
|
||||||
|
for fd, event in poll.poll(None if timeout.is_infinite else (timeout.time_left() * 1000)):
|
||||||
|
if fd == self.pipe_abort_read_r:
|
||||||
|
break
|
||||||
|
if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
|
||||||
|
raise SerialException('device reports error (poll)')
|
||||||
|
# we don't care if it is select.POLLIN or timeout, that's
|
||||||
|
# handled below
|
||||||
|
if fd == self.pipe_abort_read_r:
|
||||||
|
os.read(self.pipe_abort_read_r, 1000)
|
||||||
|
break
|
||||||
|
buf = os.read(self.fd, size - len(read))
|
||||||
|
read.extend(buf)
|
||||||
|
if timeout.expired() \
|
||||||
|
or (self._inter_byte_timeout is not None and self._inter_byte_timeout > 0) and not buf:
|
||||||
|
break # early abort on timeout
|
||||||
|
return bytes(read)
|
||||||
|
|
||||||
|
|
||||||
|
class VTIMESerial(Serial):
|
||||||
|
"""\
|
||||||
|
Implement timeout using vtime of tty device instead of using select.
|
||||||
|
This means that no inter character timeout can be specified and that
|
||||||
|
the error handling is degraded.
|
||||||
|
|
||||||
|
Overall timeout is disabled when inter-character timeout is used.
|
||||||
|
|
||||||
|
Note that this implementation does NOT support cancel_read(), it will
|
||||||
|
just ignore that.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _reconfigure_port(self, force_update=True):
|
||||||
|
"""Set communication parameters on opened port."""
|
||||||
|
super(VTIMESerial, self)._reconfigure_port()
|
||||||
|
fcntl.fcntl(self.fd, fcntl.F_SETFL, 0) # clear O_NONBLOCK
|
||||||
|
|
||||||
|
if self._inter_byte_timeout is not None:
|
||||||
|
vmin = 1
|
||||||
|
vtime = int(self._inter_byte_timeout * 10)
|
||||||
|
elif self._timeout is None:
|
||||||
|
vmin = 1
|
||||||
|
vtime = 0
|
||||||
|
else:
|
||||||
|
vmin = 0
|
||||||
|
vtime = int(self._timeout * 10)
|
||||||
|
try:
|
||||||
|
orig_attr = termios.tcgetattr(self.fd)
|
||||||
|
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = orig_attr
|
||||||
|
except termios.error as msg: # if a port is nonexistent but has a /dev file, it'll fail here
|
||||||
|
raise serial.SerialException("Could not configure port: {}".format(msg))
|
||||||
|
|
||||||
|
if vtime < 0 or vtime > 255:
|
||||||
|
raise ValueError('Invalid vtime: {!r}'.format(vtime))
|
||||||
|
cc[termios.VTIME] = vtime
|
||||||
|
cc[termios.VMIN] = vmin
|
||||||
|
|
||||||
|
termios.tcsetattr(
|
||||||
|
self.fd,
|
||||||
|
termios.TCSANOW,
|
||||||
|
[iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
read = bytearray()
|
||||||
|
while len(read) < size:
|
||||||
|
buf = os.read(self.fd, size - len(read))
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
read.extend(buf)
|
||||||
|
return bytes(read)
|
||||||
|
|
||||||
|
# hack to make hasattr return false
|
||||||
|
cancel_read = property()
|
||||||
697
deps/serial/serialutil.py
vendored
Normal file
697
deps/serial/serialutil.py
vendored
Normal file
@@ -0,0 +1,697 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# Base class and support functions used by various backends.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import io
|
||||||
|
import time
|
||||||
|
|
||||||
|
# ``memoryview`` was introduced in Python 2.7 and ``bytes(some_memoryview)``
|
||||||
|
# isn't returning the contents (very unfortunate). Therefore we need special
|
||||||
|
# cases and test for it. Ensure that there is a ``memoryview`` object for older
|
||||||
|
# Python versions. This is easier than making every test dependent on its
|
||||||
|
# existence.
|
||||||
|
try:
|
||||||
|
memoryview
|
||||||
|
except (NameError, AttributeError):
|
||||||
|
# implementation does not matter as we do not really use it.
|
||||||
|
# it just must not inherit from something else we might care for.
|
||||||
|
class memoryview(object): # pylint: disable=redefined-builtin,invalid-name
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
unicode
|
||||||
|
except (NameError, AttributeError):
|
||||||
|
unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||||
|
|
||||||
|
try:
|
||||||
|
basestring
|
||||||
|
except (NameError, AttributeError):
|
||||||
|
basestring = (str,) # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
# "for byte in data" fails for python3 as it returns ints instead of bytes
|
||||||
|
def iterbytes(b):
|
||||||
|
"""Iterate over bytes, returning bytes instead of ints (python3)"""
|
||||||
|
if isinstance(b, memoryview):
|
||||||
|
b = b.tobytes()
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
a = b[i:i + 1]
|
||||||
|
i += 1
|
||||||
|
if a:
|
||||||
|
yield a
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# all Python versions prior 3.x convert ``str([17])`` to '[17]' instead of '\x11'
|
||||||
|
# so a simple ``bytes(sequence)`` doesn't work for all versions
|
||||||
|
def to_bytes(seq):
|
||||||
|
"""convert a sequence to a bytes type"""
|
||||||
|
if isinstance(seq, bytes):
|
||||||
|
return seq
|
||||||
|
elif isinstance(seq, bytearray):
|
||||||
|
return bytes(seq)
|
||||||
|
elif isinstance(seq, memoryview):
|
||||||
|
return seq.tobytes()
|
||||||
|
elif isinstance(seq, unicode):
|
||||||
|
raise TypeError('unicode strings are not supported, please encode to bytes: {!r}'.format(seq))
|
||||||
|
else:
|
||||||
|
# handle list of integers and bytes (one or more items) for Python 2 and 3
|
||||||
|
return bytes(bytearray(seq))
|
||||||
|
|
||||||
|
|
||||||
|
# create control bytes
|
||||||
|
XON = to_bytes([17])
|
||||||
|
XOFF = to_bytes([19])
|
||||||
|
|
||||||
|
CR = to_bytes([13])
|
||||||
|
LF = to_bytes([10])
|
||||||
|
|
||||||
|
|
||||||
|
PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE = 'N', 'E', 'O', 'M', 'S'
|
||||||
|
STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
|
||||||
|
FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS = (5, 6, 7, 8)
|
||||||
|
|
||||||
|
PARITY_NAMES = {
|
||||||
|
PARITY_NONE: 'None',
|
||||||
|
PARITY_EVEN: 'Even',
|
||||||
|
PARITY_ODD: 'Odd',
|
||||||
|
PARITY_MARK: 'Mark',
|
||||||
|
PARITY_SPACE: 'Space',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SerialException(IOError):
|
||||||
|
"""Base class for serial port related exceptions."""
|
||||||
|
|
||||||
|
|
||||||
|
class SerialTimeoutException(SerialException):
|
||||||
|
"""Write timeouts give an exception"""
|
||||||
|
|
||||||
|
|
||||||
|
class PortNotOpenError(SerialException):
|
||||||
|
"""Port is not open"""
|
||||||
|
def __init__(self):
|
||||||
|
super(PortNotOpenError, self).__init__('Attempting to use a port that is not open')
|
||||||
|
|
||||||
|
|
||||||
|
class Timeout(object):
|
||||||
|
"""\
|
||||||
|
Abstraction for timeout operations. Using time.monotonic() if available
|
||||||
|
or time.time() in all other cases.
|
||||||
|
|
||||||
|
The class can also be initialized with 0 or None, in order to support
|
||||||
|
non-blocking and fully blocking I/O operations. The attributes
|
||||||
|
is_non_blocking and is_infinite are set accordingly.
|
||||||
|
"""
|
||||||
|
if hasattr(time, 'monotonic'):
|
||||||
|
# Timeout implementation with time.monotonic(). This function is only
|
||||||
|
# supported by Python 3.3 and above. It returns a time in seconds
|
||||||
|
# (float) just as time.time(), but is not affected by system clock
|
||||||
|
# adjustments.
|
||||||
|
TIME = time.monotonic
|
||||||
|
else:
|
||||||
|
# Timeout implementation with time.time(). This is compatible with all
|
||||||
|
# Python versions but has issues if the clock is adjusted while the
|
||||||
|
# timeout is running.
|
||||||
|
TIME = time.time
|
||||||
|
|
||||||
|
def __init__(self, duration):
|
||||||
|
"""Initialize a timeout with given duration"""
|
||||||
|
self.is_infinite = (duration is None)
|
||||||
|
self.is_non_blocking = (duration == 0)
|
||||||
|
self.duration = duration
|
||||||
|
if duration is not None:
|
||||||
|
self.target_time = self.TIME() + duration
|
||||||
|
else:
|
||||||
|
self.target_time = None
|
||||||
|
|
||||||
|
def expired(self):
|
||||||
|
"""Return a boolean, telling if the timeout has expired"""
|
||||||
|
return self.target_time is not None and self.time_left() <= 0
|
||||||
|
|
||||||
|
def time_left(self):
|
||||||
|
"""Return how many seconds are left until the timeout expires"""
|
||||||
|
if self.is_non_blocking:
|
||||||
|
return 0
|
||||||
|
elif self.is_infinite:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
delta = self.target_time - self.TIME()
|
||||||
|
if delta > self.duration:
|
||||||
|
# clock jumped, recalculate
|
||||||
|
self.target_time = self.TIME() + self.duration
|
||||||
|
return self.duration
|
||||||
|
else:
|
||||||
|
return max(0, delta)
|
||||||
|
|
||||||
|
def restart(self, duration):
|
||||||
|
"""\
|
||||||
|
Restart a timeout, only supported if a timeout was already set up
|
||||||
|
before.
|
||||||
|
"""
|
||||||
|
self.duration = duration
|
||||||
|
self.target_time = self.TIME() + duration
|
||||||
|
|
||||||
|
|
||||||
|
class SerialBase(io.RawIOBase):
|
||||||
|
"""\
|
||||||
|
Serial port base class. Provides __init__ function and properties to
|
||||||
|
get/set port settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# default values, may be overridden in subclasses that do not support all values
|
||||||
|
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||||
|
9600, 19200, 38400, 57600, 115200, 230400, 460800, 500000,
|
||||||
|
576000, 921600, 1000000, 1152000, 1500000, 2000000, 2500000,
|
||||||
|
3000000, 3500000, 4000000)
|
||||||
|
BYTESIZES = (FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS)
|
||||||
|
PARITIES = (PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE)
|
||||||
|
STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
port=None,
|
||||||
|
baudrate=9600,
|
||||||
|
bytesize=EIGHTBITS,
|
||||||
|
parity=PARITY_NONE,
|
||||||
|
stopbits=STOPBITS_ONE,
|
||||||
|
timeout=None,
|
||||||
|
xonxoff=False,
|
||||||
|
rtscts=False,
|
||||||
|
write_timeout=None,
|
||||||
|
dsrdtr=False,
|
||||||
|
inter_byte_timeout=None,
|
||||||
|
exclusive=None,
|
||||||
|
**kwargs):
|
||||||
|
"""\
|
||||||
|
Initialize comm port object. If a "port" is given, then the port will be
|
||||||
|
opened immediately. Otherwise a Serial port object in closed state
|
||||||
|
is returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.is_open = False
|
||||||
|
self.portstr = None
|
||||||
|
self.name = None
|
||||||
|
# correct values are assigned below through properties
|
||||||
|
self._port = None
|
||||||
|
self._baudrate = None
|
||||||
|
self._bytesize = None
|
||||||
|
self._parity = None
|
||||||
|
self._stopbits = None
|
||||||
|
self._timeout = None
|
||||||
|
self._write_timeout = None
|
||||||
|
self._xonxoff = None
|
||||||
|
self._rtscts = None
|
||||||
|
self._dsrdtr = None
|
||||||
|
self._inter_byte_timeout = None
|
||||||
|
self._rs485_mode = None # disabled by default
|
||||||
|
self._rts_state = True
|
||||||
|
self._dtr_state = True
|
||||||
|
self._break_state = False
|
||||||
|
self._exclusive = None
|
||||||
|
|
||||||
|
# assign values using get/set methods using the properties feature
|
||||||
|
self.port = port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.bytesize = bytesize
|
||||||
|
self.parity = parity
|
||||||
|
self.stopbits = stopbits
|
||||||
|
self.timeout = timeout
|
||||||
|
self.write_timeout = write_timeout
|
||||||
|
self.xonxoff = xonxoff
|
||||||
|
self.rtscts = rtscts
|
||||||
|
self.dsrdtr = dsrdtr
|
||||||
|
self.inter_byte_timeout = inter_byte_timeout
|
||||||
|
self.exclusive = exclusive
|
||||||
|
|
||||||
|
# watch for backward compatible kwargs
|
||||||
|
if 'writeTimeout' in kwargs:
|
||||||
|
self.write_timeout = kwargs.pop('writeTimeout')
|
||||||
|
if 'interCharTimeout' in kwargs:
|
||||||
|
self.inter_byte_timeout = kwargs.pop('interCharTimeout')
|
||||||
|
if kwargs:
|
||||||
|
raise ValueError('unexpected keyword arguments: {!r}'.format(kwargs))
|
||||||
|
|
||||||
|
if port is not None:
|
||||||
|
self.open()
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
# to be implemented by subclasses:
|
||||||
|
# def open(self):
|
||||||
|
# def close(self):
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self):
|
||||||
|
"""\
|
||||||
|
Get the current port setting. The value that was passed on init or using
|
||||||
|
setPort() is passed back.
|
||||||
|
"""
|
||||||
|
return self._port
|
||||||
|
|
||||||
|
@port.setter
|
||||||
|
def port(self, port):
|
||||||
|
"""\
|
||||||
|
Change the port.
|
||||||
|
"""
|
||||||
|
if port is not None and not isinstance(port, basestring):
|
||||||
|
raise ValueError('"port" must be None or a string, not {}'.format(type(port)))
|
||||||
|
was_open = self.is_open
|
||||||
|
if was_open:
|
||||||
|
self.close()
|
||||||
|
self.portstr = port
|
||||||
|
self._port = port
|
||||||
|
self.name = self.portstr
|
||||||
|
if was_open:
|
||||||
|
self.open()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def baudrate(self):
|
||||||
|
"""Get the current baud rate setting."""
|
||||||
|
return self._baudrate
|
||||||
|
|
||||||
|
@baudrate.setter
|
||||||
|
def baudrate(self, baudrate):
|
||||||
|
"""\
|
||||||
|
Change baud rate. It raises a ValueError if the port is open and the
|
||||||
|
baud rate is not possible. If the port is closed, then the value is
|
||||||
|
accepted and the exception is raised when the port is opened.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
b = int(baudrate)
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
|
||||||
|
else:
|
||||||
|
if b < 0:
|
||||||
|
raise ValueError("Not a valid baudrate: {!r}".format(baudrate))
|
||||||
|
self._baudrate = b
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bytesize(self):
|
||||||
|
"""Get the current byte size setting."""
|
||||||
|
return self._bytesize
|
||||||
|
|
||||||
|
@bytesize.setter
|
||||||
|
def bytesize(self, bytesize):
|
||||||
|
"""Change byte size."""
|
||||||
|
if bytesize not in self.BYTESIZES:
|
||||||
|
raise ValueError("Not a valid byte size: {!r}".format(bytesize))
|
||||||
|
self._bytesize = bytesize
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exclusive(self):
|
||||||
|
"""Get the current exclusive access setting."""
|
||||||
|
return self._exclusive
|
||||||
|
|
||||||
|
@exclusive.setter
|
||||||
|
def exclusive(self, exclusive):
|
||||||
|
"""Change the exclusive access setting."""
|
||||||
|
self._exclusive = exclusive
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parity(self):
|
||||||
|
"""Get the current parity setting."""
|
||||||
|
return self._parity
|
||||||
|
|
||||||
|
@parity.setter
|
||||||
|
def parity(self, parity):
|
||||||
|
"""Change parity setting."""
|
||||||
|
if parity not in self.PARITIES:
|
||||||
|
raise ValueError("Not a valid parity: {!r}".format(parity))
|
||||||
|
self._parity = parity
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stopbits(self):
|
||||||
|
"""Get the current stop bits setting."""
|
||||||
|
return self._stopbits
|
||||||
|
|
||||||
|
@stopbits.setter
|
||||||
|
def stopbits(self, stopbits):
|
||||||
|
"""Change stop bits size."""
|
||||||
|
if stopbits not in self.STOPBITS:
|
||||||
|
raise ValueError("Not a valid stop bit size: {!r}".format(stopbits))
|
||||||
|
self._stopbits = stopbits
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timeout(self):
|
||||||
|
"""Get the current timeout setting."""
|
||||||
|
return self._timeout
|
||||||
|
|
||||||
|
@timeout.setter
|
||||||
|
def timeout(self, timeout):
|
||||||
|
"""Change timeout setting."""
|
||||||
|
if timeout is not None:
|
||||||
|
try:
|
||||||
|
timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||||
|
if timeout < 0:
|
||||||
|
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||||
|
self._timeout = timeout
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def write_timeout(self):
|
||||||
|
"""Get the current timeout setting."""
|
||||||
|
return self._write_timeout
|
||||||
|
|
||||||
|
@write_timeout.setter
|
||||||
|
def write_timeout(self, timeout):
|
||||||
|
"""Change timeout setting."""
|
||||||
|
if timeout is not None:
|
||||||
|
if timeout < 0:
|
||||||
|
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||||
|
try:
|
||||||
|
timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError("Not a valid timeout: {!r}".format(timeout))
|
||||||
|
|
||||||
|
self._write_timeout = timeout
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inter_byte_timeout(self):
|
||||||
|
"""Get the current inter-character timeout setting."""
|
||||||
|
return self._inter_byte_timeout
|
||||||
|
|
||||||
|
@inter_byte_timeout.setter
|
||||||
|
def inter_byte_timeout(self, ic_timeout):
|
||||||
|
"""Change inter-byte timeout setting."""
|
||||||
|
if ic_timeout is not None:
|
||||||
|
if ic_timeout < 0:
|
||||||
|
raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
|
||||||
|
try:
|
||||||
|
ic_timeout + 1 # test if it's a number, will throw a TypeError if not...
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError("Not a valid timeout: {!r}".format(ic_timeout))
|
||||||
|
|
||||||
|
self._inter_byte_timeout = ic_timeout
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def xonxoff(self):
|
||||||
|
"""Get the current XON/XOFF setting."""
|
||||||
|
return self._xonxoff
|
||||||
|
|
||||||
|
@xonxoff.setter
|
||||||
|
def xonxoff(self, xonxoff):
|
||||||
|
"""Change XON/XOFF setting."""
|
||||||
|
self._xonxoff = xonxoff
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rtscts(self):
|
||||||
|
"""Get the current RTS/CTS flow control setting."""
|
||||||
|
return self._rtscts
|
||||||
|
|
||||||
|
@rtscts.setter
|
||||||
|
def rtscts(self, rtscts):
|
||||||
|
"""Change RTS/CTS flow control setting."""
|
||||||
|
self._rtscts = rtscts
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsrdtr(self):
|
||||||
|
"""Get the current DSR/DTR flow control setting."""
|
||||||
|
return self._dsrdtr
|
||||||
|
|
||||||
|
@dsrdtr.setter
|
||||||
|
def dsrdtr(self, dsrdtr=None):
|
||||||
|
"""Change DsrDtr flow control setting."""
|
||||||
|
if dsrdtr is None:
|
||||||
|
# if not set, keep backwards compatibility and follow rtscts setting
|
||||||
|
self._dsrdtr = self._rtscts
|
||||||
|
else:
|
||||||
|
# if defined independently, follow its value
|
||||||
|
self._dsrdtr = dsrdtr
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rts(self):
|
||||||
|
return self._rts_state
|
||||||
|
|
||||||
|
@rts.setter
|
||||||
|
def rts(self, value):
|
||||||
|
self._rts_state = value
|
||||||
|
if self.is_open:
|
||||||
|
self._update_rts_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dtr(self):
|
||||||
|
return self._dtr_state
|
||||||
|
|
||||||
|
@dtr.setter
|
||||||
|
def dtr(self, value):
|
||||||
|
self._dtr_state = value
|
||||||
|
if self.is_open:
|
||||||
|
self._update_dtr_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def break_condition(self):
|
||||||
|
return self._break_state
|
||||||
|
|
||||||
|
@break_condition.setter
|
||||||
|
def break_condition(self, value):
|
||||||
|
self._break_state = value
|
||||||
|
if self.is_open:
|
||||||
|
self._update_break_state()
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# functions useful for RS-485 adapters
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rs485_mode(self):
|
||||||
|
"""\
|
||||||
|
Enable RS485 mode and apply new settings, set to None to disable.
|
||||||
|
See serial.rs485.RS485Settings for more info about the value.
|
||||||
|
"""
|
||||||
|
return self._rs485_mode
|
||||||
|
|
||||||
|
@rs485_mode.setter
|
||||||
|
def rs485_mode(self, rs485_settings):
|
||||||
|
self._rs485_mode = rs485_settings
|
||||||
|
if self.is_open:
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
_SAVED_SETTINGS = ('baudrate', 'bytesize', 'parity', 'stopbits', 'xonxoff',
|
||||||
|
'dsrdtr', 'rtscts', 'timeout', 'write_timeout',
|
||||||
|
'inter_byte_timeout')
|
||||||
|
|
||||||
|
def get_settings(self):
|
||||||
|
"""\
|
||||||
|
Get current port settings as a dictionary. For use with
|
||||||
|
apply_settings().
|
||||||
|
"""
|
||||||
|
return dict([(key, getattr(self, '_' + key)) for key in self._SAVED_SETTINGS])
|
||||||
|
|
||||||
|
def apply_settings(self, d):
|
||||||
|
"""\
|
||||||
|
Apply stored settings from a dictionary returned from
|
||||||
|
get_settings(). It's allowed to delete keys from the dictionary. These
|
||||||
|
values will simply left unchanged.
|
||||||
|
"""
|
||||||
|
for key in self._SAVED_SETTINGS:
|
||||||
|
if key in d and d[key] != getattr(self, '_' + key): # check against internal "_" value
|
||||||
|
setattr(self, key, d[key]) # set non "_" value to use properties write function
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""String representation of the current port settings and its state."""
|
||||||
|
return '{name}<id=0x{id:x}, open={p.is_open}>(port={p.portstr!r}, ' \
|
||||||
|
'baudrate={p.baudrate!r}, bytesize={p.bytesize!r}, parity={p.parity!r}, ' \
|
||||||
|
'stopbits={p.stopbits!r}, timeout={p.timeout!r}, xonxoff={p.xonxoff!r}, ' \
|
||||||
|
'rtscts={p.rtscts!r}, dsrdtr={p.dsrdtr!r})'.format(
|
||||||
|
name=self.__class__.__name__, id=id(self), p=self)
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# compatibility with io library
|
||||||
|
# pylint: disable=invalid-name,missing-docstring
|
||||||
|
|
||||||
|
def readable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def writable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def seekable(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def readinto(self, b):
|
||||||
|
data = self.read(len(b))
|
||||||
|
n = len(data)
|
||||||
|
try:
|
||||||
|
b[:n] = data
|
||||||
|
except TypeError as err:
|
||||||
|
import array
|
||||||
|
if not isinstance(b, array.array):
|
||||||
|
raise err
|
||||||
|
b[:n] = array.array('b', data)
|
||||||
|
return n
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# context manager
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
if self._port is not None and not self.is_open:
|
||||||
|
self.open()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args, **kwargs):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
def send_break(self, duration=0.25):
|
||||||
|
"""\
|
||||||
|
Send break condition. Timed, returns to idle state after given
|
||||||
|
duration.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self.break_condition = True
|
||||||
|
time.sleep(duration)
|
||||||
|
self.break_condition = False
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# backwards compatibility / deprecated functions
|
||||||
|
|
||||||
|
def flushInput(self):
|
||||||
|
self.reset_input_buffer()
|
||||||
|
|
||||||
|
def flushOutput(self):
|
||||||
|
self.reset_output_buffer()
|
||||||
|
|
||||||
|
def inWaiting(self):
|
||||||
|
return self.in_waiting
|
||||||
|
|
||||||
|
def sendBreak(self, duration=0.25):
|
||||||
|
self.send_break(duration)
|
||||||
|
|
||||||
|
def setRTS(self, value=1):
|
||||||
|
self.rts = value
|
||||||
|
|
||||||
|
def setDTR(self, value=1):
|
||||||
|
self.dtr = value
|
||||||
|
|
||||||
|
def getCTS(self):
|
||||||
|
return self.cts
|
||||||
|
|
||||||
|
def getDSR(self):
|
||||||
|
return self.dsr
|
||||||
|
|
||||||
|
def getRI(self):
|
||||||
|
return self.ri
|
||||||
|
|
||||||
|
def getCD(self):
|
||||||
|
return self.cd
|
||||||
|
|
||||||
|
def setPort(self, port):
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
@property
|
||||||
|
def writeTimeout(self):
|
||||||
|
return self.write_timeout
|
||||||
|
|
||||||
|
@writeTimeout.setter
|
||||||
|
def writeTimeout(self, timeout):
|
||||||
|
self.write_timeout = timeout
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interCharTimeout(self):
|
||||||
|
return self.inter_byte_timeout
|
||||||
|
|
||||||
|
@interCharTimeout.setter
|
||||||
|
def interCharTimeout(self, interCharTimeout):
|
||||||
|
self.inter_byte_timeout = interCharTimeout
|
||||||
|
|
||||||
|
def getSettingsDict(self):
|
||||||
|
return self.get_settings()
|
||||||
|
|
||||||
|
def applySettingsDict(self, d):
|
||||||
|
self.apply_settings(d)
|
||||||
|
|
||||||
|
def isOpen(self):
|
||||||
|
return self.is_open
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# additional functionality
|
||||||
|
|
||||||
|
def read_all(self):
|
||||||
|
"""\
|
||||||
|
Read all bytes currently available in the buffer of the OS.
|
||||||
|
"""
|
||||||
|
return self.read(self.in_waiting)
|
||||||
|
|
||||||
|
def read_until(self, expected=LF, size=None):
|
||||||
|
"""\
|
||||||
|
Read until an expected sequence is found ('\n' by default), the size
|
||||||
|
is exceeded or until timeout occurs.
|
||||||
|
"""
|
||||||
|
lenterm = len(expected)
|
||||||
|
line = bytearray()
|
||||||
|
timeout = Timeout(self._timeout)
|
||||||
|
while True:
|
||||||
|
c = self.read(1)
|
||||||
|
if c:
|
||||||
|
line += c
|
||||||
|
if line[-lenterm:] == expected:
|
||||||
|
break
|
||||||
|
if size is not None and len(line) >= size:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if timeout.expired():
|
||||||
|
break
|
||||||
|
return bytes(line)
|
||||||
|
|
||||||
|
def iread_until(self, *args, **kwargs):
|
||||||
|
"""\
|
||||||
|
Read lines, implemented as generator. It will raise StopIteration on
|
||||||
|
timeout (empty read).
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
line = self.read_until(*args, **kwargs)
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
yield line
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
s = SerialBase()
|
||||||
|
sys.stdout.write('port name: {}\n'.format(s.name))
|
||||||
|
sys.stdout.write('baud rates: {}\n'.format(s.BAUDRATES))
|
||||||
|
sys.stdout.write('byte sizes: {}\n'.format(s.BYTESIZES))
|
||||||
|
sys.stdout.write('parities: {}\n'.format(s.PARITIES))
|
||||||
|
sys.stdout.write('stop bits: {}\n'.format(s.STOPBITS))
|
||||||
|
sys.stdout.write('{}\n'.format(s))
|
||||||
477
deps/serial/serialwin32.py
vendored
Normal file
477
deps/serial/serialwin32.py
vendored
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# backend for Windows ("win32" incl. 32/64 bit support)
|
||||||
|
#
|
||||||
|
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
#
|
||||||
|
# Initial patch to use ctypes by Giovanni Bajo <rasky@develer.com>
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name,too-few-public-methods
|
||||||
|
import ctypes
|
||||||
|
import time
|
||||||
|
from serial import win32
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(SerialBase):
|
||||||
|
"""Serial port implementation for Win32 based on ctypes."""
|
||||||
|
|
||||||
|
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||||
|
9600, 19200, 38400, 57600, 115200)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._port_handle = None
|
||||||
|
self._overlapped_read = None
|
||||||
|
self._overlapped_write = None
|
||||||
|
super(Serial, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""\
|
||||||
|
Open port with current settings. This may throw a SerialException
|
||||||
|
if the port cannot be opened.
|
||||||
|
"""
|
||||||
|
if self._port is None:
|
||||||
|
raise SerialException("Port must be configured before it can be used.")
|
||||||
|
if self.is_open:
|
||||||
|
raise SerialException("Port is already open.")
|
||||||
|
# the "\\.\COMx" format is required for devices other than COM1-COM8
|
||||||
|
# not all versions of windows seem to support this properly
|
||||||
|
# so that the first few ports are used with the DOS device name
|
||||||
|
port = self.name
|
||||||
|
try:
|
||||||
|
if port.upper().startswith('COM') and int(port[3:]) > 8:
|
||||||
|
port = '\\\\.\\' + port
|
||||||
|
except ValueError:
|
||||||
|
# for like COMnotanumber
|
||||||
|
pass
|
||||||
|
self._port_handle = win32.CreateFile(
|
||||||
|
port,
|
||||||
|
win32.GENERIC_READ | win32.GENERIC_WRITE,
|
||||||
|
0, # exclusive access
|
||||||
|
None, # no security
|
||||||
|
win32.OPEN_EXISTING,
|
||||||
|
win32.FILE_ATTRIBUTE_NORMAL | win32.FILE_FLAG_OVERLAPPED,
|
||||||
|
0)
|
||||||
|
if self._port_handle == win32.INVALID_HANDLE_VALUE:
|
||||||
|
self._port_handle = None # 'cause __del__ is called anyway
|
||||||
|
raise SerialException("could not open port {!r}: {!r}".format(self.portstr, ctypes.WinError()))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._overlapped_read = win32.OVERLAPPED()
|
||||||
|
self._overlapped_read.hEvent = win32.CreateEvent(None, 1, 0, None)
|
||||||
|
self._overlapped_write = win32.OVERLAPPED()
|
||||||
|
#~ self._overlapped_write.hEvent = win32.CreateEvent(None, 1, 0, None)
|
||||||
|
self._overlapped_write.hEvent = win32.CreateEvent(None, 0, 0, None)
|
||||||
|
|
||||||
|
# Setup a 4k buffer
|
||||||
|
win32.SetupComm(self._port_handle, 4096, 4096)
|
||||||
|
|
||||||
|
# Save original timeout values:
|
||||||
|
self._orgTimeouts = win32.COMMTIMEOUTS()
|
||||||
|
win32.GetCommTimeouts(self._port_handle, ctypes.byref(self._orgTimeouts))
|
||||||
|
|
||||||
|
self._reconfigure_port()
|
||||||
|
|
||||||
|
# Clear buffers:
|
||||||
|
# Remove anything that was there
|
||||||
|
win32.PurgeComm(
|
||||||
|
self._port_handle,
|
||||||
|
win32.PURGE_TXCLEAR | win32.PURGE_TXABORT |
|
||||||
|
win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
self._close()
|
||||||
|
except:
|
||||||
|
# ignore any exception when closing the port
|
||||||
|
# also to keep original exception that happened when setting up
|
||||||
|
pass
|
||||||
|
self._port_handle = None
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.is_open = True
|
||||||
|
|
||||||
|
def _reconfigure_port(self):
|
||||||
|
"""Set communication parameters on opened port."""
|
||||||
|
if not self._port_handle:
|
||||||
|
raise SerialException("Can only operate on a valid port handle")
|
||||||
|
|
||||||
|
# Set Windows timeout values
|
||||||
|
# timeouts is a tuple with the following items:
|
||||||
|
# (ReadIntervalTimeout,ReadTotalTimeoutMultiplier,
|
||||||
|
# ReadTotalTimeoutConstant,WriteTotalTimeoutMultiplier,
|
||||||
|
# WriteTotalTimeoutConstant)
|
||||||
|
timeouts = win32.COMMTIMEOUTS()
|
||||||
|
if self._timeout is None:
|
||||||
|
pass # default of all zeros is OK
|
||||||
|
elif self._timeout == 0:
|
||||||
|
timeouts.ReadIntervalTimeout = win32.MAXDWORD
|
||||||
|
else:
|
||||||
|
timeouts.ReadTotalTimeoutConstant = max(int(self._timeout * 1000), 1)
|
||||||
|
if self._timeout != 0 and self._inter_byte_timeout is not None:
|
||||||
|
timeouts.ReadIntervalTimeout = max(int(self._inter_byte_timeout * 1000), 1)
|
||||||
|
|
||||||
|
if self._write_timeout is None:
|
||||||
|
pass
|
||||||
|
elif self._write_timeout == 0:
|
||||||
|
timeouts.WriteTotalTimeoutConstant = win32.MAXDWORD
|
||||||
|
else:
|
||||||
|
timeouts.WriteTotalTimeoutConstant = max(int(self._write_timeout * 1000), 1)
|
||||||
|
win32.SetCommTimeouts(self._port_handle, ctypes.byref(timeouts))
|
||||||
|
|
||||||
|
win32.SetCommMask(self._port_handle, win32.EV_ERR)
|
||||||
|
|
||||||
|
# Setup the connection info.
|
||||||
|
# Get state and modify it:
|
||||||
|
comDCB = win32.DCB()
|
||||||
|
win32.GetCommState(self._port_handle, ctypes.byref(comDCB))
|
||||||
|
comDCB.BaudRate = self._baudrate
|
||||||
|
|
||||||
|
if self._bytesize == serial.FIVEBITS:
|
||||||
|
comDCB.ByteSize = 5
|
||||||
|
elif self._bytesize == serial.SIXBITS:
|
||||||
|
comDCB.ByteSize = 6
|
||||||
|
elif self._bytesize == serial.SEVENBITS:
|
||||||
|
comDCB.ByteSize = 7
|
||||||
|
elif self._bytesize == serial.EIGHTBITS:
|
||||||
|
comDCB.ByteSize = 8
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported number of data bits: {!r}".format(self._bytesize))
|
||||||
|
|
||||||
|
if self._parity == serial.PARITY_NONE:
|
||||||
|
comDCB.Parity = win32.NOPARITY
|
||||||
|
comDCB.fParity = 0 # Disable Parity Check
|
||||||
|
elif self._parity == serial.PARITY_EVEN:
|
||||||
|
comDCB.Parity = win32.EVENPARITY
|
||||||
|
comDCB.fParity = 1 # Enable Parity Check
|
||||||
|
elif self._parity == serial.PARITY_ODD:
|
||||||
|
comDCB.Parity = win32.ODDPARITY
|
||||||
|
comDCB.fParity = 1 # Enable Parity Check
|
||||||
|
elif self._parity == serial.PARITY_MARK:
|
||||||
|
comDCB.Parity = win32.MARKPARITY
|
||||||
|
comDCB.fParity = 1 # Enable Parity Check
|
||||||
|
elif self._parity == serial.PARITY_SPACE:
|
||||||
|
comDCB.Parity = win32.SPACEPARITY
|
||||||
|
comDCB.fParity = 1 # Enable Parity Check
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported parity mode: {!r}".format(self._parity))
|
||||||
|
|
||||||
|
if self._stopbits == serial.STOPBITS_ONE:
|
||||||
|
comDCB.StopBits = win32.ONESTOPBIT
|
||||||
|
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||||
|
comDCB.StopBits = win32.ONE5STOPBITS
|
||||||
|
elif self._stopbits == serial.STOPBITS_TWO:
|
||||||
|
comDCB.StopBits = win32.TWOSTOPBITS
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported number of stop bits: {!r}".format(self._stopbits))
|
||||||
|
|
||||||
|
comDCB.fBinary = 1 # Enable Binary Transmission
|
||||||
|
# Char. w/ Parity-Err are replaced with 0xff (if fErrorChar is set to TRUE)
|
||||||
|
if self._rs485_mode is None:
|
||||||
|
if self._rtscts:
|
||||||
|
comDCB.fRtsControl = win32.RTS_CONTROL_HANDSHAKE
|
||||||
|
else:
|
||||||
|
comDCB.fRtsControl = win32.RTS_CONTROL_ENABLE if self._rts_state else win32.RTS_CONTROL_DISABLE
|
||||||
|
comDCB.fOutxCtsFlow = self._rtscts
|
||||||
|
else:
|
||||||
|
# checks for unsupported settings
|
||||||
|
# XXX verify if platform really does not have a setting for those
|
||||||
|
if not self._rs485_mode.rts_level_for_tx:
|
||||||
|
raise ValueError(
|
||||||
|
'Unsupported value for RS485Settings.rts_level_for_tx: {!r} (only True is allowed)'.format(
|
||||||
|
self._rs485_mode.rts_level_for_tx,))
|
||||||
|
if self._rs485_mode.rts_level_for_rx:
|
||||||
|
raise ValueError(
|
||||||
|
'Unsupported value for RS485Settings.rts_level_for_rx: {!r} (only False is allowed)'.format(
|
||||||
|
self._rs485_mode.rts_level_for_rx,))
|
||||||
|
if self._rs485_mode.delay_before_tx is not None:
|
||||||
|
raise ValueError(
|
||||||
|
'Unsupported value for RS485Settings.delay_before_tx: {!r} (only None is allowed)'.format(
|
||||||
|
self._rs485_mode.delay_before_tx,))
|
||||||
|
if self._rs485_mode.delay_before_rx is not None:
|
||||||
|
raise ValueError(
|
||||||
|
'Unsupported value for RS485Settings.delay_before_rx: {!r} (only None is allowed)'.format(
|
||||||
|
self._rs485_mode.delay_before_rx,))
|
||||||
|
if self._rs485_mode.loopback:
|
||||||
|
raise ValueError(
|
||||||
|
'Unsupported value for RS485Settings.loopback: {!r} (only False is allowed)'.format(
|
||||||
|
self._rs485_mode.loopback,))
|
||||||
|
comDCB.fRtsControl = win32.RTS_CONTROL_TOGGLE
|
||||||
|
comDCB.fOutxCtsFlow = 0
|
||||||
|
|
||||||
|
if self._dsrdtr:
|
||||||
|
comDCB.fDtrControl = win32.DTR_CONTROL_HANDSHAKE
|
||||||
|
else:
|
||||||
|
comDCB.fDtrControl = win32.DTR_CONTROL_ENABLE if self._dtr_state else win32.DTR_CONTROL_DISABLE
|
||||||
|
comDCB.fOutxDsrFlow = self._dsrdtr
|
||||||
|
comDCB.fOutX = self._xonxoff
|
||||||
|
comDCB.fInX = self._xonxoff
|
||||||
|
comDCB.fNull = 0
|
||||||
|
comDCB.fErrorChar = 0
|
||||||
|
comDCB.fAbortOnError = 0
|
||||||
|
comDCB.XonChar = serial.XON
|
||||||
|
comDCB.XoffChar = serial.XOFF
|
||||||
|
|
||||||
|
if not win32.SetCommState(self._port_handle, ctypes.byref(comDCB)):
|
||||||
|
raise SerialException(
|
||||||
|
'Cannot configure port, something went wrong. '
|
||||||
|
'Original message: {!r}'.format(ctypes.WinError()))
|
||||||
|
|
||||||
|
#~ def __del__(self):
|
||||||
|
#~ self.close()
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
"""internal close port helper"""
|
||||||
|
if self._port_handle is not None:
|
||||||
|
# Restore original timeout values:
|
||||||
|
win32.SetCommTimeouts(self._port_handle, self._orgTimeouts)
|
||||||
|
if self._overlapped_read is not None:
|
||||||
|
self.cancel_read()
|
||||||
|
win32.CloseHandle(self._overlapped_read.hEvent)
|
||||||
|
self._overlapped_read = None
|
||||||
|
if self._overlapped_write is not None:
|
||||||
|
self.cancel_write()
|
||||||
|
win32.CloseHandle(self._overlapped_write.hEvent)
|
||||||
|
self._overlapped_write = None
|
||||||
|
win32.CloseHandle(self._port_handle)
|
||||||
|
self._port_handle = None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close port"""
|
||||||
|
if self.is_open:
|
||||||
|
self._close()
|
||||||
|
self.is_open = False
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
"""Return the number of bytes currently in the input buffer."""
|
||||||
|
flags = win32.DWORD()
|
||||||
|
comstat = win32.COMSTAT()
|
||||||
|
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||||
|
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||||
|
return comstat.cbInQue
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if size > 0:
|
||||||
|
win32.ResetEvent(self._overlapped_read.hEvent)
|
||||||
|
flags = win32.DWORD()
|
||||||
|
comstat = win32.COMSTAT()
|
||||||
|
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||||
|
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||||
|
n = min(comstat.cbInQue, size) if self.timeout == 0 else size
|
||||||
|
if n > 0:
|
||||||
|
buf = ctypes.create_string_buffer(n)
|
||||||
|
rc = win32.DWORD()
|
||||||
|
read_ok = win32.ReadFile(
|
||||||
|
self._port_handle,
|
||||||
|
buf,
|
||||||
|
n,
|
||||||
|
ctypes.byref(rc),
|
||||||
|
ctypes.byref(self._overlapped_read))
|
||||||
|
if not read_ok and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||||
|
raise SerialException("ReadFile failed ({!r})".format(ctypes.WinError()))
|
||||||
|
result_ok = win32.GetOverlappedResult(
|
||||||
|
self._port_handle,
|
||||||
|
ctypes.byref(self._overlapped_read),
|
||||||
|
ctypes.byref(rc),
|
||||||
|
True)
|
||||||
|
if not result_ok:
|
||||||
|
if win32.GetLastError() != win32.ERROR_OPERATION_ABORTED:
|
||||||
|
raise SerialException("GetOverlappedResult failed ({!r})".format(ctypes.WinError()))
|
||||||
|
read = buf.raw[:rc.value]
|
||||||
|
else:
|
||||||
|
read = bytes()
|
||||||
|
else:
|
||||||
|
read = bytes()
|
||||||
|
return bytes(read)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""Output the given byte string over the serial port."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
#~ if not isinstance(data, (bytes, bytearray)):
|
||||||
|
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
|
||||||
|
# convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
|
||||||
|
data = to_bytes(data)
|
||||||
|
if data:
|
||||||
|
#~ win32event.ResetEvent(self._overlapped_write.hEvent)
|
||||||
|
n = win32.DWORD()
|
||||||
|
success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
|
||||||
|
if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0)
|
||||||
|
if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||||
|
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
|
||||||
|
|
||||||
|
# Wait for the write to complete.
|
||||||
|
#~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE)
|
||||||
|
win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True)
|
||||||
|
if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
|
||||||
|
return n.value # canceled IO is no error
|
||||||
|
if n.value != len(data):
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
return n.value
|
||||||
|
else:
|
||||||
|
errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
|
||||||
|
if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
|
||||||
|
win32.ERROR_OPERATION_ABORTED):
|
||||||
|
return 0
|
||||||
|
elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
|
||||||
|
# no info on true length provided by OS function in async mode
|
||||||
|
return len(data)
|
||||||
|
else:
|
||||||
|
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
"""\
|
||||||
|
Flush of file like objects. In this case, wait until all data
|
||||||
|
is written.
|
||||||
|
"""
|
||||||
|
while self.out_waiting:
|
||||||
|
time.sleep(0.05)
|
||||||
|
# XXX could also use WaitCommEvent with mask EV_TXEMPTY, but it would
|
||||||
|
# require overlapped IO and it's also only possible to set a single mask
|
||||||
|
# on the port---
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
"""Clear input buffer, discarding all that is in the buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
win32.PurgeComm(self._port_handle, win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
"""\
|
||||||
|
Clear output buffer, aborting the current output and discarding all
|
||||||
|
that is in the buffer.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
win32.PurgeComm(self._port_handle, win32.PURGE_TXCLEAR | win32.PURGE_TXABORT)
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""Set break: Controls TXD. When active, to transmitting is possible."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self._break_state:
|
||||||
|
win32.SetCommBreak(self._port_handle)
|
||||||
|
else:
|
||||||
|
win32.ClearCommBreak(self._port_handle)
|
||||||
|
|
||||||
|
def _update_rts_state(self):
|
||||||
|
"""Set terminal status line: Request To Send"""
|
||||||
|
if self._rts_state:
|
||||||
|
win32.EscapeCommFunction(self._port_handle, win32.SETRTS)
|
||||||
|
else:
|
||||||
|
win32.EscapeCommFunction(self._port_handle, win32.CLRRTS)
|
||||||
|
|
||||||
|
def _update_dtr_state(self):
|
||||||
|
"""Set terminal status line: Data Terminal Ready"""
|
||||||
|
if self._dtr_state:
|
||||||
|
win32.EscapeCommFunction(self._port_handle, win32.SETDTR)
|
||||||
|
else:
|
||||||
|
win32.EscapeCommFunction(self._port_handle, win32.CLRDTR)
|
||||||
|
|
||||||
|
def _GetCommModemStatus(self):
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
stat = win32.DWORD()
|
||||||
|
win32.GetCommModemStatus(self._port_handle, ctypes.byref(stat))
|
||||||
|
return stat.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cts(self):
|
||||||
|
"""Read terminal status line: Clear To Send"""
|
||||||
|
return win32.MS_CTS_ON & self._GetCommModemStatus() != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsr(self):
|
||||||
|
"""Read terminal status line: Data Set Ready"""
|
||||||
|
return win32.MS_DSR_ON & self._GetCommModemStatus() != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ri(self):
|
||||||
|
"""Read terminal status line: Ring Indicator"""
|
||||||
|
return win32.MS_RING_ON & self._GetCommModemStatus() != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cd(self):
|
||||||
|
"""Read terminal status line: Carrier Detect"""
|
||||||
|
return win32.MS_RLSD_ON & self._GetCommModemStatus() != 0
|
||||||
|
|
||||||
|
# - - platform specific - - - -
|
||||||
|
|
||||||
|
def set_buffer_size(self, rx_size=4096, tx_size=None):
|
||||||
|
"""\
|
||||||
|
Recommend a buffer size to the driver (device driver can ignore this
|
||||||
|
value). Must be called after the port is opened.
|
||||||
|
"""
|
||||||
|
if tx_size is None:
|
||||||
|
tx_size = rx_size
|
||||||
|
win32.SetupComm(self._port_handle, rx_size, tx_size)
|
||||||
|
|
||||||
|
def set_output_flow_control(self, enable=True):
|
||||||
|
"""\
|
||||||
|
Manually control flow - when software flow control is enabled.
|
||||||
|
This will do the same as if XON (true) or XOFF (false) are received
|
||||||
|
from the other device and control the transmission accordingly.
|
||||||
|
WARNING: this function is not portable to different platforms!
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if enable:
|
||||||
|
win32.EscapeCommFunction(self._port_handle, win32.SETXON)
|
||||||
|
else:
|
||||||
|
win32.EscapeCommFunction(self._port_handle, win32.SETXOFF)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def out_waiting(self):
|
||||||
|
"""Return how many bytes the in the outgoing buffer"""
|
||||||
|
flags = win32.DWORD()
|
||||||
|
comstat = win32.COMSTAT()
|
||||||
|
if not win32.ClearCommError(self._port_handle, ctypes.byref(flags), ctypes.byref(comstat)):
|
||||||
|
raise SerialException("ClearCommError failed ({!r})".format(ctypes.WinError()))
|
||||||
|
return comstat.cbOutQue
|
||||||
|
|
||||||
|
def _cancel_overlapped_io(self, overlapped):
|
||||||
|
"""Cancel a blocking read operation, may be called from other thread"""
|
||||||
|
# check if read operation is pending
|
||||||
|
rc = win32.DWORD()
|
||||||
|
err = win32.GetOverlappedResult(
|
||||||
|
self._port_handle,
|
||||||
|
ctypes.byref(overlapped),
|
||||||
|
ctypes.byref(rc),
|
||||||
|
False)
|
||||||
|
if not err and win32.GetLastError() in (win32.ERROR_IO_PENDING, win32.ERROR_IO_INCOMPLETE):
|
||||||
|
# cancel, ignoring any errors (e.g. it may just have finished on its own)
|
||||||
|
win32.CancelIoEx(self._port_handle, overlapped)
|
||||||
|
|
||||||
|
def cancel_read(self):
|
||||||
|
"""Cancel a blocking read operation, may be called from other thread"""
|
||||||
|
self._cancel_overlapped_io(self._overlapped_read)
|
||||||
|
|
||||||
|
def cancel_write(self):
|
||||||
|
"""Cancel a blocking write operation, may be called from other thread"""
|
||||||
|
self._cancel_overlapped_io(self._overlapped_write)
|
||||||
|
|
||||||
|
@SerialBase.exclusive.setter
|
||||||
|
def exclusive(self, exclusive):
|
||||||
|
"""Change the exclusive access setting."""
|
||||||
|
if exclusive is not None and not exclusive:
|
||||||
|
raise ValueError('win32 only supports exclusive access (not: {})'.format(exclusive))
|
||||||
|
else:
|
||||||
|
serial.SerialBase.exclusive.__set__(self, exclusive)
|
||||||
297
deps/serial/threaded/__init__.py
vendored
Normal file
297
deps/serial/threaded/__init__.py
vendored
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Working with threading and pySerial
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2015-2016 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
"""\
|
||||||
|
Support threading with serial ports.
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
class Protocol(object):
|
||||||
|
"""\
|
||||||
|
Protocol as used by the ReaderThread. This base class provides empty
|
||||||
|
implementations of all methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
"""Called when reader thread is started"""
|
||||||
|
|
||||||
|
def data_received(self, data):
|
||||||
|
"""Called with snippets received from the serial port"""
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
"""\
|
||||||
|
Called when the serial port is closed or the reader loop terminated
|
||||||
|
otherwise.
|
||||||
|
"""
|
||||||
|
if isinstance(exc, Exception):
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
class Packetizer(Protocol):
|
||||||
|
"""
|
||||||
|
Read binary packets from serial port. Packets are expected to be terminated
|
||||||
|
with a TERMINATOR byte (null byte by default).
|
||||||
|
|
||||||
|
The class also keeps track of the transport.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TERMINATOR = b'\0'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.buffer = bytearray()
|
||||||
|
self.transport = None
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
"""Store transport"""
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
"""Forget transport"""
|
||||||
|
self.transport = None
|
||||||
|
super(Packetizer, self).connection_lost(exc)
|
||||||
|
|
||||||
|
def data_received(self, data):
|
||||||
|
"""Buffer received data, find TERMINATOR, call handle_packet"""
|
||||||
|
self.buffer.extend(data)
|
||||||
|
while self.TERMINATOR in self.buffer:
|
||||||
|
packet, self.buffer = self.buffer.split(self.TERMINATOR, 1)
|
||||||
|
self.handle_packet(packet)
|
||||||
|
|
||||||
|
def handle_packet(self, packet):
|
||||||
|
"""Process packets - to be overridden by subclassing"""
|
||||||
|
raise NotImplementedError('please implement functionality in handle_packet')
|
||||||
|
|
||||||
|
|
||||||
|
class FramedPacket(Protocol):
|
||||||
|
"""
|
||||||
|
Read binary packets. Packets are expected to have a start and stop marker.
|
||||||
|
|
||||||
|
The class also keeps track of the transport.
|
||||||
|
"""
|
||||||
|
|
||||||
|
START = b'('
|
||||||
|
STOP = b')'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.packet = bytearray()
|
||||||
|
self.in_packet = False
|
||||||
|
self.transport = None
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
"""Store transport"""
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
"""Forget transport"""
|
||||||
|
self.transport = None
|
||||||
|
self.in_packet = False
|
||||||
|
del self.packet[:]
|
||||||
|
super(FramedPacket, self).connection_lost(exc)
|
||||||
|
|
||||||
|
def data_received(self, data):
|
||||||
|
"""Find data enclosed in START/STOP, call handle_packet"""
|
||||||
|
for byte in serial.iterbytes(data):
|
||||||
|
if byte == self.START:
|
||||||
|
self.in_packet = True
|
||||||
|
elif byte == self.STOP:
|
||||||
|
self.in_packet = False
|
||||||
|
self.handle_packet(bytes(self.packet)) # make read-only copy
|
||||||
|
del self.packet[:]
|
||||||
|
elif self.in_packet:
|
||||||
|
self.packet.extend(byte)
|
||||||
|
else:
|
||||||
|
self.handle_out_of_packet_data(byte)
|
||||||
|
|
||||||
|
def handle_packet(self, packet):
|
||||||
|
"""Process packets - to be overridden by subclassing"""
|
||||||
|
raise NotImplementedError('please implement functionality in handle_packet')
|
||||||
|
|
||||||
|
def handle_out_of_packet_data(self, data):
|
||||||
|
"""Process data that is received outside of packets"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LineReader(Packetizer):
|
||||||
|
"""
|
||||||
|
Read and write (Unicode) lines from/to serial port.
|
||||||
|
The encoding is applied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TERMINATOR = b'\r\n'
|
||||||
|
ENCODING = 'utf-8'
|
||||||
|
UNICODE_HANDLING = 'replace'
|
||||||
|
|
||||||
|
def handle_packet(self, packet):
|
||||||
|
self.handle_line(packet.decode(self.ENCODING, self.UNICODE_HANDLING))
|
||||||
|
|
||||||
|
def handle_line(self, line):
|
||||||
|
"""Process one line - to be overridden by subclassing"""
|
||||||
|
raise NotImplementedError('please implement functionality in handle_line')
|
||||||
|
|
||||||
|
def write_line(self, text):
|
||||||
|
"""
|
||||||
|
Write text to the transport. ``text`` is a Unicode string and the encoding
|
||||||
|
is applied before sending ans also the newline is append.
|
||||||
|
"""
|
||||||
|
# + is not the best choice but bytes does not support % or .format in py3 and we want a single write call
|
||||||
|
self.transport.write(text.encode(self.ENCODING, self.UNICODE_HANDLING) + self.TERMINATOR)
|
||||||
|
|
||||||
|
|
||||||
|
class ReaderThread(threading.Thread):
|
||||||
|
"""\
|
||||||
|
Implement a serial port read loop and dispatch to a Protocol instance (like
|
||||||
|
the asyncio.Protocol) but do it with threads.
|
||||||
|
|
||||||
|
Calls to close() will close the serial port but it is also possible to just
|
||||||
|
stop() this thread and continue the serial port instance otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, serial_instance, protocol_factory):
|
||||||
|
"""\
|
||||||
|
Initialize thread.
|
||||||
|
|
||||||
|
Note that the serial_instance' timeout is set to one second!
|
||||||
|
Other settings are not changed.
|
||||||
|
"""
|
||||||
|
super(ReaderThread, self).__init__()
|
||||||
|
self.daemon = True
|
||||||
|
self.serial = serial_instance
|
||||||
|
self.protocol_factory = protocol_factory
|
||||||
|
self.alive = True
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._connection_made = threading.Event()
|
||||||
|
self.protocol = None
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the reader thread"""
|
||||||
|
self.alive = False
|
||||||
|
if hasattr(self.serial, 'cancel_read'):
|
||||||
|
self.serial.cancel_read()
|
||||||
|
self.join(2)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Reader loop"""
|
||||||
|
if not hasattr(self.serial, 'cancel_read'):
|
||||||
|
self.serial.timeout = 1
|
||||||
|
self.protocol = self.protocol_factory()
|
||||||
|
try:
|
||||||
|
self.protocol.connection_made(self)
|
||||||
|
except Exception as e:
|
||||||
|
self.alive = False
|
||||||
|
self.protocol.connection_lost(e)
|
||||||
|
self._connection_made.set()
|
||||||
|
return
|
||||||
|
error = None
|
||||||
|
self._connection_made.set()
|
||||||
|
while self.alive and self.serial.is_open:
|
||||||
|
try:
|
||||||
|
# read all that is there or wait for one byte (blocking)
|
||||||
|
data = self.serial.read(self.serial.in_waiting or 1)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
# probably some I/O problem such as disconnected USB serial
|
||||||
|
# adapters -> exit
|
||||||
|
error = e
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if data:
|
||||||
|
# make a separated try-except for called user code
|
||||||
|
try:
|
||||||
|
self.protocol.data_received(data)
|
||||||
|
except Exception as e:
|
||||||
|
error = e
|
||||||
|
break
|
||||||
|
self.alive = False
|
||||||
|
self.protocol.connection_lost(error)
|
||||||
|
self.protocol = None
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""Thread safe writing (uses lock)"""
|
||||||
|
with self._lock:
|
||||||
|
return self.serial.write(data)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the serial port and exit reader thread (uses lock)"""
|
||||||
|
# use the lock to let other threads finish writing
|
||||||
|
with self._lock:
|
||||||
|
# first stop reading, so that closing can be done on idle port
|
||||||
|
self.stop()
|
||||||
|
self.serial.close()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""
|
||||||
|
Wait until connection is set up and return the transport and protocol
|
||||||
|
instances.
|
||||||
|
"""
|
||||||
|
if self.alive:
|
||||||
|
self._connection_made.wait()
|
||||||
|
if not self.alive:
|
||||||
|
raise RuntimeError('connection_lost already called')
|
||||||
|
return (self, self.protocol)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('already stopped')
|
||||||
|
|
||||||
|
# - - context manager, returns protocol
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""\
|
||||||
|
Enter context handler. May raise RuntimeError in case the connection
|
||||||
|
could not be created.
|
||||||
|
"""
|
||||||
|
self.start()
|
||||||
|
self._connection_made.wait()
|
||||||
|
if not self.alive:
|
||||||
|
raise RuntimeError('connection_lost already called')
|
||||||
|
return self.protocol
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Leave context: close port"""
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# pylint: disable=wrong-import-position
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
#~ PORT = 'spy:///dev/ttyUSB0'
|
||||||
|
PORT = 'loop://'
|
||||||
|
|
||||||
|
class PrintLines(LineReader):
|
||||||
|
def connection_made(self, transport):
|
||||||
|
super(PrintLines, self).connection_made(transport)
|
||||||
|
sys.stdout.write('port opened\n')
|
||||||
|
self.write_line('hello world')
|
||||||
|
|
||||||
|
def handle_line(self, data):
|
||||||
|
sys.stdout.write('line received: {!r}\n'.format(data))
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
if exc:
|
||||||
|
traceback.print_exc(exc)
|
||||||
|
sys.stdout.write('port closed\n')
|
||||||
|
|
||||||
|
ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
|
||||||
|
with ReaderThread(ser, PrintLines) as protocol:
|
||||||
|
protocol.write_line('hello')
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# alternative usage
|
||||||
|
ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
|
||||||
|
t = ReaderThread(ser, PrintLines)
|
||||||
|
t.start()
|
||||||
|
transport, protocol = t.connect()
|
||||||
|
protocol.write_line('hello')
|
||||||
|
time.sleep(2)
|
||||||
|
t.close()
|
||||||
0
deps/serial/tools/__init__.py
vendored
Normal file
0
deps/serial/tools/__init__.py
vendored
Normal file
126
deps/serial/tools/hexlify_codec.py
vendored
Normal file
126
deps/serial/tools/hexlify_codec.py
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# This is a codec to create and decode hexdumps with spaces between characters. used by miniterm.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2015-2016 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
"""\
|
||||||
|
Python 'hex' Codec - 2-digit hex with spaces content transfer encoding.
|
||||||
|
|
||||||
|
Encode and decode may be a bit missleading at first sight...
|
||||||
|
|
||||||
|
The textual representation is a hex dump: e.g. "40 41"
|
||||||
|
The "encoded" data of this is the binary form, e.g. b"@A"
|
||||||
|
|
||||||
|
Therefore decoding is binary to text and thus converting binary data to hex dump.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import serial
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
unicode
|
||||||
|
except (NameError, AttributeError):
|
||||||
|
unicode = str # for Python 3, pylint: disable=redefined-builtin,invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
HEXDIGITS = '0123456789ABCDEF'
|
||||||
|
|
||||||
|
|
||||||
|
# Codec APIs
|
||||||
|
|
||||||
|
def hex_encode(data, errors='strict'):
|
||||||
|
"""'40 41 42' -> b'@ab'"""
|
||||||
|
return (serial.to_bytes([int(h, 16) for h in data.split()]), len(data))
|
||||||
|
|
||||||
|
|
||||||
|
def hex_decode(data, errors='strict'):
|
||||||
|
"""b'@ab' -> '40 41 42'"""
|
||||||
|
return (unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data))), len(data))
|
||||||
|
|
||||||
|
|
||||||
|
class Codec(codecs.Codec):
|
||||||
|
def encode(self, data, errors='strict'):
|
||||||
|
"""'40 41 42' -> b'@ab'"""
|
||||||
|
return serial.to_bytes([int(h, 16) for h in data.split()])
|
||||||
|
|
||||||
|
def decode(self, data, errors='strict'):
|
||||||
|
"""b'@ab' -> '40 41 42'"""
|
||||||
|
return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
|
||||||
|
|
||||||
|
|
||||||
|
class IncrementalEncoder(codecs.IncrementalEncoder):
|
||||||
|
"""Incremental hex encoder"""
|
||||||
|
|
||||||
|
def __init__(self, errors='strict'):
|
||||||
|
self.errors = errors
|
||||||
|
self.state = 0
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.state = 0
|
||||||
|
|
||||||
|
def getstate(self):
|
||||||
|
return self.state
|
||||||
|
|
||||||
|
def setstate(self, state):
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
def encode(self, data, final=False):
|
||||||
|
"""\
|
||||||
|
Incremental encode, keep track of digits and emit a byte when a pair
|
||||||
|
of hex digits is found. The space is optional unless the error
|
||||||
|
handling is defined to be 'strict'.
|
||||||
|
"""
|
||||||
|
state = self.state
|
||||||
|
encoded = []
|
||||||
|
for c in data.upper():
|
||||||
|
if c in HEXDIGITS:
|
||||||
|
z = HEXDIGITS.index(c)
|
||||||
|
if state:
|
||||||
|
encoded.append(z + (state & 0xf0))
|
||||||
|
state = 0
|
||||||
|
else:
|
||||||
|
state = 0x100 + (z << 4)
|
||||||
|
elif c == ' ': # allow spaces to separate values
|
||||||
|
if state and self.errors == 'strict':
|
||||||
|
raise UnicodeError('odd number of hex digits')
|
||||||
|
state = 0
|
||||||
|
else:
|
||||||
|
if self.errors == 'strict':
|
||||||
|
raise UnicodeError('non-hex digit found: {!r}'.format(c))
|
||||||
|
self.state = state
|
||||||
|
return serial.to_bytes(encoded)
|
||||||
|
|
||||||
|
|
||||||
|
class IncrementalDecoder(codecs.IncrementalDecoder):
|
||||||
|
"""Incremental decoder"""
|
||||||
|
def decode(self, data, final=False):
|
||||||
|
return unicode(''.join('{:02X} '.format(ord(b)) for b in serial.iterbytes(data)))
|
||||||
|
|
||||||
|
|
||||||
|
class StreamWriter(Codec, codecs.StreamWriter):
|
||||||
|
"""Combination of hexlify codec and StreamWriter"""
|
||||||
|
|
||||||
|
|
||||||
|
class StreamReader(Codec, codecs.StreamReader):
|
||||||
|
"""Combination of hexlify codec and StreamReader"""
|
||||||
|
|
||||||
|
|
||||||
|
def getregentry():
|
||||||
|
"""encodings module API"""
|
||||||
|
return codecs.CodecInfo(
|
||||||
|
name='hexlify',
|
||||||
|
encode=hex_encode,
|
||||||
|
decode=hex_decode,
|
||||||
|
incrementalencoder=IncrementalEncoder,
|
||||||
|
incrementaldecoder=IncrementalDecoder,
|
||||||
|
streamwriter=StreamWriter,
|
||||||
|
streamreader=StreamReader,
|
||||||
|
#~ _is_text_encoding=True,
|
||||||
|
)
|
||||||
110
deps/serial/tools/list_ports.py
vendored
Normal file
110
deps/serial/tools/list_ports.py
vendored
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Serial port enumeration. Console tool and backend selection.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
"""\
|
||||||
|
This module will provide a function called comports that returns an
|
||||||
|
iterable (generator or list) that will enumerate available com ports. Note that
|
||||||
|
on some systems non-existent ports may be listed.
|
||||||
|
|
||||||
|
Additionally a grep function is supplied that can be used to search for ports
|
||||||
|
based on their descriptions or hardware ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# chose an implementation, depending on os
|
||||||
|
#~ if sys.platform == 'cli':
|
||||||
|
#~ else:
|
||||||
|
if os.name == 'nt': # sys.platform == 'win32':
|
||||||
|
from serial.tools.list_ports_windows import comports
|
||||||
|
elif os.name == 'posix':
|
||||||
|
from serial.tools.list_ports_posix import comports
|
||||||
|
#~ elif os.name == 'java':
|
||||||
|
else:
|
||||||
|
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
|
||||||
|
def grep(regexp, include_links=False):
|
||||||
|
"""\
|
||||||
|
Search for ports using a regular expression. Port name, description and
|
||||||
|
hardware ID are searched. The function returns an iterable that returns the
|
||||||
|
same tuples as comport() would do.
|
||||||
|
"""
|
||||||
|
r = re.compile(regexp, re.I)
|
||||||
|
for info in comports(include_links):
|
||||||
|
port, desc, hwid = info
|
||||||
|
if r.search(port) or r.search(desc) or r.search(hwid):
|
||||||
|
yield info
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Serial port enumeration')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'regexp',
|
||||||
|
nargs='?',
|
||||||
|
help='only show ports that match this regex')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-v', '--verbose',
|
||||||
|
action='store_true',
|
||||||
|
help='show more messages')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-q', '--quiet',
|
||||||
|
action='store_true',
|
||||||
|
help='suppress all messages')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-n',
|
||||||
|
type=int,
|
||||||
|
help='only output the N-th entry')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'-s', '--include-links',
|
||||||
|
action='store_true',
|
||||||
|
help='include entries that are symlinks to real devices')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
hits = 0
|
||||||
|
# get iteraror w/ or w/o filter
|
||||||
|
if args.regexp:
|
||||||
|
if not args.quiet:
|
||||||
|
sys.stderr.write("Filtered list with regexp: {!r}\n".format(args.regexp))
|
||||||
|
iterator = sorted(grep(args.regexp, include_links=args.include_links))
|
||||||
|
else:
|
||||||
|
iterator = sorted(comports(include_links=args.include_links))
|
||||||
|
# list them
|
||||||
|
for n, (port, desc, hwid) in enumerate(iterator, 1):
|
||||||
|
if args.n is None or args.n == n:
|
||||||
|
sys.stdout.write("{:20}\n".format(port))
|
||||||
|
if args.verbose:
|
||||||
|
sys.stdout.write(" desc: {}\n".format(desc))
|
||||||
|
sys.stdout.write(" hwid: {}\n".format(hwid))
|
||||||
|
hits += 1
|
||||||
|
if not args.quiet:
|
||||||
|
if hits:
|
||||||
|
sys.stderr.write("{} ports found\n".format(hits))
|
||||||
|
else:
|
||||||
|
sys.stderr.write("no ports found\n")
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
121
deps/serial/tools/list_ports_common.py
vendored
Normal file
121
deps/serial/tools/list_ports_common.py
vendored
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# This is a helper module for the various platform dependent list_port
|
||||||
|
# implementations.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import re
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
|
def numsplit(text):
|
||||||
|
"""\
|
||||||
|
Convert string into a list of texts and numbers in order to support a
|
||||||
|
natural sorting.
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for group in re.split(r'(\d+)', text):
|
||||||
|
if group:
|
||||||
|
try:
|
||||||
|
group = int(group)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
result.append(group)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ListPortInfo(object):
|
||||||
|
"""Info collection base class for serial ports"""
|
||||||
|
|
||||||
|
def __init__(self, device, skip_link_detection=False):
|
||||||
|
self.device = device
|
||||||
|
self.name = os.path.basename(device)
|
||||||
|
self.description = 'n/a'
|
||||||
|
self.hwid = 'n/a'
|
||||||
|
# USB specific data
|
||||||
|
self.vid = None
|
||||||
|
self.pid = None
|
||||||
|
self.serial_number = None
|
||||||
|
self.location = None
|
||||||
|
self.manufacturer = None
|
||||||
|
self.product = None
|
||||||
|
self.interface = None
|
||||||
|
# special handling for links
|
||||||
|
if not skip_link_detection and device is not None and os.path.islink(device):
|
||||||
|
self.hwid = 'LINK={}'.format(os.path.realpath(device))
|
||||||
|
|
||||||
|
def usb_description(self):
|
||||||
|
"""return a short string to name the port based on USB info"""
|
||||||
|
if self.interface is not None:
|
||||||
|
return '{} - {}'.format(self.product, self.interface)
|
||||||
|
elif self.product is not None:
|
||||||
|
return self.product
|
||||||
|
else:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def usb_info(self):
|
||||||
|
"""return a string with USB related information about device"""
|
||||||
|
return 'USB VID:PID={:04X}:{:04X}{}{}'.format(
|
||||||
|
self.vid or 0,
|
||||||
|
self.pid or 0,
|
||||||
|
' SER={}'.format(self.serial_number) if self.serial_number is not None else '',
|
||||||
|
' LOCATION={}'.format(self.location) if self.location is not None else '')
|
||||||
|
|
||||||
|
def apply_usb_info(self):
|
||||||
|
"""update description and hwid from USB data"""
|
||||||
|
self.description = self.usb_description()
|
||||||
|
self.hwid = self.usb_info()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, ListPortInfo) and self.device == other.device
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.device)
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if not isinstance(other, ListPortInfo):
|
||||||
|
raise TypeError('unorderable types: {}() and {}()'.format(
|
||||||
|
type(self).__name__,
|
||||||
|
type(other).__name__))
|
||||||
|
return numsplit(self.device) < numsplit(other.device)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} - {}'.format(self.device, self.description)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
"""Item access: backwards compatible -> (port, desc, hwid)"""
|
||||||
|
if index == 0:
|
||||||
|
return self.device
|
||||||
|
elif index == 1:
|
||||||
|
return self.description
|
||||||
|
elif index == 2:
|
||||||
|
return self.hwid
|
||||||
|
else:
|
||||||
|
raise IndexError('{} > 2'.format(index))
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
def list_links(devices):
|
||||||
|
"""\
|
||||||
|
search all /dev devices and look for symlinks to known ports already
|
||||||
|
listed in devices.
|
||||||
|
"""
|
||||||
|
links = []
|
||||||
|
for device in glob.glob('/dev/*'):
|
||||||
|
if os.path.islink(device) and os.path.realpath(device) in devices:
|
||||||
|
links.append(device)
|
||||||
|
return links
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(ListPortInfo('dummy'))
|
||||||
109
deps/serial/tools/list_ports_linux.py
vendored
Normal file
109
deps/serial/tools/list_ports_linux.py
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# This is a module that gathers a list of serial ports including details on
|
||||||
|
# GNU/Linux systems.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
from serial.tools import list_ports_common
|
||||||
|
|
||||||
|
|
||||||
|
class SysFS(list_ports_common.ListPortInfo):
|
||||||
|
"""Wrapper for easy sysfs access and device info"""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
super(SysFS, self).__init__(device)
|
||||||
|
# special handling for links
|
||||||
|
if device is not None and os.path.islink(device):
|
||||||
|
device = os.path.realpath(device)
|
||||||
|
is_link = True
|
||||||
|
else:
|
||||||
|
is_link = False
|
||||||
|
self.usb_device_path = None
|
||||||
|
if os.path.exists('/sys/class/tty/{}/device'.format(self.name)):
|
||||||
|
self.device_path = os.path.realpath('/sys/class/tty/{}/device'.format(self.name))
|
||||||
|
self.subsystem = os.path.basename(os.path.realpath(os.path.join(self.device_path, 'subsystem')))
|
||||||
|
else:
|
||||||
|
self.device_path = None
|
||||||
|
self.subsystem = None
|
||||||
|
# check device type
|
||||||
|
if self.subsystem == 'usb-serial':
|
||||||
|
self.usb_interface_path = os.path.dirname(self.device_path)
|
||||||
|
elif self.subsystem == 'usb':
|
||||||
|
self.usb_interface_path = self.device_path
|
||||||
|
else:
|
||||||
|
self.usb_interface_path = None
|
||||||
|
# fill-in info for USB devices
|
||||||
|
if self.usb_interface_path is not None:
|
||||||
|
self.usb_device_path = os.path.dirname(self.usb_interface_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
num_if = int(self.read_line(self.usb_device_path, 'bNumInterfaces'))
|
||||||
|
except ValueError:
|
||||||
|
num_if = 1
|
||||||
|
|
||||||
|
self.vid = int(self.read_line(self.usb_device_path, 'idVendor'), 16)
|
||||||
|
self.pid = int(self.read_line(self.usb_device_path, 'idProduct'), 16)
|
||||||
|
self.serial_number = self.read_line(self.usb_device_path, 'serial')
|
||||||
|
if num_if > 1: # multi interface devices like FT4232
|
||||||
|
self.location = os.path.basename(self.usb_interface_path)
|
||||||
|
else:
|
||||||
|
self.location = os.path.basename(self.usb_device_path)
|
||||||
|
|
||||||
|
self.manufacturer = self.read_line(self.usb_device_path, 'manufacturer')
|
||||||
|
self.product = self.read_line(self.usb_device_path, 'product')
|
||||||
|
self.interface = self.read_line(self.usb_interface_path, 'interface')
|
||||||
|
|
||||||
|
if self.subsystem in ('usb', 'usb-serial'):
|
||||||
|
self.apply_usb_info()
|
||||||
|
#~ elif self.subsystem in ('pnp', 'amba'): # PCI based devices, raspi
|
||||||
|
elif self.subsystem == 'pnp': # PCI based devices
|
||||||
|
self.description = self.name
|
||||||
|
self.hwid = self.read_line(self.device_path, 'id')
|
||||||
|
elif self.subsystem == 'amba': # raspi
|
||||||
|
self.description = self.name
|
||||||
|
self.hwid = os.path.basename(self.device_path)
|
||||||
|
|
||||||
|
if is_link:
|
||||||
|
self.hwid += ' LINK={}'.format(device)
|
||||||
|
|
||||||
|
def read_line(self, *args):
|
||||||
|
"""\
|
||||||
|
Helper function to read a single line from a file.
|
||||||
|
One or more parameters are allowed, they are joined with os.path.join.
|
||||||
|
Returns None on errors..
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(os.path.join(*args)) as f:
|
||||||
|
line = f.readline().strip()
|
||||||
|
return line
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def comports(include_links=False):
|
||||||
|
devices = glob.glob('/dev/ttyS*') # built-in serial ports
|
||||||
|
devices.extend(glob.glob('/dev/ttyUSB*')) # usb-serial with own driver
|
||||||
|
devices.extend(glob.glob('/dev/ttyXRUSB*')) # xr-usb-serial port exar (DELL Edge 3001)
|
||||||
|
devices.extend(glob.glob('/dev/ttyACM*')) # usb-serial with CDC-ACM profile
|
||||||
|
devices.extend(glob.glob('/dev/ttyAMA*')) # ARM internal port (raspi)
|
||||||
|
devices.extend(glob.glob('/dev/rfcomm*')) # BT serial devices
|
||||||
|
devices.extend(glob.glob('/dev/ttyAP*')) # Advantech multi-port serial controllers
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [info
|
||||||
|
for info in [SysFS(d) for d in devices]
|
||||||
|
if info.subsystem != "platform"] # hide non-present internal serial ports
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for info in sorted(comports()):
|
||||||
|
print("{0}: {0.subsystem}".format(info))
|
||||||
299
deps/serial/tools/list_ports_osx.py
vendored
Normal file
299
deps/serial/tools/list_ports_osx.py
vendored
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# This is a module that gathers a list of serial ports including details on OSX
|
||||||
|
#
|
||||||
|
# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools
|
||||||
|
# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston
|
||||||
|
# and modifications by cliechti, hoihu, hardkrash
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2013-2020
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
|
||||||
|
# List all of the callout devices in OS/X by querying IOKit.
|
||||||
|
|
||||||
|
# See the following for a reference of how to do this:
|
||||||
|
# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD
|
||||||
|
|
||||||
|
# More help from darwin_hid.py
|
||||||
|
|
||||||
|
# Also see the 'IORegistryExplorer' for an idea of what we are actually searching
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
from serial.tools import list_ports_common
|
||||||
|
|
||||||
|
iokit = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/IOKit.framework/IOKit')
|
||||||
|
cf = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation')
|
||||||
|
|
||||||
|
# kIOMasterPortDefault is no longer exported in BigSur but no biggie, using NULL works just the same
|
||||||
|
kIOMasterPortDefault = 0 # WAS: ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault")
|
||||||
|
kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault")
|
||||||
|
|
||||||
|
kCFStringEncodingMacRoman = 0
|
||||||
|
kCFStringEncodingUTF8 = 0x08000100
|
||||||
|
|
||||||
|
# defined in `IOKit/usb/USBSpec.h`
|
||||||
|
kUSBVendorString = 'USB Vendor Name'
|
||||||
|
kUSBSerialNumberString = 'USB Serial Number'
|
||||||
|
|
||||||
|
# `io_name_t` defined as `typedef char io_name_t[128];`
|
||||||
|
# in `device/device_types.h`
|
||||||
|
io_name_size = 128
|
||||||
|
|
||||||
|
# defined in `mach/kern_return.h`
|
||||||
|
KERN_SUCCESS = 0
|
||||||
|
# kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h`
|
||||||
|
kern_return_t = ctypes.c_int
|
||||||
|
|
||||||
|
iokit.IOServiceMatching.restype = ctypes.c_void_p
|
||||||
|
|
||||||
|
iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IOServiceGetMatchingServices.restype = kern_return_t
|
||||||
|
|
||||||
|
iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IOServiceGetMatchingServices.restype = kern_return_t
|
||||||
|
|
||||||
|
iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32]
|
||||||
|
iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p
|
||||||
|
|
||||||
|
iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IORegistryEntryGetPath.restype = kern_return_t
|
||||||
|
|
||||||
|
iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IORegistryEntryGetName.restype = kern_return_t
|
||||||
|
|
||||||
|
iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
||||||
|
iokit.IOObjectGetClass.restype = kern_return_t
|
||||||
|
|
||||||
|
iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]
|
||||||
|
|
||||||
|
|
||||||
|
cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32]
|
||||||
|
cf.CFStringCreateWithCString.restype = ctypes.c_void_p
|
||||||
|
|
||||||
|
cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
|
||||||
|
cf.CFStringGetCStringPtr.restype = ctypes.c_char_p
|
||||||
|
|
||||||
|
cf.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32]
|
||||||
|
cf.CFStringGetCString.restype = ctypes.c_bool
|
||||||
|
|
||||||
|
cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
|
||||||
|
cf.CFNumberGetValue.restype = ctypes.c_void_p
|
||||||
|
|
||||||
|
# void CFRelease ( CFTypeRef cf );
|
||||||
|
cf.CFRelease.argtypes = [ctypes.c_void_p]
|
||||||
|
cf.CFRelease.restype = None
|
||||||
|
|
||||||
|
# CFNumber type defines
|
||||||
|
kCFNumberSInt8Type = 1
|
||||||
|
kCFNumberSInt16Type = 2
|
||||||
|
kCFNumberSInt32Type = 3
|
||||||
|
kCFNumberSInt64Type = 4
|
||||||
|
|
||||||
|
|
||||||
|
def get_string_property(device_type, property):
|
||||||
|
"""
|
||||||
|
Search the given device for the specified string property
|
||||||
|
|
||||||
|
@param device_type Type of Device
|
||||||
|
@param property String to search for
|
||||||
|
@return Python string containing the value, or None if not found.
|
||||||
|
"""
|
||||||
|
key = cf.CFStringCreateWithCString(
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
property.encode("utf-8"),
|
||||||
|
kCFStringEncodingUTF8)
|
||||||
|
|
||||||
|
CFContainer = iokit.IORegistryEntryCreateCFProperty(
|
||||||
|
device_type,
|
||||||
|
key,
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
0)
|
||||||
|
output = None
|
||||||
|
|
||||||
|
if CFContainer:
|
||||||
|
output = cf.CFStringGetCStringPtr(CFContainer, 0)
|
||||||
|
if output is not None:
|
||||||
|
output = output.decode('utf-8')
|
||||||
|
else:
|
||||||
|
buffer = ctypes.create_string_buffer(io_name_size);
|
||||||
|
success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8)
|
||||||
|
if success:
|
||||||
|
output = buffer.value.decode('utf-8')
|
||||||
|
cf.CFRelease(CFContainer)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def get_int_property(device_type, property, cf_number_type):
|
||||||
|
"""
|
||||||
|
Search the given device for the specified string property
|
||||||
|
|
||||||
|
@param device_type Device to search
|
||||||
|
@param property String to search for
|
||||||
|
@param cf_number_type CFType number
|
||||||
|
|
||||||
|
@return Python string containing the value, or None if not found.
|
||||||
|
"""
|
||||||
|
key = cf.CFStringCreateWithCString(
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
property.encode("utf-8"),
|
||||||
|
kCFStringEncodingUTF8)
|
||||||
|
|
||||||
|
CFContainer = iokit.IORegistryEntryCreateCFProperty(
|
||||||
|
device_type,
|
||||||
|
key,
|
||||||
|
kCFAllocatorDefault,
|
||||||
|
0)
|
||||||
|
|
||||||
|
if CFContainer:
|
||||||
|
if (cf_number_type == kCFNumberSInt32Type):
|
||||||
|
number = ctypes.c_uint32()
|
||||||
|
elif (cf_number_type == kCFNumberSInt16Type):
|
||||||
|
number = ctypes.c_uint16()
|
||||||
|
cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number))
|
||||||
|
cf.CFRelease(CFContainer)
|
||||||
|
return number.value
|
||||||
|
return None
|
||||||
|
|
||||||
|
def IORegistryEntryGetName(device):
|
||||||
|
devicename = ctypes.create_string_buffer(io_name_size);
|
||||||
|
res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename))
|
||||||
|
if res != KERN_SUCCESS:
|
||||||
|
return None
|
||||||
|
# this works in python2 but may not be valid. Also I don't know if
|
||||||
|
# this encoding is guaranteed. It may be dependent on system locale.
|
||||||
|
return devicename.value.decode('utf-8')
|
||||||
|
|
||||||
|
def IOObjectGetClass(device):
|
||||||
|
classname = ctypes.create_string_buffer(io_name_size)
|
||||||
|
iokit.IOObjectGetClass(device, ctypes.byref(classname))
|
||||||
|
return classname.value
|
||||||
|
|
||||||
|
def GetParentDeviceByType(device, parent_type):
|
||||||
|
""" Find the first parent of a device that implements the parent_type
|
||||||
|
@param IOService Service to inspect
|
||||||
|
@return Pointer to the parent type, or None if it was not found.
|
||||||
|
"""
|
||||||
|
# First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice.
|
||||||
|
parent_type = parent_type.encode('utf-8')
|
||||||
|
while IOObjectGetClass(device) != parent_type:
|
||||||
|
parent = ctypes.c_void_p()
|
||||||
|
response = iokit.IORegistryEntryGetParentEntry(
|
||||||
|
device,
|
||||||
|
"IOService".encode("utf-8"),
|
||||||
|
ctypes.byref(parent))
|
||||||
|
# If we weren't able to find a parent for the device, we're done.
|
||||||
|
if response != KERN_SUCCESS:
|
||||||
|
return None
|
||||||
|
device = parent
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def GetIOServicesByType(service_type):
|
||||||
|
"""
|
||||||
|
returns iterator over specified service_type
|
||||||
|
"""
|
||||||
|
serial_port_iterator = ctypes.c_void_p()
|
||||||
|
|
||||||
|
iokit.IOServiceGetMatchingServices(
|
||||||
|
kIOMasterPortDefault,
|
||||||
|
iokit.IOServiceMatching(service_type.encode('utf-8')),
|
||||||
|
ctypes.byref(serial_port_iterator))
|
||||||
|
|
||||||
|
services = []
|
||||||
|
while iokit.IOIteratorIsValid(serial_port_iterator):
|
||||||
|
service = iokit.IOIteratorNext(serial_port_iterator)
|
||||||
|
if not service:
|
||||||
|
break
|
||||||
|
services.append(service)
|
||||||
|
iokit.IOObjectRelease(serial_port_iterator)
|
||||||
|
return services
|
||||||
|
|
||||||
|
|
||||||
|
def location_to_string(locationID):
|
||||||
|
"""
|
||||||
|
helper to calculate port and bus number from locationID
|
||||||
|
"""
|
||||||
|
loc = ['{}-'.format(locationID >> 24)]
|
||||||
|
while locationID & 0xf00000:
|
||||||
|
if len(loc) > 1:
|
||||||
|
loc.append('.')
|
||||||
|
loc.append('{}'.format((locationID >> 20) & 0xf))
|
||||||
|
locationID <<= 4
|
||||||
|
return ''.join(loc)
|
||||||
|
|
||||||
|
|
||||||
|
class SuitableSerialInterface(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def scan_interfaces():
|
||||||
|
"""
|
||||||
|
helper function to scan USB interfaces
|
||||||
|
returns a list of SuitableSerialInterface objects with name and id attributes
|
||||||
|
"""
|
||||||
|
interfaces = []
|
||||||
|
for service in GetIOServicesByType('IOSerialBSDClient'):
|
||||||
|
device = get_string_property(service, "IOCalloutDevice")
|
||||||
|
if device:
|
||||||
|
usb_device = GetParentDeviceByType(service, "IOUSBInterface")
|
||||||
|
if usb_device:
|
||||||
|
name = get_string_property(usb_device, "USB Interface Name") or None
|
||||||
|
locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or ''
|
||||||
|
i = SuitableSerialInterface()
|
||||||
|
i.id = locationID
|
||||||
|
i.name = name
|
||||||
|
interfaces.append(i)
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
|
||||||
|
def search_for_locationID_in_interfaces(serial_interfaces, locationID):
|
||||||
|
for interface in serial_interfaces:
|
||||||
|
if (interface.id == locationID):
|
||||||
|
return interface.name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def comports(include_links=False):
|
||||||
|
# XXX include_links is currently ignored. are links in /dev even supported here?
|
||||||
|
# Scan for all iokit serial ports
|
||||||
|
services = GetIOServicesByType('IOSerialBSDClient')
|
||||||
|
ports = []
|
||||||
|
serial_interfaces = scan_interfaces()
|
||||||
|
for service in services:
|
||||||
|
# First, add the callout device file.
|
||||||
|
device = get_string_property(service, "IOCalloutDevice")
|
||||||
|
if device:
|
||||||
|
info = list_ports_common.ListPortInfo(device)
|
||||||
|
# If the serial port is implemented by IOUSBDevice
|
||||||
|
# NOTE IOUSBDevice was deprecated as of 10.11 and finally on Apple Silicon
|
||||||
|
# devices has been completely removed. Thanks to @oskay for this patch.
|
||||||
|
usb_device = GetParentDeviceByType(service, "IOUSBHostDevice")
|
||||||
|
if not usb_device:
|
||||||
|
usb_device = GetParentDeviceByType(service, "IOUSBDevice")
|
||||||
|
if usb_device:
|
||||||
|
# fetch some useful informations from properties
|
||||||
|
info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type)
|
||||||
|
info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type)
|
||||||
|
info.serial_number = get_string_property(usb_device, kUSBSerialNumberString)
|
||||||
|
# We know this is a usb device, so the
|
||||||
|
# IORegistryEntryName should always be aliased to the
|
||||||
|
# usb product name string descriptor.
|
||||||
|
info.product = IORegistryEntryGetName(usb_device) or 'n/a'
|
||||||
|
info.manufacturer = get_string_property(usb_device, kUSBVendorString)
|
||||||
|
locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type)
|
||||||
|
info.location = location_to_string(locationID)
|
||||||
|
info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID)
|
||||||
|
info.apply_usb_info()
|
||||||
|
ports.append(info)
|
||||||
|
return ports
|
||||||
|
|
||||||
|
# test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for port, desc, hwid in sorted(comports()):
|
||||||
|
print("{}: {} [{}]".format(port, desc, hwid))
|
||||||
119
deps/serial/tools/list_ports_posix.py
vendored
Normal file
119
deps/serial/tools/list_ports_posix.py
vendored
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# This is a module that gathers a list of serial ports on POSIXy systems.
|
||||||
|
# For some specific implementations, see also list_ports_linux, list_ports_osx
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
"""\
|
||||||
|
The ``comports`` function is expected to return an iterable that yields tuples
|
||||||
|
of 3 strings: port name, human readable description and a hardware ID.
|
||||||
|
|
||||||
|
As currently no method is known to get the second two strings easily, they are
|
||||||
|
currently just identical to the port name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from serial.tools import list_ports_common
|
||||||
|
|
||||||
|
# try to detect the OS so that a device can be selected...
|
||||||
|
plat = sys.platform.lower()
|
||||||
|
|
||||||
|
if plat[:5] == 'linux': # Linux (confirmed) # noqa
|
||||||
|
from serial.tools.list_ports_linux import comports
|
||||||
|
|
||||||
|
elif plat[:6] == 'darwin': # OS X (confirmed)
|
||||||
|
from serial.tools.list_ports_osx import comports
|
||||||
|
|
||||||
|
elif plat == 'cygwin': # cygwin/win32
|
||||||
|
# cygwin accepts /dev/com* in many contexts
|
||||||
|
# (such as 'open' call, explicit 'ls'), but 'glob.glob'
|
||||||
|
# and bare 'ls' do not; so use /dev/ttyS* instead
|
||||||
|
def comports(include_links=False):
|
||||||
|
devices = glob.glob('/dev/ttyS*')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
elif plat[:7] == 'openbsd': # OpenBSD
|
||||||
|
def comports(include_links=False):
|
||||||
|
devices = glob.glob('/dev/cua*')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
elif plat[:3] == 'bsd' or plat[:7] == 'freebsd':
|
||||||
|
def comports(include_links=False):
|
||||||
|
devices = glob.glob('/dev/cua*[!.init][!.lock]')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
elif plat[:6] == 'netbsd': # NetBSD
|
||||||
|
def comports(include_links=False):
|
||||||
|
"""scan for available ports. return a list of device names."""
|
||||||
|
devices = glob.glob('/dev/dty*')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
elif plat[:4] == 'irix': # IRIX
|
||||||
|
def comports(include_links=False):
|
||||||
|
"""scan for available ports. return a list of device names."""
|
||||||
|
devices = glob.glob('/dev/ttyf*')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
elif plat[:2] == 'hp': # HP-UX (not tested)
|
||||||
|
def comports(include_links=False):
|
||||||
|
"""scan for available ports. return a list of device names."""
|
||||||
|
devices = glob.glob('/dev/tty*p0')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
elif plat[:5] == 'sunos': # Solaris/SunOS
|
||||||
|
def comports(include_links=False):
|
||||||
|
"""scan for available ports. return a list of device names."""
|
||||||
|
devices = glob.glob('/dev/tty*c')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
elif plat[:3] == 'aix': # AIX
|
||||||
|
def comports(include_links=False):
|
||||||
|
"""scan for available ports. return a list of device names."""
|
||||||
|
devices = glob.glob('/dev/tty*')
|
||||||
|
if include_links:
|
||||||
|
devices.extend(list_ports_common.list_links(devices))
|
||||||
|
return [list_ports_common.ListPortInfo(d) for d in devices]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# platform detection has failed...
|
||||||
|
import serial
|
||||||
|
sys.stderr.write("""\
|
||||||
|
don't know how to enumerate ttys on this system.
|
||||||
|
! I you know how the serial ports are named send this information to
|
||||||
|
! the author of this module:
|
||||||
|
|
||||||
|
sys.platform = {!r}
|
||||||
|
os.name = {!r}
|
||||||
|
pySerial version = {}
|
||||||
|
|
||||||
|
also add the naming scheme of the serial ports and with a bit luck you can get
|
||||||
|
this module running...
|
||||||
|
""".format(sys.platform, os.name, serial.VERSION))
|
||||||
|
raise ImportError("Sorry: no implementation for your platform ('{}') available".format(os.name))
|
||||||
|
|
||||||
|
# test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for port, desc, hwid in sorted(comports()):
|
||||||
|
print("{}: {} [{}]".format(port, desc, hwid))
|
||||||
427
deps/serial/tools/list_ports_windows.py
vendored
Normal file
427
deps/serial/tools/list_ports_windows.py
vendored
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# Enumerate serial ports on Windows including a human readable description
|
||||||
|
# and hardware information.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2016 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name,too-few-public-methods
|
||||||
|
import re
|
||||||
|
import ctypes
|
||||||
|
from ctypes.wintypes import BOOL
|
||||||
|
from ctypes.wintypes import HWND
|
||||||
|
from ctypes.wintypes import DWORD
|
||||||
|
from ctypes.wintypes import WORD
|
||||||
|
from ctypes.wintypes import LONG
|
||||||
|
from ctypes.wintypes import ULONG
|
||||||
|
from ctypes.wintypes import HKEY
|
||||||
|
from ctypes.wintypes import BYTE
|
||||||
|
import serial
|
||||||
|
from serial.win32 import ULONG_PTR
|
||||||
|
from serial.tools import list_ports_common
|
||||||
|
|
||||||
|
|
||||||
|
def ValidHandle(value, func, arguments):
|
||||||
|
if value == 0:
|
||||||
|
raise ctypes.WinError()
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
NULL = 0
|
||||||
|
HDEVINFO = ctypes.c_void_p
|
||||||
|
LPCTSTR = ctypes.c_wchar_p
|
||||||
|
PCTSTR = ctypes.c_wchar_p
|
||||||
|
PTSTR = ctypes.c_wchar_p
|
||||||
|
LPDWORD = PDWORD = ctypes.POINTER(DWORD)
|
||||||
|
#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE)
|
||||||
|
LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types
|
||||||
|
|
||||||
|
ACCESS_MASK = DWORD
|
||||||
|
REGSAM = ACCESS_MASK
|
||||||
|
|
||||||
|
|
||||||
|
class GUID(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('Data1', DWORD),
|
||||||
|
('Data2', WORD),
|
||||||
|
('Data3', WORD),
|
||||||
|
('Data4', BYTE * 8),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format(
|
||||||
|
self.Data1,
|
||||||
|
self.Data2,
|
||||||
|
self.Data3,
|
||||||
|
''.join(["{:02x}".format(d) for d in self.Data4[:2]]),
|
||||||
|
''.join(["{:02x}".format(d) for d in self.Data4[2:]]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SP_DEVINFO_DATA(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('cbSize', DWORD),
|
||||||
|
('ClassGuid', GUID),
|
||||||
|
('DevInst', DWORD),
|
||||||
|
('Reserved', ULONG_PTR),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst)
|
||||||
|
|
||||||
|
|
||||||
|
PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA)
|
||||||
|
|
||||||
|
PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p
|
||||||
|
|
||||||
|
setupapi = ctypes.windll.LoadLibrary("setupapi")
|
||||||
|
SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList
|
||||||
|
SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO]
|
||||||
|
SetupDiDestroyDeviceInfoList.restype = BOOL
|
||||||
|
|
||||||
|
SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW
|
||||||
|
SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD]
|
||||||
|
SetupDiClassGuidsFromName.restype = BOOL
|
||||||
|
|
||||||
|
SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo
|
||||||
|
SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA]
|
||||||
|
SetupDiEnumDeviceInfo.restype = BOOL
|
||||||
|
|
||||||
|
SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW
|
||||||
|
SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD]
|
||||||
|
SetupDiGetClassDevs.restype = HDEVINFO
|
||||||
|
SetupDiGetClassDevs.errcheck = ValidHandle
|
||||||
|
|
||||||
|
SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW
|
||||||
|
SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD]
|
||||||
|
SetupDiGetDeviceRegistryProperty.restype = BOOL
|
||||||
|
|
||||||
|
SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW
|
||||||
|
SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD]
|
||||||
|
SetupDiGetDeviceInstanceId.restype = BOOL
|
||||||
|
|
||||||
|
SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey
|
||||||
|
SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM]
|
||||||
|
SetupDiOpenDevRegKey.restype = HKEY
|
||||||
|
|
||||||
|
advapi32 = ctypes.windll.LoadLibrary("Advapi32")
|
||||||
|
RegCloseKey = advapi32.RegCloseKey
|
||||||
|
RegCloseKey.argtypes = [HKEY]
|
||||||
|
RegCloseKey.restype = LONG
|
||||||
|
|
||||||
|
RegQueryValueEx = advapi32.RegQueryValueExW
|
||||||
|
RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD]
|
||||||
|
RegQueryValueEx.restype = LONG
|
||||||
|
|
||||||
|
cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32")
|
||||||
|
CM_Get_Parent = cfgmgr32.CM_Get_Parent
|
||||||
|
CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG]
|
||||||
|
CM_Get_Parent.restype = LONG
|
||||||
|
|
||||||
|
CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW
|
||||||
|
CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG]
|
||||||
|
CM_Get_Device_IDW.restype = LONG
|
||||||
|
|
||||||
|
CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err
|
||||||
|
CM_MapCrToWin32Err.argtypes = [DWORD, DWORD]
|
||||||
|
CM_MapCrToWin32Err.restype = DWORD
|
||||||
|
|
||||||
|
|
||||||
|
DIGCF_PRESENT = 2
|
||||||
|
DIGCF_DEVICEINTERFACE = 16
|
||||||
|
INVALID_HANDLE_VALUE = 0
|
||||||
|
ERROR_INSUFFICIENT_BUFFER = 122
|
||||||
|
ERROR_NOT_FOUND = 1168
|
||||||
|
SPDRP_HARDWAREID = 1
|
||||||
|
SPDRP_FRIENDLYNAME = 12
|
||||||
|
SPDRP_LOCATION_PATHS = 35
|
||||||
|
SPDRP_MFG = 11
|
||||||
|
DICS_FLAG_GLOBAL = 1
|
||||||
|
DIREG_DEV = 0x00000001
|
||||||
|
KEY_READ = 0x20019
|
||||||
|
|
||||||
|
|
||||||
|
MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5
|
||||||
|
|
||||||
|
|
||||||
|
def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0, last_serial_number=None):
|
||||||
|
""" Get the serial number of the parent of a device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
child_devinst: The device instance handle to get the parent serial number of.
|
||||||
|
child_vid: The vendor ID of the child device.
|
||||||
|
child_pid: The product ID of the child device.
|
||||||
|
depth: The current iteration depth of the USB device tree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If the traversal depth is beyond the max, abandon attempting to find the serial number.
|
||||||
|
if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH:
|
||||||
|
return '' if not last_serial_number else last_serial_number
|
||||||
|
|
||||||
|
# Get the parent device instance.
|
||||||
|
devinst = DWORD()
|
||||||
|
ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0)
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0))
|
||||||
|
|
||||||
|
# If there is no parent available, the child was the root device. We cannot traverse
|
||||||
|
# further.
|
||||||
|
if win_error == ERROR_NOT_FOUND:
|
||||||
|
return '' if not last_serial_number else last_serial_number
|
||||||
|
|
||||||
|
raise ctypes.WinError(win_error)
|
||||||
|
|
||||||
|
# Get the ID of the parent device and parse it for vendor ID, product ID, and serial number.
|
||||||
|
parentHardwareID = ctypes.create_unicode_buffer(250)
|
||||||
|
|
||||||
|
ret = CM_Get_Device_IDW(
|
||||||
|
devinst,
|
||||||
|
parentHardwareID,
|
||||||
|
ctypes.sizeof(parentHardwareID) - 1,
|
||||||
|
0)
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0)))
|
||||||
|
|
||||||
|
parentHardwareID_str = parentHardwareID.value
|
||||||
|
m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?',
|
||||||
|
parentHardwareID_str,
|
||||||
|
re.I)
|
||||||
|
|
||||||
|
# return early if we have no matches (likely malformed serial, traversed too far)
|
||||||
|
if not m:
|
||||||
|
return '' if not last_serial_number else last_serial_number
|
||||||
|
|
||||||
|
vid = None
|
||||||
|
pid = None
|
||||||
|
serial_number = None
|
||||||
|
if m.group(1):
|
||||||
|
vid = int(m.group(1), 16)
|
||||||
|
if m.group(3):
|
||||||
|
pid = int(m.group(3), 16)
|
||||||
|
if m.group(7):
|
||||||
|
serial_number = m.group(7)
|
||||||
|
|
||||||
|
# store what we found as a fallback for malformed serial values up the chain
|
||||||
|
found_serial_number = serial_number
|
||||||
|
|
||||||
|
# Check that the USB serial number only contains alpha-numeric characters. It may be a windows
|
||||||
|
# device ID (ephemeral ID).
|
||||||
|
if serial_number and not re.match(r'^\w+$', serial_number):
|
||||||
|
serial_number = None
|
||||||
|
|
||||||
|
if not vid or not pid:
|
||||||
|
# If pid and vid are not available at this device level, continue to the parent.
|
||||||
|
return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
|
||||||
|
|
||||||
|
if pid != child_pid or vid != child_vid:
|
||||||
|
# If the VID or PID has changed, we are no longer looking at the same physical device. The
|
||||||
|
# serial number is unknown.
|
||||||
|
return '' if not last_serial_number else last_serial_number
|
||||||
|
|
||||||
|
# In this case, the vid and pid of the parent device are identical to the child. However, if
|
||||||
|
# there still isn't a serial number available, continue to the next parent.
|
||||||
|
if not serial_number:
|
||||||
|
return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
|
||||||
|
|
||||||
|
# Finally, the VID and PID are identical to the child and a serial number is present, so return
|
||||||
|
# it.
|
||||||
|
return serial_number
|
||||||
|
|
||||||
|
|
||||||
|
def iterate_comports():
|
||||||
|
"""Return a generator that yields descriptions for serial ports"""
|
||||||
|
PortsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
|
||||||
|
ports_guids_size = DWORD()
|
||||||
|
if not SetupDiClassGuidsFromName(
|
||||||
|
"Ports",
|
||||||
|
PortsGUIDs,
|
||||||
|
ctypes.sizeof(PortsGUIDs),
|
||||||
|
ctypes.byref(ports_guids_size)):
|
||||||
|
raise ctypes.WinError()
|
||||||
|
|
||||||
|
ModemsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough...
|
||||||
|
modems_guids_size = DWORD()
|
||||||
|
if not SetupDiClassGuidsFromName(
|
||||||
|
"Modem",
|
||||||
|
ModemsGUIDs,
|
||||||
|
ctypes.sizeof(ModemsGUIDs),
|
||||||
|
ctypes.byref(modems_guids_size)):
|
||||||
|
raise ctypes.WinError()
|
||||||
|
|
||||||
|
GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value]
|
||||||
|
|
||||||
|
# repeat for all possible GUIDs
|
||||||
|
for index in range(len(GUIDs)):
|
||||||
|
bInterfaceNumber = None
|
||||||
|
g_hdi = SetupDiGetClassDevs(
|
||||||
|
ctypes.byref(GUIDs[index]),
|
||||||
|
None,
|
||||||
|
NULL,
|
||||||
|
DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports
|
||||||
|
|
||||||
|
devinfo = SP_DEVINFO_DATA()
|
||||||
|
devinfo.cbSize = ctypes.sizeof(devinfo)
|
||||||
|
index = 0
|
||||||
|
while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)):
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
# get the real com port name
|
||||||
|
hkey = SetupDiOpenDevRegKey(
|
||||||
|
g_hdi,
|
||||||
|
ctypes.byref(devinfo),
|
||||||
|
DICS_FLAG_GLOBAL,
|
||||||
|
0,
|
||||||
|
DIREG_DEV, # DIREG_DRV for SW info
|
||||||
|
KEY_READ)
|
||||||
|
port_name_buffer = ctypes.create_unicode_buffer(250)
|
||||||
|
port_name_length = ULONG(ctypes.sizeof(port_name_buffer))
|
||||||
|
RegQueryValueEx(
|
||||||
|
hkey,
|
||||||
|
"PortName",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
ctypes.byref(port_name_buffer),
|
||||||
|
ctypes.byref(port_name_length))
|
||||||
|
RegCloseKey(hkey)
|
||||||
|
|
||||||
|
# unfortunately does this method also include parallel ports.
|
||||||
|
# we could check for names starting with COM or just exclude LPT
|
||||||
|
# and hope that other "unknown" names are serial ports...
|
||||||
|
if port_name_buffer.value.startswith('LPT'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# hardware ID
|
||||||
|
szHardwareID = ctypes.create_unicode_buffer(250)
|
||||||
|
# try to get ID that includes serial number
|
||||||
|
if not SetupDiGetDeviceInstanceId(
|
||||||
|
g_hdi,
|
||||||
|
ctypes.byref(devinfo),
|
||||||
|
#~ ctypes.byref(szHardwareID),
|
||||||
|
szHardwareID,
|
||||||
|
ctypes.sizeof(szHardwareID) - 1,
|
||||||
|
None):
|
||||||
|
# fall back to more generic hardware ID if that would fail
|
||||||
|
if not SetupDiGetDeviceRegistryProperty(
|
||||||
|
g_hdi,
|
||||||
|
ctypes.byref(devinfo),
|
||||||
|
SPDRP_HARDWAREID,
|
||||||
|
None,
|
||||||
|
ctypes.byref(szHardwareID),
|
||||||
|
ctypes.sizeof(szHardwareID) - 1,
|
||||||
|
None):
|
||||||
|
# Ignore ERROR_INSUFFICIENT_BUFFER
|
||||||
|
if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
|
||||||
|
raise ctypes.WinError()
|
||||||
|
# stringify
|
||||||
|
szHardwareID_str = szHardwareID.value
|
||||||
|
|
||||||
|
info = list_ports_common.ListPortInfo(port_name_buffer.value, skip_link_detection=True)
|
||||||
|
|
||||||
|
# in case of USB, make a more readable string, similar to that form
|
||||||
|
# that we also generate on other platforms
|
||||||
|
if szHardwareID_str.startswith('USB'):
|
||||||
|
m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I)
|
||||||
|
if m:
|
||||||
|
info.vid = int(m.group(1), 16)
|
||||||
|
if m.group(3):
|
||||||
|
info.pid = int(m.group(3), 16)
|
||||||
|
if m.group(5):
|
||||||
|
bInterfaceNumber = int(m.group(5))
|
||||||
|
|
||||||
|
# Check that the USB serial number only contains alpha-numeric characters. It
|
||||||
|
# may be a windows device ID (ephemeral ID) for composite devices.
|
||||||
|
if m.group(7) and re.match(r'^\w+$', m.group(7)):
|
||||||
|
info.serial_number = m.group(7)
|
||||||
|
else:
|
||||||
|
info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid)
|
||||||
|
|
||||||
|
# calculate a location string
|
||||||
|
loc_path_str = ctypes.create_unicode_buffer(250)
|
||||||
|
if SetupDiGetDeviceRegistryProperty(
|
||||||
|
g_hdi,
|
||||||
|
ctypes.byref(devinfo),
|
||||||
|
SPDRP_LOCATION_PATHS,
|
||||||
|
None,
|
||||||
|
ctypes.byref(loc_path_str),
|
||||||
|
ctypes.sizeof(loc_path_str) - 1,
|
||||||
|
None):
|
||||||
|
m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value)
|
||||||
|
location = []
|
||||||
|
for g in m:
|
||||||
|
if g.group(1):
|
||||||
|
location.append('{:d}'.format(int(g.group(1)) + 1))
|
||||||
|
else:
|
||||||
|
if len(location) > 1:
|
||||||
|
location.append('.')
|
||||||
|
else:
|
||||||
|
location.append('-')
|
||||||
|
location.append(g.group(2))
|
||||||
|
if bInterfaceNumber is not None:
|
||||||
|
location.append(':{}.{}'.format(
|
||||||
|
'x', # XXX how to determine correct bConfigurationValue?
|
||||||
|
bInterfaceNumber))
|
||||||
|
if location:
|
||||||
|
info.location = ''.join(location)
|
||||||
|
info.hwid = info.usb_info()
|
||||||
|
elif szHardwareID_str.startswith('FTDIBUS'):
|
||||||
|
m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I)
|
||||||
|
if m:
|
||||||
|
info.vid = int(m.group(1), 16)
|
||||||
|
info.pid = int(m.group(2), 16)
|
||||||
|
if m.group(4):
|
||||||
|
info.serial_number = m.group(4)
|
||||||
|
# USB location is hidden by FDTI driver :(
|
||||||
|
info.hwid = info.usb_info()
|
||||||
|
else:
|
||||||
|
info.hwid = szHardwareID_str
|
||||||
|
|
||||||
|
# friendly name
|
||||||
|
szFriendlyName = ctypes.create_unicode_buffer(250)
|
||||||
|
if SetupDiGetDeviceRegistryProperty(
|
||||||
|
g_hdi,
|
||||||
|
ctypes.byref(devinfo),
|
||||||
|
SPDRP_FRIENDLYNAME,
|
||||||
|
#~ SPDRP_DEVICEDESC,
|
||||||
|
None,
|
||||||
|
ctypes.byref(szFriendlyName),
|
||||||
|
ctypes.sizeof(szFriendlyName) - 1,
|
||||||
|
None):
|
||||||
|
info.description = szFriendlyName.value
|
||||||
|
#~ else:
|
||||||
|
# Ignore ERROR_INSUFFICIENT_BUFFER
|
||||||
|
#~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
|
||||||
|
#~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value))
|
||||||
|
# ignore errors and still include the port in the list, friendly name will be same as port name
|
||||||
|
|
||||||
|
# manufacturer
|
||||||
|
szManufacturer = ctypes.create_unicode_buffer(250)
|
||||||
|
if SetupDiGetDeviceRegistryProperty(
|
||||||
|
g_hdi,
|
||||||
|
ctypes.byref(devinfo),
|
||||||
|
SPDRP_MFG,
|
||||||
|
#~ SPDRP_DEVICEDESC,
|
||||||
|
None,
|
||||||
|
ctypes.byref(szManufacturer),
|
||||||
|
ctypes.sizeof(szManufacturer) - 1,
|
||||||
|
None):
|
||||||
|
info.manufacturer = szManufacturer.value
|
||||||
|
yield info
|
||||||
|
SetupDiDestroyDeviceInfoList(g_hdi)
|
||||||
|
|
||||||
|
|
||||||
|
def comports(include_links=False):
|
||||||
|
"""Return a list of info objects about serial ports"""
|
||||||
|
return list(iterate_comports())
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for port, desc, hwid in sorted(comports()):
|
||||||
|
print("{}: {} [{}]".format(port, desc, hwid))
|
||||||
1042
deps/serial/tools/miniterm.py
vendored
Normal file
1042
deps/serial/tools/miniterm.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
0
deps/serial/urlhandler/__init__.py
vendored
Normal file
0
deps/serial/urlhandler/__init__.py
vendored
Normal file
57
deps/serial/urlhandler/protocol_alt.py
vendored
Normal file
57
deps/serial/urlhandler/protocol_alt.py
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# This module implements a special URL handler that allows selecting an
|
||||||
|
# alternate implementation provided by some backends.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
#
|
||||||
|
# URL format: alt://port[?option[=value][&option[=value]]]
|
||||||
|
# options:
|
||||||
|
# - class=X used class named X instead of Serial
|
||||||
|
#
|
||||||
|
# example:
|
||||||
|
# use poll based implementation on Posix (Linux):
|
||||||
|
# python -m serial.tools.miniterm alt:///dev/ttyUSB0?class=PosixPollSerial
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
import serial
|
||||||
|
|
||||||
|
|
||||||
|
def serial_class_for_url(url):
|
||||||
|
"""extract host and port from an URL string"""
|
||||||
|
parts = urlparse.urlsplit(url)
|
||||||
|
if parts.scheme != 'alt':
|
||||||
|
raise serial.SerialException(
|
||||||
|
'expected a string in the form "alt://port[?option[=value][&option[=value]]]": '
|
||||||
|
'not starting with alt:// ({!r})'.format(parts.scheme))
|
||||||
|
class_name = 'Serial'
|
||||||
|
try:
|
||||||
|
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||||
|
if option == 'class':
|
||||||
|
class_name = values[0]
|
||||||
|
else:
|
||||||
|
raise ValueError('unknown option: {!r}'.format(option))
|
||||||
|
except ValueError as e:
|
||||||
|
raise serial.SerialException(
|
||||||
|
'expected a string in the form '
|
||||||
|
'"alt://port[?option[=value][&option[=value]]]": {!r}'.format(e))
|
||||||
|
if not hasattr(serial, class_name):
|
||||||
|
raise ValueError('unknown class: {!r}'.format(class_name))
|
||||||
|
cls = getattr(serial, class_name)
|
||||||
|
if not issubclass(cls, serial.Serial):
|
||||||
|
raise ValueError('class {!r} is not an instance of Serial'.format(class_name))
|
||||||
|
return (''.join([parts.netloc, parts.path]), cls)
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
if __name__ == '__main__':
|
||||||
|
s = serial.serial_for_url('alt:///dev/ttyS0?class=PosixPollSerial')
|
||||||
|
print(s)
|
||||||
258
deps/serial/urlhandler/protocol_cp2110.py
vendored
Normal file
258
deps/serial/urlhandler/protocol_cp2110.py
vendored
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# Backend for Silicon Labs CP2110/4 HID-to-UART devices.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
# (C) 2019 Google LLC
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
# This backend implements support for HID-to-UART devices manufactured
|
||||||
|
# by Silicon Labs and marketed as CP2110 and CP2114. The
|
||||||
|
# implementation is (mostly) OS-independent and in userland. It relies
|
||||||
|
# on cython-hidapi (https://github.com/trezor/cython-hidapi).
|
||||||
|
|
||||||
|
# The HID-to-UART protocol implemented by CP2110/4 is described in the
|
||||||
|
# AN434 document from Silicon Labs:
|
||||||
|
# https://www.silabs.com/documents/public/application-notes/AN434-CP2110-4-Interface-Specification.pdf
|
||||||
|
|
||||||
|
# TODO items:
|
||||||
|
|
||||||
|
# - rtscts support is configured for hardware flow control, but the
|
||||||
|
# signaling is missing (AN434 suggests this is done through GPIO).
|
||||||
|
# - Cancelling reads and writes is not supported.
|
||||||
|
# - Baudrate validation is not implemented, as it depends on model and configuration.
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
import Queue
|
||||||
|
except ImportError:
|
||||||
|
import queue as Queue
|
||||||
|
|
||||||
|
import hid # hidapi
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.serialutil import SerialBase, SerialException, PortNotOpenError, to_bytes, Timeout
|
||||||
|
|
||||||
|
|
||||||
|
# Report IDs and related constant
|
||||||
|
_REPORT_GETSET_UART_ENABLE = 0x41
|
||||||
|
_DISABLE_UART = 0x00
|
||||||
|
_ENABLE_UART = 0x01
|
||||||
|
|
||||||
|
_REPORT_SET_PURGE_FIFOS = 0x43
|
||||||
|
_PURGE_TX_FIFO = 0x01
|
||||||
|
_PURGE_RX_FIFO = 0x02
|
||||||
|
|
||||||
|
_REPORT_GETSET_UART_CONFIG = 0x50
|
||||||
|
|
||||||
|
_REPORT_SET_TRANSMIT_LINE_BREAK = 0x51
|
||||||
|
_REPORT_SET_STOP_LINE_BREAK = 0x52
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(SerialBase):
|
||||||
|
# This is not quite correct. AN343 specifies that the minimum
|
||||||
|
# baudrate is different between CP2110 and CP2114, and it's halved
|
||||||
|
# when using non-8-bit symbols.
|
||||||
|
BAUDRATES = (300, 375, 600, 1200, 1800, 2400, 4800, 9600, 19200,
|
||||||
|
38400, 57600, 115200, 230400, 460800, 500000, 576000,
|
||||||
|
921600, 1000000)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._hid_handle = None
|
||||||
|
self._read_buffer = None
|
||||||
|
self._thread = None
|
||||||
|
super(Serial, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
if self._port is None:
|
||||||
|
raise SerialException("Port must be configured before it can be used.")
|
||||||
|
if self.is_open:
|
||||||
|
raise SerialException("Port is already open.")
|
||||||
|
|
||||||
|
self._read_buffer = Queue.Queue()
|
||||||
|
|
||||||
|
self._hid_handle = hid.device()
|
||||||
|
try:
|
||||||
|
portpath = self.from_url(self.portstr)
|
||||||
|
self._hid_handle.open_path(portpath)
|
||||||
|
except OSError as msg:
|
||||||
|
raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._reconfigure_port()
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
self._hid_handle.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self._hid_handle = None
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.is_open = True
|
||||||
|
self._thread = threading.Thread(target=self._hid_read_loop)
|
||||||
|
self._thread.setDaemon(True)
|
||||||
|
self._thread.setName('pySerial CP2110 reader thread for {}'.format(self._port))
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def from_url(self, url):
|
||||||
|
parts = urlparse.urlsplit(url)
|
||||||
|
if parts.scheme != "cp2110":
|
||||||
|
raise SerialException(
|
||||||
|
'expected a string in the forms '
|
||||||
|
'"cp2110:///dev/hidraw9" or "cp2110://0001:0023:00": '
|
||||||
|
'not starting with cp2110:// {{!r}}'.format(parts.scheme))
|
||||||
|
if parts.netloc: # cp2100://BUS:DEVICE:ENDPOINT, for libusb
|
||||||
|
return parts.netloc.encode('utf-8')
|
||||||
|
return parts.path.encode('utf-8')
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.is_open = False
|
||||||
|
if self._thread:
|
||||||
|
self._thread.join(1) # read timeout is 0.1
|
||||||
|
self._thread = None
|
||||||
|
self._hid_handle.close()
|
||||||
|
self._hid_handle = None
|
||||||
|
|
||||||
|
def _reconfigure_port(self):
|
||||||
|
parity_value = None
|
||||||
|
if self._parity == serial.PARITY_NONE:
|
||||||
|
parity_value = 0x00
|
||||||
|
elif self._parity == serial.PARITY_ODD:
|
||||||
|
parity_value = 0x01
|
||||||
|
elif self._parity == serial.PARITY_EVEN:
|
||||||
|
parity_value = 0x02
|
||||||
|
elif self._parity == serial.PARITY_MARK:
|
||||||
|
parity_value = 0x03
|
||||||
|
elif self._parity == serial.PARITY_SPACE:
|
||||||
|
parity_value = 0x04
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid parity: {!r}'.format(self._parity))
|
||||||
|
|
||||||
|
if self.rtscts:
|
||||||
|
flow_control_value = 0x01
|
||||||
|
else:
|
||||||
|
flow_control_value = 0x00
|
||||||
|
|
||||||
|
data_bits_value = None
|
||||||
|
if self._bytesize == 5:
|
||||||
|
data_bits_value = 0x00
|
||||||
|
elif self._bytesize == 6:
|
||||||
|
data_bits_value = 0x01
|
||||||
|
elif self._bytesize == 7:
|
||||||
|
data_bits_value = 0x02
|
||||||
|
elif self._bytesize == 8:
|
||||||
|
data_bits_value = 0x03
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
|
||||||
|
|
||||||
|
stop_bits_value = None
|
||||||
|
if self._stopbits == serial.STOPBITS_ONE:
|
||||||
|
stop_bits_value = 0x00
|
||||||
|
elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
|
||||||
|
stop_bits_value = 0x01
|
||||||
|
elif self._stopbits == serial.STOPBITS_TWO:
|
||||||
|
stop_bits_value = 0x01
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
|
||||||
|
|
||||||
|
configuration_report = struct.pack(
|
||||||
|
'>BLBBBB',
|
||||||
|
_REPORT_GETSET_UART_CONFIG,
|
||||||
|
self._baudrate,
|
||||||
|
parity_value,
|
||||||
|
flow_control_value,
|
||||||
|
data_bits_value,
|
||||||
|
stop_bits_value)
|
||||||
|
|
||||||
|
self._hid_handle.send_feature_report(configuration_report)
|
||||||
|
|
||||||
|
self._hid_handle.send_feature_report(
|
||||||
|
bytes((_REPORT_GETSET_UART_ENABLE, _ENABLE_UART)))
|
||||||
|
self._update_break_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
return self._read_buffer.qsize()
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._hid_handle.send_feature_report(
|
||||||
|
bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_RX_FIFO)))
|
||||||
|
# empty read buffer
|
||||||
|
while self._read_buffer.qsize():
|
||||||
|
self._read_buffer.get(False)
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
self._hid_handle.send_feature_report(
|
||||||
|
bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_TX_FIFO)))
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
if not self._hid_handle:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
|
||||||
|
if self._break_state:
|
||||||
|
self._hid_handle.send_feature_report(
|
||||||
|
bytes((_REPORT_SET_TRANSMIT_LINE_BREAK, 0)))
|
||||||
|
else:
|
||||||
|
# Note that while AN434 states "There are no data bytes in
|
||||||
|
# the payload other than the Report ID", either hidapi or
|
||||||
|
# Linux does not seem to send the report otherwise.
|
||||||
|
self._hid_handle.send_feature_report(
|
||||||
|
bytes((_REPORT_SET_STOP_LINE_BREAK, 0)))
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
|
||||||
|
data = bytearray()
|
||||||
|
try:
|
||||||
|
timeout = Timeout(self._timeout)
|
||||||
|
while len(data) < size:
|
||||||
|
if self._thread is None:
|
||||||
|
raise SerialException('connection failed (reader thread died)')
|
||||||
|
buf = self._read_buffer.get(True, timeout.time_left())
|
||||||
|
if buf is None:
|
||||||
|
return bytes(data)
|
||||||
|
data += buf
|
||||||
|
if timeout.expired():
|
||||||
|
break
|
||||||
|
except Queue.Empty: # -> timeout
|
||||||
|
pass
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
data = to_bytes(data)
|
||||||
|
tx_len = len(data)
|
||||||
|
while tx_len > 0:
|
||||||
|
to_be_sent = min(tx_len, 0x3F)
|
||||||
|
report = to_bytes([to_be_sent]) + data[:to_be_sent]
|
||||||
|
self._hid_handle.write(report)
|
||||||
|
|
||||||
|
data = data[to_be_sent:]
|
||||||
|
tx_len = len(data)
|
||||||
|
|
||||||
|
def _hid_read_loop(self):
|
||||||
|
try:
|
||||||
|
while self.is_open:
|
||||||
|
data = self._hid_handle.read(64, timeout_ms=100)
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
data_len = data.pop(0)
|
||||||
|
assert data_len == len(data)
|
||||||
|
self._read_buffer.put(bytearray(data))
|
||||||
|
finally:
|
||||||
|
self._thread = None
|
||||||
91
deps/serial/urlhandler/protocol_hwgrep.py
vendored
Normal file
91
deps/serial/urlhandler/protocol_hwgrep.py
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# This module implements a special URL handler that uses the port listing to
|
||||||
|
# find ports by searching the string descriptions.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
#
|
||||||
|
# URL format: hwgrep://<regexp>&<option>
|
||||||
|
#
|
||||||
|
# where <regexp> is a Python regexp according to the re module
|
||||||
|
#
|
||||||
|
# violating the normal definition for URLs, the charachter `&` is used to
|
||||||
|
# separate parameters from the arguments (instead of `?`, but the question mark
|
||||||
|
# is heavily used in regexp'es)
|
||||||
|
#
|
||||||
|
# options:
|
||||||
|
# n=<N> pick the N'th entry instead of the first one (numbering starts at 1)
|
||||||
|
# skip_busy tries to open port to check if it is busy, fails on posix as ports are not locked!
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
|
||||||
|
try:
|
||||||
|
basestring
|
||||||
|
except NameError:
|
||||||
|
basestring = str # python 3 pylint: disable=redefined-builtin
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(serial.Serial):
|
||||||
|
"""Just inherit the native Serial port implementation and patch the port property."""
|
||||||
|
# pylint: disable=no-member
|
||||||
|
|
||||||
|
@serial.Serial.port.setter
|
||||||
|
def port(self, value):
|
||||||
|
"""translate port name before storing it"""
|
||||||
|
if isinstance(value, basestring) and value.startswith('hwgrep://'):
|
||||||
|
serial.Serial.port.__set__(self, self.from_url(value))
|
||||||
|
else:
|
||||||
|
serial.Serial.port.__set__(self, value)
|
||||||
|
|
||||||
|
def from_url(self, url):
|
||||||
|
"""extract host and port from an URL string"""
|
||||||
|
if url.lower().startswith("hwgrep://"):
|
||||||
|
url = url[9:]
|
||||||
|
n = 0
|
||||||
|
test_open = False
|
||||||
|
args = url.split('&')
|
||||||
|
regexp = args.pop(0)
|
||||||
|
for arg in args:
|
||||||
|
if '=' in arg:
|
||||||
|
option, value = arg.split('=', 1)
|
||||||
|
else:
|
||||||
|
option = arg
|
||||||
|
value = None
|
||||||
|
if option == 'n':
|
||||||
|
# pick n'th element
|
||||||
|
n = int(value) - 1
|
||||||
|
if n < 1:
|
||||||
|
raise ValueError('option "n" expects a positive integer larger than 1: {!r}'.format(value))
|
||||||
|
elif option == 'skip_busy':
|
||||||
|
# open to test if port is available. not the nicest way..
|
||||||
|
test_open = True
|
||||||
|
else:
|
||||||
|
raise ValueError('unknown option: {!r}'.format(option))
|
||||||
|
# use a for loop to get the 1st element from the generator
|
||||||
|
for port, desc, hwid in sorted(serial.tools.list_ports.grep(regexp)):
|
||||||
|
if test_open:
|
||||||
|
try:
|
||||||
|
s = serial.Serial(port)
|
||||||
|
except serial.SerialException:
|
||||||
|
# it has some error, skip this one
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
s.close()
|
||||||
|
if n:
|
||||||
|
n -= 1
|
||||||
|
continue
|
||||||
|
return port
|
||||||
|
else:
|
||||||
|
raise serial.SerialException('no ports found matching regexp {!r}'.format(url))
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
if __name__ == '__main__':
|
||||||
|
s = Serial(None)
|
||||||
|
s.port = 'hwgrep://ttyS0'
|
||||||
|
print(s)
|
||||||
308
deps/serial/urlhandler/protocol_loop.py
vendored
Normal file
308
deps/serial/urlhandler/protocol_loop.py
vendored
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# This module implements a loop back connection receiving itself what it sent.
|
||||||
|
#
|
||||||
|
# The purpose of this module is.. well... You can run the unit tests with it.
|
||||||
|
# and it was so easy to implement ;-)
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2020 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
#
|
||||||
|
# URL format: loop://[option[/option...]]
|
||||||
|
# options:
|
||||||
|
# - "debug" print diagnostic messages
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import numbers
|
||||||
|
import time
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
try:
|
||||||
|
import queue
|
||||||
|
except ImportError:
|
||||||
|
import Queue as queue
|
||||||
|
|
||||||
|
from serial.serialutil import SerialBase, SerialException, to_bytes, iterbytes, SerialTimeoutException, PortNotOpenError
|
||||||
|
|
||||||
|
# map log level names to constants. used in from_url()
|
||||||
|
LOGGER_LEVELS = {
|
||||||
|
'debug': logging.DEBUG,
|
||||||
|
'info': logging.INFO,
|
||||||
|
'warning': logging.WARNING,
|
||||||
|
'error': logging.ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(SerialBase):
|
||||||
|
"""Serial port implementation that simulates a loop back connection in plain software."""
|
||||||
|
|
||||||
|
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||||
|
9600, 19200, 38400, 57600, 115200)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.buffer_size = 4096
|
||||||
|
self.queue = None
|
||||||
|
self.logger = None
|
||||||
|
self._cancel_write = False
|
||||||
|
super(Serial, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""\
|
||||||
|
Open port with current settings. This may throw a SerialException
|
||||||
|
if the port cannot be opened.
|
||||||
|
"""
|
||||||
|
if self.is_open:
|
||||||
|
raise SerialException("Port is already open.")
|
||||||
|
self.logger = None
|
||||||
|
self.queue = queue.Queue(self.buffer_size)
|
||||||
|
|
||||||
|
if self._port is None:
|
||||||
|
raise SerialException("Port must be configured before it can be used.")
|
||||||
|
# not that there is anything to open, but the function applies the
|
||||||
|
# options found in the URL
|
||||||
|
self.from_url(self.port)
|
||||||
|
|
||||||
|
# not that there anything to configure...
|
||||||
|
self._reconfigure_port()
|
||||||
|
# all things set up get, now a clean start
|
||||||
|
self.is_open = True
|
||||||
|
if not self._dsrdtr:
|
||||||
|
self._update_dtr_state()
|
||||||
|
if not self._rtscts:
|
||||||
|
self._update_rts_state()
|
||||||
|
self.reset_input_buffer()
|
||||||
|
self.reset_output_buffer()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.is_open:
|
||||||
|
self.is_open = False
|
||||||
|
try:
|
||||||
|
self.queue.put_nowait(None)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
super(Serial, self).close()
|
||||||
|
|
||||||
|
def _reconfigure_port(self):
|
||||||
|
"""\
|
||||||
|
Set communication parameters on opened port. For the loop://
|
||||||
|
protocol all settings are ignored!
|
||||||
|
"""
|
||||||
|
# not that's it of any real use, but it helps in the unit tests
|
||||||
|
if not isinstance(self._baudrate, numbers.Integral) or not 0 < self._baudrate < 2 ** 32:
|
||||||
|
raise ValueError("invalid baudrate: {!r}".format(self._baudrate))
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('_reconfigure_port()')
|
||||||
|
|
||||||
|
def from_url(self, url):
|
||||||
|
"""extract host and port from an URL string"""
|
||||||
|
parts = urlparse.urlsplit(url)
|
||||||
|
if parts.scheme != "loop":
|
||||||
|
raise SerialException(
|
||||||
|
'expected a string in the form '
|
||||||
|
'"loop://[?logging={debug|info|warning|error}]": not starting '
|
||||||
|
'with loop:// ({!r})'.format(parts.scheme))
|
||||||
|
try:
|
||||||
|
# process options now, directly altering self
|
||||||
|
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||||
|
if option == 'logging':
|
||||||
|
logging.basicConfig() # XXX is that good to call it here?
|
||||||
|
self.logger = logging.getLogger('pySerial.loop')
|
||||||
|
self.logger.setLevel(LOGGER_LEVELS[values[0]])
|
||||||
|
self.logger.debug('enabled logging')
|
||||||
|
else:
|
||||||
|
raise ValueError('unknown option: {!r}'.format(option))
|
||||||
|
except ValueError as e:
|
||||||
|
raise SerialException(
|
||||||
|
'expected a string in the form '
|
||||||
|
'"loop://[?logging={debug|info|warning|error}]": {}'.format(e))
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
"""Return the number of bytes currently in the input buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
# attention the logged value can differ from return value in
|
||||||
|
# threaded environments...
|
||||||
|
self.logger.debug('in_waiting -> {:d}'.format(self.queue.qsize()))
|
||||||
|
return self.queue.qsize()
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self._timeout is not None and self._timeout != 0:
|
||||||
|
timeout = time.time() + self._timeout
|
||||||
|
else:
|
||||||
|
timeout = None
|
||||||
|
data = bytearray()
|
||||||
|
while size > 0 and self.is_open:
|
||||||
|
try:
|
||||||
|
b = self.queue.get(timeout=self._timeout) # XXX inter char timeout
|
||||||
|
except queue.Empty:
|
||||||
|
if self._timeout == 0:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if b is not None:
|
||||||
|
data += b
|
||||||
|
size -= 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
# check for timeout now, after data has been read.
|
||||||
|
# useful for timeout = 0 (non blocking) read
|
||||||
|
if timeout and time.time() > timeout:
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('read timeout')
|
||||||
|
break
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
def cancel_read(self):
|
||||||
|
self.queue.put_nowait(None)
|
||||||
|
|
||||||
|
def cancel_write(self):
|
||||||
|
self._cancel_write = True
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""\
|
||||||
|
Output the given byte string over the serial port. Can block if the
|
||||||
|
connection is blocked. May raise SerialException if the connection is
|
||||||
|
closed.
|
||||||
|
"""
|
||||||
|
self._cancel_write = False
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
data = to_bytes(data)
|
||||||
|
# calculate aprox time that would be used to send the data
|
||||||
|
time_used_to_send = 10.0 * len(data) / self._baudrate
|
||||||
|
# when a write timeout is configured check if we would be successful
|
||||||
|
# (not sending anything, not even the part that would have time)
|
||||||
|
if self._write_timeout is not None and time_used_to_send > self._write_timeout:
|
||||||
|
# must wait so that unit test succeeds
|
||||||
|
time_left = self._write_timeout
|
||||||
|
while time_left > 0 and not self._cancel_write:
|
||||||
|
time.sleep(min(time_left, 0.5))
|
||||||
|
time_left -= 0.5
|
||||||
|
if self._cancel_write:
|
||||||
|
return 0 # XXX
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
for byte in iterbytes(data):
|
||||||
|
self.queue.put(byte, timeout=self._write_timeout)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
"""Clear input buffer, discarding all that is in the buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('reset_input_buffer()')
|
||||||
|
try:
|
||||||
|
while self.queue.qsize():
|
||||||
|
self.queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
"""\
|
||||||
|
Clear output buffer, aborting the current output and
|
||||||
|
discarding all that is in the buffer.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('reset_output_buffer()')
|
||||||
|
try:
|
||||||
|
while self.queue.qsize():
|
||||||
|
self.queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def out_waiting(self):
|
||||||
|
"""Return how many bytes the in the outgoing buffer"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
# attention the logged value can differ from return value in
|
||||||
|
# threaded environments...
|
||||||
|
self.logger.debug('out_waiting -> {:d}'.format(self.queue.qsize()))
|
||||||
|
return self.queue.qsize()
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""\
|
||||||
|
Set break: Controls TXD. When active, to transmitting is
|
||||||
|
possible.
|
||||||
|
"""
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('_update_break_state({!r})'.format(self._break_state))
|
||||||
|
|
||||||
|
def _update_rts_state(self):
|
||||||
|
"""Set terminal status line: Request To Send"""
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('_update_rts_state({!r}) -> state of CTS'.format(self._rts_state))
|
||||||
|
|
||||||
|
def _update_dtr_state(self):
|
||||||
|
"""Set terminal status line: Data Terminal Ready"""
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('_update_dtr_state({!r}) -> state of DSR'.format(self._dtr_state))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cts(self):
|
||||||
|
"""Read terminal status line: Clear To Send"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('CTS -> state of RTS ({!r})'.format(self._rts_state))
|
||||||
|
return self._rts_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsr(self):
|
||||||
|
"""Read terminal status line: Data Set Ready"""
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('DSR -> state of DTR ({!r})'.format(self._dtr_state))
|
||||||
|
return self._dtr_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ri(self):
|
||||||
|
"""Read terminal status line: Ring Indicator"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('returning dummy for RI')
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cd(self):
|
||||||
|
"""Read terminal status line: Carrier Detect"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('returning dummy for CD')
|
||||||
|
return True
|
||||||
|
|
||||||
|
# - - - platform specific - - -
|
||||||
|
# None so far
|
||||||
|
|
||||||
|
|
||||||
|
# simple client test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
s = Serial('loop://')
|
||||||
|
sys.stdout.write('{}\n'.format(s))
|
||||||
|
|
||||||
|
sys.stdout.write("write...\n")
|
||||||
|
s.write("hello\n")
|
||||||
|
s.flush()
|
||||||
|
sys.stdout.write("read: {!r}\n".format(s.read(5)))
|
||||||
|
|
||||||
|
s.close()
|
||||||
12
deps/serial/urlhandler/protocol_rfc2217.py
vendored
Normal file
12
deps/serial/urlhandler/protocol_rfc2217.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# This is a thin wrapper to load the rfc2217 implementation.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2011 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from serial.rfc2217 import Serial # noqa
|
||||||
359
deps/serial/urlhandler/protocol_socket.py
vendored
Normal file
359
deps/serial/urlhandler/protocol_socket.py
vendored
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# This module implements a simple socket based client.
|
||||||
|
# It does not support changing any port parameters and will silently ignore any
|
||||||
|
# requests to do so.
|
||||||
|
#
|
||||||
|
# The purpose of this module is that applications using pySerial can connect to
|
||||||
|
# TCP/IP to serial port converters that do not support RFC 2217.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
#
|
||||||
|
# URL format: socket://<host>:<port>[/option[/option...]]
|
||||||
|
# options:
|
||||||
|
# - "debug" print diagnostic messages
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
from serial.serialutil import SerialBase, SerialException, to_bytes, \
|
||||||
|
PortNotOpenError, SerialTimeoutException, Timeout
|
||||||
|
|
||||||
|
# map log level names to constants. used in from_url()
|
||||||
|
LOGGER_LEVELS = {
|
||||||
|
'debug': logging.DEBUG,
|
||||||
|
'info': logging.INFO,
|
||||||
|
'warning': logging.WARNING,
|
||||||
|
'error': logging.ERROR,
|
||||||
|
}
|
||||||
|
|
||||||
|
POLL_TIMEOUT = 5
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(SerialBase):
|
||||||
|
"""Serial port implementation for plain sockets."""
|
||||||
|
|
||||||
|
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||||||
|
9600, 19200, 38400, 57600, 115200)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""\
|
||||||
|
Open port with current settings. This may throw a SerialException
|
||||||
|
if the port cannot be opened.
|
||||||
|
"""
|
||||||
|
self.logger = None
|
||||||
|
if self._port is None:
|
||||||
|
raise SerialException("Port must be configured before it can be used.")
|
||||||
|
if self.is_open:
|
||||||
|
raise SerialException("Port is already open.")
|
||||||
|
try:
|
||||||
|
# timeout is used for write timeout support :/ and to get an initial connection timeout
|
||||||
|
self._socket = socket.create_connection(self.from_url(self.portstr), timeout=POLL_TIMEOUT)
|
||||||
|
except Exception as msg:
|
||||||
|
self._socket = None
|
||||||
|
raise SerialException("Could not open port {}: {}".format(self.portstr, msg))
|
||||||
|
# after connecting, switch to non-blocking, we're using select
|
||||||
|
self._socket.setblocking(False)
|
||||||
|
|
||||||
|
# not that there is anything to configure...
|
||||||
|
self._reconfigure_port()
|
||||||
|
# all things set up get, now a clean start
|
||||||
|
self.is_open = True
|
||||||
|
if not self._dsrdtr:
|
||||||
|
self._update_dtr_state()
|
||||||
|
if not self._rtscts:
|
||||||
|
self._update_rts_state()
|
||||||
|
self.reset_input_buffer()
|
||||||
|
self.reset_output_buffer()
|
||||||
|
|
||||||
|
def _reconfigure_port(self):
|
||||||
|
"""\
|
||||||
|
Set communication parameters on opened port. For the socket://
|
||||||
|
protocol all settings are ignored!
|
||||||
|
"""
|
||||||
|
if self._socket is None:
|
||||||
|
raise SerialException("Can only operate on open ports")
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('ignored port configuration change')
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close port"""
|
||||||
|
if self.is_open:
|
||||||
|
if self._socket:
|
||||||
|
try:
|
||||||
|
self._socket.shutdown(socket.SHUT_RDWR)
|
||||||
|
self._socket.close()
|
||||||
|
except:
|
||||||
|
# ignore errors.
|
||||||
|
pass
|
||||||
|
self._socket = None
|
||||||
|
self.is_open = False
|
||||||
|
# in case of quick reconnects, give the server some time
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
def from_url(self, url):
|
||||||
|
"""extract host and port from an URL string"""
|
||||||
|
parts = urlparse.urlsplit(url)
|
||||||
|
if parts.scheme != "socket":
|
||||||
|
raise SerialException(
|
||||||
|
'expected a string in the form '
|
||||||
|
'"socket://<host>:<port>[?logging={debug|info|warning|error}]": '
|
||||||
|
'not starting with socket:// ({!r})'.format(parts.scheme))
|
||||||
|
try:
|
||||||
|
# process options now, directly altering self
|
||||||
|
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||||
|
if option == 'logging':
|
||||||
|
logging.basicConfig() # XXX is that good to call it here?
|
||||||
|
self.logger = logging.getLogger('pySerial.socket')
|
||||||
|
self.logger.setLevel(LOGGER_LEVELS[values[0]])
|
||||||
|
self.logger.debug('enabled logging')
|
||||||
|
else:
|
||||||
|
raise ValueError('unknown option: {!r}'.format(option))
|
||||||
|
if not 0 <= parts.port < 65536:
|
||||||
|
raise ValueError("port not in range 0...65535")
|
||||||
|
except ValueError as e:
|
||||||
|
raise SerialException(
|
||||||
|
'expected a string in the form '
|
||||||
|
'"socket://<host>:<port>[?logging={debug|info|warning|error}]": {}'.format(e))
|
||||||
|
|
||||||
|
return (parts.hostname, parts.port)
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
"""Return the number of bytes currently in the input buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
# Poll the socket to see if it is ready for reading.
|
||||||
|
# If ready, at least one byte will be to read.
|
||||||
|
lr, lw, lx = select.select([self._socket], [], [], 0)
|
||||||
|
return len(lr)
|
||||||
|
|
||||||
|
# select based implementation, similar to posix, but only using socket API
|
||||||
|
# to be portable, additionally handle socket timeout which is used to
|
||||||
|
# emulate write timeouts
|
||||||
|
def read(self, size=1):
|
||||||
|
"""\
|
||||||
|
Read size bytes from the serial port. If a timeout is set it may
|
||||||
|
return less characters as requested. With no timeout it will block
|
||||||
|
until the requested number of bytes is read.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
read = bytearray()
|
||||||
|
timeout = Timeout(self._timeout)
|
||||||
|
while len(read) < size:
|
||||||
|
try:
|
||||||
|
ready, _, _ = select.select([self._socket], [], [], timeout.time_left())
|
||||||
|
# If select was used with a timeout, and the timeout occurs, it
|
||||||
|
# returns with empty lists -> thus abort read operation.
|
||||||
|
# For timeout == 0 (non-blocking operation) also abort when
|
||||||
|
# there is nothing to read.
|
||||||
|
if not ready:
|
||||||
|
break # timeout
|
||||||
|
buf = self._socket.recv(size - len(read))
|
||||||
|
# read should always return some data as select reported it was
|
||||||
|
# ready to read when we get to this point, unless it is EOF
|
||||||
|
if not buf:
|
||||||
|
raise SerialException('socket disconnected')
|
||||||
|
read.extend(buf)
|
||||||
|
except OSError as e:
|
||||||
|
# this is for Python 3.x where select.error is a subclass of
|
||||||
|
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||||
|
# https://www.python.org/dev/peps/pep-0475.
|
||||||
|
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('read failed: {}'.format(e))
|
||||||
|
except (select.error, socket.error) as e:
|
||||||
|
# this is for Python 2.x
|
||||||
|
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||||
|
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||||
|
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('read failed: {}'.format(e))
|
||||||
|
if timeout.expired():
|
||||||
|
break
|
||||||
|
return bytes(read)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"""\
|
||||||
|
Output the given byte string over the serial port. Can block if the
|
||||||
|
connection is blocked. May raise SerialException if the connection is
|
||||||
|
closed.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
|
||||||
|
d = to_bytes(data)
|
||||||
|
tx_len = length = len(d)
|
||||||
|
timeout = Timeout(self._write_timeout)
|
||||||
|
while tx_len > 0:
|
||||||
|
try:
|
||||||
|
n = self._socket.send(d)
|
||||||
|
if timeout.is_non_blocking:
|
||||||
|
# Zero timeout indicates non-blocking - simply return the
|
||||||
|
# number of bytes of data actually written
|
||||||
|
return n
|
||||||
|
elif not timeout.is_infinite:
|
||||||
|
# when timeout is set, use select to wait for being ready
|
||||||
|
# with the time left as timeout
|
||||||
|
if timeout.expired():
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
_, ready, _ = select.select([], [self._socket], [], timeout.time_left())
|
||||||
|
if not ready:
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
else:
|
||||||
|
assert timeout.time_left() is None
|
||||||
|
# wait for write operation
|
||||||
|
_, ready, _ = select.select([], [self._socket], [], None)
|
||||||
|
if not ready:
|
||||||
|
raise SerialException('write failed (select)')
|
||||||
|
d = d[n:]
|
||||||
|
tx_len -= n
|
||||||
|
except SerialException:
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
# this is for Python 3.x where select.error is a subclass of
|
||||||
|
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||||
|
# https://www.python.org/dev/peps/pep-0475.
|
||||||
|
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('write failed: {}'.format(e))
|
||||||
|
except select.error as e:
|
||||||
|
# this is for Python 2.x
|
||||||
|
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||||
|
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||||
|
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('write failed: {}'.format(e))
|
||||||
|
if not timeout.is_non_blocking and timeout.expired():
|
||||||
|
raise SerialTimeoutException('Write timeout')
|
||||||
|
return length - len(d)
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
"""Clear input buffer, discarding all that is in the buffer."""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
|
||||||
|
# just use recv to remove input, while there is some
|
||||||
|
ready = True
|
||||||
|
while ready:
|
||||||
|
ready, _, _ = select.select([self._socket], [], [], 0)
|
||||||
|
try:
|
||||||
|
if ready:
|
||||||
|
ready = self._socket.recv(4096)
|
||||||
|
except OSError as e:
|
||||||
|
# this is for Python 3.x where select.error is a subclass of
|
||||||
|
# OSError ignore BlockingIOErrors and EINTR. other errors are shown
|
||||||
|
# https://www.python.org/dev/peps/pep-0475.
|
||||||
|
if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('read failed: {}'.format(e))
|
||||||
|
except (select.error, socket.error) as e:
|
||||||
|
# this is for Python 2.x
|
||||||
|
# ignore BlockingIOErrors and EINTR. all errors are shown
|
||||||
|
# see also http://www.python.org/dev/peps/pep-3151/#select
|
||||||
|
if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
|
||||||
|
raise SerialException('read failed: {}'.format(e))
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
"""\
|
||||||
|
Clear output buffer, aborting the current output and
|
||||||
|
discarding all that is in the buffer.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('ignored reset_output_buffer')
|
||||||
|
|
||||||
|
def send_break(self, duration=0.25):
|
||||||
|
"""\
|
||||||
|
Send break condition. Timed, returns to idle state after given
|
||||||
|
duration.
|
||||||
|
"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('ignored send_break({!r})'.format(duration))
|
||||||
|
|
||||||
|
def _update_break_state(self):
|
||||||
|
"""Set break: Controls TXD. When active, to transmitting is
|
||||||
|
possible."""
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('ignored _update_break_state({!r})'.format(self._break_state))
|
||||||
|
|
||||||
|
def _update_rts_state(self):
|
||||||
|
"""Set terminal status line: Request To Send"""
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('ignored _update_rts_state({!r})'.format(self._rts_state))
|
||||||
|
|
||||||
|
def _update_dtr_state(self):
|
||||||
|
"""Set terminal status line: Data Terminal Ready"""
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('ignored _update_dtr_state({!r})'.format(self._dtr_state))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cts(self):
|
||||||
|
"""Read terminal status line: Clear To Send"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('returning dummy for cts')
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dsr(self):
|
||||||
|
"""Read terminal status line: Data Set Ready"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('returning dummy for dsr')
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ri(self):
|
||||||
|
"""Read terminal status line: Ring Indicator"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('returning dummy for ri')
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cd(self):
|
||||||
|
"""Read terminal status line: Carrier Detect"""
|
||||||
|
if not self.is_open:
|
||||||
|
raise PortNotOpenError()
|
||||||
|
if self.logger:
|
||||||
|
self.logger.info('returning dummy for cd)')
|
||||||
|
return True
|
||||||
|
|
||||||
|
# - - - platform specific - - -
|
||||||
|
|
||||||
|
# works on Linux and probably all the other POSIX systems
|
||||||
|
def fileno(self):
|
||||||
|
"""Get the file handle of the underlying socket for use with select"""
|
||||||
|
return self._socket.fileno()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# simple client test
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
s = Serial('socket://localhost:7000')
|
||||||
|
sys.stdout.write('{}\n'.format(s))
|
||||||
|
|
||||||
|
sys.stdout.write("write...\n")
|
||||||
|
s.write(b"hello\n")
|
||||||
|
s.flush()
|
||||||
|
sys.stdout.write("read: {}\n".format(s.read(5)))
|
||||||
|
|
||||||
|
s.close()
|
||||||
290
deps/serial/urlhandler/protocol_spy.py
vendored
Normal file
290
deps/serial/urlhandler/protocol_spy.py
vendored
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# This module implements a special URL handler that wraps an other port,
|
||||||
|
# print the traffic for debugging purposes. With this, it is possible
|
||||||
|
# to debug the serial port traffic on every application that uses
|
||||||
|
# serial_for_url.
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
#
|
||||||
|
# URL format: spy://port[?option[=value][&option[=value]]]
|
||||||
|
# options:
|
||||||
|
# - dev=X a file or device to write to
|
||||||
|
# - color use escape code to colorize output
|
||||||
|
# - raw forward raw bytes instead of hexdump
|
||||||
|
#
|
||||||
|
# example:
|
||||||
|
# redirect output to an other terminal window on Posix (Linux):
|
||||||
|
# python -m serial.tools.miniterm spy:///dev/ttyUSB0?dev=/dev/pts/14\&color
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.serialutil import to_bytes
|
||||||
|
|
||||||
|
try:
|
||||||
|
import urlparse
|
||||||
|
except ImportError:
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def sixteen(data):
|
||||||
|
"""\
|
||||||
|
yield tuples of hex and ASCII display in multiples of 16. Includes a
|
||||||
|
space after 8 bytes and (None, None) after 16 bytes and at the end.
|
||||||
|
"""
|
||||||
|
n = 0
|
||||||
|
for b in serial.iterbytes(data):
|
||||||
|
yield ('{:02X} '.format(ord(b)), b.decode('ascii') if b' ' <= b < b'\x7f' else '.')
|
||||||
|
n += 1
|
||||||
|
if n == 8:
|
||||||
|
yield (' ', '')
|
||||||
|
elif n >= 16:
|
||||||
|
yield (None, None)
|
||||||
|
n = 0
|
||||||
|
if n > 0:
|
||||||
|
while n < 16:
|
||||||
|
n += 1
|
||||||
|
if n == 8:
|
||||||
|
yield (' ', '')
|
||||||
|
yield (' ', ' ')
|
||||||
|
yield (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def hexdump(data):
|
||||||
|
"""yield lines with hexdump of data"""
|
||||||
|
values = []
|
||||||
|
ascii = []
|
||||||
|
offset = 0
|
||||||
|
for h, a in sixteen(data):
|
||||||
|
if h is None:
|
||||||
|
yield (offset, ' '.join([''.join(values), ''.join(ascii)]))
|
||||||
|
del values[:]
|
||||||
|
del ascii[:]
|
||||||
|
offset += 0x10
|
||||||
|
else:
|
||||||
|
values.append(h)
|
||||||
|
ascii.append(a)
|
||||||
|
|
||||||
|
|
||||||
|
class FormatRaw(object):
|
||||||
|
"""Forward only RX and TX data to output."""
|
||||||
|
|
||||||
|
def __init__(self, output, color):
|
||||||
|
self.output = output
|
||||||
|
self.color = color
|
||||||
|
self.rx_color = '\x1b[32m'
|
||||||
|
self.tx_color = '\x1b[31m'
|
||||||
|
|
||||||
|
def rx(self, data):
|
||||||
|
"""show received data"""
|
||||||
|
if self.color:
|
||||||
|
self.output.write(self.rx_color)
|
||||||
|
self.output.write(data)
|
||||||
|
self.output.flush()
|
||||||
|
|
||||||
|
def tx(self, data):
|
||||||
|
"""show transmitted data"""
|
||||||
|
if self.color:
|
||||||
|
self.output.write(self.tx_color)
|
||||||
|
self.output.write(data)
|
||||||
|
self.output.flush()
|
||||||
|
|
||||||
|
def control(self, name, value):
|
||||||
|
"""(do not) show control calls"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FormatHexdump(object):
|
||||||
|
"""\
|
||||||
|
Create a hex dump of RX ad TX data, show when control lines are read or
|
||||||
|
written.
|
||||||
|
|
||||||
|
output example::
|
||||||
|
|
||||||
|
000000.000 Q-RX flushInput
|
||||||
|
000002.469 RTS inactive
|
||||||
|
000002.773 RTS active
|
||||||
|
000003.001 TX 48 45 4C 4C 4F HELLO
|
||||||
|
000003.102 RX 48 45 4C 4C 4F HELLO
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, output, color):
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.output = output
|
||||||
|
self.color = color
|
||||||
|
self.rx_color = '\x1b[32m'
|
||||||
|
self.tx_color = '\x1b[31m'
|
||||||
|
self.control_color = '\x1b[37m'
|
||||||
|
|
||||||
|
def write_line(self, timestamp, label, value, value2=''):
|
||||||
|
self.output.write('{:010.3f} {:4} {}{}\n'.format(timestamp, label, value, value2))
|
||||||
|
self.output.flush()
|
||||||
|
|
||||||
|
def rx(self, data):
|
||||||
|
"""show received data as hex dump"""
|
||||||
|
if self.color:
|
||||||
|
self.output.write(self.rx_color)
|
||||||
|
if data:
|
||||||
|
for offset, row in hexdump(data):
|
||||||
|
self.write_line(time.time() - self.start_time, 'RX', '{:04X} '.format(offset), row)
|
||||||
|
else:
|
||||||
|
self.write_line(time.time() - self.start_time, 'RX', '<empty>')
|
||||||
|
|
||||||
|
def tx(self, data):
|
||||||
|
"""show transmitted data as hex dump"""
|
||||||
|
if self.color:
|
||||||
|
self.output.write(self.tx_color)
|
||||||
|
for offset, row in hexdump(data):
|
||||||
|
self.write_line(time.time() - self.start_time, 'TX', '{:04X} '.format(offset), row)
|
||||||
|
|
||||||
|
def control(self, name, value):
|
||||||
|
"""show control calls"""
|
||||||
|
if self.color:
|
||||||
|
self.output.write(self.control_color)
|
||||||
|
self.write_line(time.time() - self.start_time, name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class Serial(serial.Serial):
|
||||||
|
"""\
|
||||||
|
Inherit the native Serial port implementation and wrap all the methods and
|
||||||
|
attributes.
|
||||||
|
"""
|
||||||
|
# pylint: disable=no-member
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Serial, self).__init__(*args, **kwargs)
|
||||||
|
self.formatter = None
|
||||||
|
self.show_all = False
|
||||||
|
|
||||||
|
@serial.Serial.port.setter
|
||||||
|
def port(self, value):
|
||||||
|
if value is not None:
|
||||||
|
serial.Serial.port.__set__(self, self.from_url(value))
|
||||||
|
|
||||||
|
def from_url(self, url):
|
||||||
|
"""extract host and port from an URL string"""
|
||||||
|
parts = urlparse.urlsplit(url)
|
||||||
|
if parts.scheme != 'spy':
|
||||||
|
raise serial.SerialException(
|
||||||
|
'expected a string in the form '
|
||||||
|
'"spy://port[?option[=value][&option[=value]]]": '
|
||||||
|
'not starting with spy:// ({!r})'.format(parts.scheme))
|
||||||
|
# process options now, directly altering self
|
||||||
|
formatter = FormatHexdump
|
||||||
|
color = False
|
||||||
|
output = sys.stderr
|
||||||
|
try:
|
||||||
|
for option, values in urlparse.parse_qs(parts.query, True).items():
|
||||||
|
if option == 'file':
|
||||||
|
output = open(values[0], 'w')
|
||||||
|
elif option == 'color':
|
||||||
|
color = True
|
||||||
|
elif option == 'raw':
|
||||||
|
formatter = FormatRaw
|
||||||
|
elif option == 'all':
|
||||||
|
self.show_all = True
|
||||||
|
else:
|
||||||
|
raise ValueError('unknown option: {!r}'.format(option))
|
||||||
|
except ValueError as e:
|
||||||
|
raise serial.SerialException(
|
||||||
|
'expected a string in the form '
|
||||||
|
'"spy://port[?option[=value][&option[=value]]]": {}'.format(e))
|
||||||
|
self.formatter = formatter(output, color)
|
||||||
|
return ''.join([parts.netloc, parts.path])
|
||||||
|
|
||||||
|
def write(self, tx):
|
||||||
|
tx = to_bytes(tx)
|
||||||
|
self.formatter.tx(tx)
|
||||||
|
return super(Serial, self).write(tx)
|
||||||
|
|
||||||
|
def read(self, size=1):
|
||||||
|
rx = super(Serial, self).read(size)
|
||||||
|
if rx or self.show_all:
|
||||||
|
self.formatter.rx(rx)
|
||||||
|
return rx
|
||||||
|
|
||||||
|
if hasattr(serial.Serial, 'cancel_read'):
|
||||||
|
def cancel_read(self):
|
||||||
|
self.formatter.control('Q-RX', 'cancel_read')
|
||||||
|
super(Serial, self).cancel_read()
|
||||||
|
|
||||||
|
if hasattr(serial.Serial, 'cancel_write'):
|
||||||
|
def cancel_write(self):
|
||||||
|
self.formatter.control('Q-TX', 'cancel_write')
|
||||||
|
super(Serial, self).cancel_write()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
n = super(Serial, self).in_waiting
|
||||||
|
if self.show_all:
|
||||||
|
self.formatter.control('Q-RX', 'in_waiting -> {}'.format(n))
|
||||||
|
return n
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.formatter.control('Q-TX', 'flush')
|
||||||
|
super(Serial, self).flush()
|
||||||
|
|
||||||
|
def reset_input_buffer(self):
|
||||||
|
self.formatter.control('Q-RX', 'reset_input_buffer')
|
||||||
|
super(Serial, self).reset_input_buffer()
|
||||||
|
|
||||||
|
def reset_output_buffer(self):
|
||||||
|
self.formatter.control('Q-TX', 'reset_output_buffer')
|
||||||
|
super(Serial, self).reset_output_buffer()
|
||||||
|
|
||||||
|
def send_break(self, duration=0.25):
|
||||||
|
self.formatter.control('BRK', 'send_break {}s'.format(duration))
|
||||||
|
super(Serial, self).send_break(duration)
|
||||||
|
|
||||||
|
@serial.Serial.break_condition.setter
|
||||||
|
def break_condition(self, level):
|
||||||
|
self.formatter.control('BRK', 'active' if level else 'inactive')
|
||||||
|
serial.Serial.break_condition.__set__(self, level)
|
||||||
|
|
||||||
|
@serial.Serial.rts.setter
|
||||||
|
def rts(self, level):
|
||||||
|
self.formatter.control('RTS', 'active' if level else 'inactive')
|
||||||
|
serial.Serial.rts.__set__(self, level)
|
||||||
|
|
||||||
|
@serial.Serial.dtr.setter
|
||||||
|
def dtr(self, level):
|
||||||
|
self.formatter.control('DTR', 'active' if level else 'inactive')
|
||||||
|
serial.Serial.dtr.__set__(self, level)
|
||||||
|
|
||||||
|
@serial.Serial.cts.getter
|
||||||
|
def cts(self):
|
||||||
|
level = super(Serial, self).cts
|
||||||
|
self.formatter.control('CTS', 'active' if level else 'inactive')
|
||||||
|
return level
|
||||||
|
|
||||||
|
@serial.Serial.dsr.getter
|
||||||
|
def dsr(self):
|
||||||
|
level = super(Serial, self).dsr
|
||||||
|
self.formatter.control('DSR', 'active' if level else 'inactive')
|
||||||
|
return level
|
||||||
|
|
||||||
|
@serial.Serial.ri.getter
|
||||||
|
def ri(self):
|
||||||
|
level = super(Serial, self).ri
|
||||||
|
self.formatter.control('RI', 'active' if level else 'inactive')
|
||||||
|
return level
|
||||||
|
|
||||||
|
@serial.Serial.cd.getter
|
||||||
|
def cd(self):
|
||||||
|
level = super(Serial, self).cd
|
||||||
|
self.formatter.control('CD', 'active' if level else 'inactive')
|
||||||
|
return level
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ser = Serial(None)
|
||||||
|
ser.port = 'spy:///dev/ttyS0'
|
||||||
|
print(ser)
|
||||||
366
deps/serial/win32.py
vendored
Normal file
366
deps/serial/win32.py
vendored
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
#! python
|
||||||
|
#
|
||||||
|
# Constants and types for use with Windows API, used by serialwin32.py
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name,too-few-public-methods,protected-access,too-many-instance-attributes
|
||||||
|
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
from ctypes import c_ulong, c_void_p, c_int64, c_char, \
|
||||||
|
WinDLL, sizeof, Structure, Union, POINTER
|
||||||
|
from ctypes.wintypes import HANDLE
|
||||||
|
from ctypes.wintypes import BOOL
|
||||||
|
from ctypes.wintypes import LPCWSTR
|
||||||
|
from ctypes.wintypes import DWORD
|
||||||
|
from ctypes.wintypes import WORD
|
||||||
|
from ctypes.wintypes import BYTE
|
||||||
|
_stdcall_libraries = {}
|
||||||
|
_stdcall_libraries['kernel32'] = WinDLL('kernel32')
|
||||||
|
|
||||||
|
INVALID_HANDLE_VALUE = HANDLE(-1).value
|
||||||
|
|
||||||
|
|
||||||
|
# some details of the windows API differ between 32 and 64 bit systems..
|
||||||
|
def is_64bit():
|
||||||
|
"""Returns true when running on a 64 bit system"""
|
||||||
|
return sizeof(c_ulong) != sizeof(c_void_p)
|
||||||
|
|
||||||
|
# ULONG_PTR is a an ordinary number, not a pointer and contrary to the name it
|
||||||
|
# is either 32 or 64 bits, depending on the type of windows...
|
||||||
|
# so test if this a 32 bit windows...
|
||||||
|
if is_64bit():
|
||||||
|
ULONG_PTR = c_int64
|
||||||
|
else:
|
||||||
|
ULONG_PTR = c_ulong
|
||||||
|
|
||||||
|
|
||||||
|
class _SECURITY_ATTRIBUTES(Structure):
|
||||||
|
pass
|
||||||
|
LPSECURITY_ATTRIBUTES = POINTER(_SECURITY_ATTRIBUTES)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
CreateEventW = _stdcall_libraries['kernel32'].CreateEventW
|
||||||
|
except AttributeError:
|
||||||
|
# Fallback to non wide char version for old OS...
|
||||||
|
from ctypes.wintypes import LPCSTR
|
||||||
|
CreateEventA = _stdcall_libraries['kernel32'].CreateEventA
|
||||||
|
CreateEventA.restype = HANDLE
|
||||||
|
CreateEventA.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCSTR]
|
||||||
|
CreateEvent = CreateEventA
|
||||||
|
|
||||||
|
CreateFileA = _stdcall_libraries['kernel32'].CreateFileA
|
||||||
|
CreateFileA.restype = HANDLE
|
||||||
|
CreateFileA.argtypes = [LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE]
|
||||||
|
CreateFile = CreateFileA
|
||||||
|
else:
|
||||||
|
CreateEventW.restype = HANDLE
|
||||||
|
CreateEventW.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCWSTR]
|
||||||
|
CreateEvent = CreateEventW # alias
|
||||||
|
|
||||||
|
CreateFileW = _stdcall_libraries['kernel32'].CreateFileW
|
||||||
|
CreateFileW.restype = HANDLE
|
||||||
|
CreateFileW.argtypes = [LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE]
|
||||||
|
CreateFile = CreateFileW # alias
|
||||||
|
|
||||||
|
|
||||||
|
class _OVERLAPPED(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
OVERLAPPED = _OVERLAPPED
|
||||||
|
|
||||||
|
|
||||||
|
class _COMSTAT(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
COMSTAT = _COMSTAT
|
||||||
|
|
||||||
|
|
||||||
|
class _DCB(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
DCB = _DCB
|
||||||
|
|
||||||
|
|
||||||
|
class _COMMTIMEOUTS(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
COMMTIMEOUTS = _COMMTIMEOUTS
|
||||||
|
|
||||||
|
GetLastError = _stdcall_libraries['kernel32'].GetLastError
|
||||||
|
GetLastError.restype = DWORD
|
||||||
|
GetLastError.argtypes = []
|
||||||
|
|
||||||
|
LPOVERLAPPED = POINTER(_OVERLAPPED)
|
||||||
|
LPDWORD = POINTER(DWORD)
|
||||||
|
|
||||||
|
GetOverlappedResult = _stdcall_libraries['kernel32'].GetOverlappedResult
|
||||||
|
GetOverlappedResult.restype = BOOL
|
||||||
|
GetOverlappedResult.argtypes = [HANDLE, LPOVERLAPPED, LPDWORD, BOOL]
|
||||||
|
|
||||||
|
ResetEvent = _stdcall_libraries['kernel32'].ResetEvent
|
||||||
|
ResetEvent.restype = BOOL
|
||||||
|
ResetEvent.argtypes = [HANDLE]
|
||||||
|
|
||||||
|
LPCVOID = c_void_p
|
||||||
|
|
||||||
|
WriteFile = _stdcall_libraries['kernel32'].WriteFile
|
||||||
|
WriteFile.restype = BOOL
|
||||||
|
WriteFile.argtypes = [HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED]
|
||||||
|
|
||||||
|
LPVOID = c_void_p
|
||||||
|
|
||||||
|
ReadFile = _stdcall_libraries['kernel32'].ReadFile
|
||||||
|
ReadFile.restype = BOOL
|
||||||
|
ReadFile.argtypes = [HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED]
|
||||||
|
|
||||||
|
CloseHandle = _stdcall_libraries['kernel32'].CloseHandle
|
||||||
|
CloseHandle.restype = BOOL
|
||||||
|
CloseHandle.argtypes = [HANDLE]
|
||||||
|
|
||||||
|
ClearCommBreak = _stdcall_libraries['kernel32'].ClearCommBreak
|
||||||
|
ClearCommBreak.restype = BOOL
|
||||||
|
ClearCommBreak.argtypes = [HANDLE]
|
||||||
|
|
||||||
|
LPCOMSTAT = POINTER(_COMSTAT)
|
||||||
|
|
||||||
|
ClearCommError = _stdcall_libraries['kernel32'].ClearCommError
|
||||||
|
ClearCommError.restype = BOOL
|
||||||
|
ClearCommError.argtypes = [HANDLE, LPDWORD, LPCOMSTAT]
|
||||||
|
|
||||||
|
SetupComm = _stdcall_libraries['kernel32'].SetupComm
|
||||||
|
SetupComm.restype = BOOL
|
||||||
|
SetupComm.argtypes = [HANDLE, DWORD, DWORD]
|
||||||
|
|
||||||
|
EscapeCommFunction = _stdcall_libraries['kernel32'].EscapeCommFunction
|
||||||
|
EscapeCommFunction.restype = BOOL
|
||||||
|
EscapeCommFunction.argtypes = [HANDLE, DWORD]
|
||||||
|
|
||||||
|
GetCommModemStatus = _stdcall_libraries['kernel32'].GetCommModemStatus
|
||||||
|
GetCommModemStatus.restype = BOOL
|
||||||
|
GetCommModemStatus.argtypes = [HANDLE, LPDWORD]
|
||||||
|
|
||||||
|
LPDCB = POINTER(_DCB)
|
||||||
|
|
||||||
|
GetCommState = _stdcall_libraries['kernel32'].GetCommState
|
||||||
|
GetCommState.restype = BOOL
|
||||||
|
GetCommState.argtypes = [HANDLE, LPDCB]
|
||||||
|
|
||||||
|
LPCOMMTIMEOUTS = POINTER(_COMMTIMEOUTS)
|
||||||
|
|
||||||
|
GetCommTimeouts = _stdcall_libraries['kernel32'].GetCommTimeouts
|
||||||
|
GetCommTimeouts.restype = BOOL
|
||||||
|
GetCommTimeouts.argtypes = [HANDLE, LPCOMMTIMEOUTS]
|
||||||
|
|
||||||
|
PurgeComm = _stdcall_libraries['kernel32'].PurgeComm
|
||||||
|
PurgeComm.restype = BOOL
|
||||||
|
PurgeComm.argtypes = [HANDLE, DWORD]
|
||||||
|
|
||||||
|
SetCommBreak = _stdcall_libraries['kernel32'].SetCommBreak
|
||||||
|
SetCommBreak.restype = BOOL
|
||||||
|
SetCommBreak.argtypes = [HANDLE]
|
||||||
|
|
||||||
|
SetCommMask = _stdcall_libraries['kernel32'].SetCommMask
|
||||||
|
SetCommMask.restype = BOOL
|
||||||
|
SetCommMask.argtypes = [HANDLE, DWORD]
|
||||||
|
|
||||||
|
SetCommState = _stdcall_libraries['kernel32'].SetCommState
|
||||||
|
SetCommState.restype = BOOL
|
||||||
|
SetCommState.argtypes = [HANDLE, LPDCB]
|
||||||
|
|
||||||
|
SetCommTimeouts = _stdcall_libraries['kernel32'].SetCommTimeouts
|
||||||
|
SetCommTimeouts.restype = BOOL
|
||||||
|
SetCommTimeouts.argtypes = [HANDLE, LPCOMMTIMEOUTS]
|
||||||
|
|
||||||
|
WaitForSingleObject = _stdcall_libraries['kernel32'].WaitForSingleObject
|
||||||
|
WaitForSingleObject.restype = DWORD
|
||||||
|
WaitForSingleObject.argtypes = [HANDLE, DWORD]
|
||||||
|
|
||||||
|
WaitCommEvent = _stdcall_libraries['kernel32'].WaitCommEvent
|
||||||
|
WaitCommEvent.restype = BOOL
|
||||||
|
WaitCommEvent.argtypes = [HANDLE, LPDWORD, LPOVERLAPPED]
|
||||||
|
|
||||||
|
CancelIoEx = _stdcall_libraries['kernel32'].CancelIoEx
|
||||||
|
CancelIoEx.restype = BOOL
|
||||||
|
CancelIoEx.argtypes = [HANDLE, LPOVERLAPPED]
|
||||||
|
|
||||||
|
ONESTOPBIT = 0 # Variable c_int
|
||||||
|
TWOSTOPBITS = 2 # Variable c_int
|
||||||
|
ONE5STOPBITS = 1
|
||||||
|
|
||||||
|
NOPARITY = 0 # Variable c_int
|
||||||
|
ODDPARITY = 1 # Variable c_int
|
||||||
|
EVENPARITY = 2 # Variable c_int
|
||||||
|
MARKPARITY = 3
|
||||||
|
SPACEPARITY = 4
|
||||||
|
|
||||||
|
RTS_CONTROL_HANDSHAKE = 2 # Variable c_int
|
||||||
|
RTS_CONTROL_DISABLE = 0 # Variable c_int
|
||||||
|
RTS_CONTROL_ENABLE = 1 # Variable c_int
|
||||||
|
RTS_CONTROL_TOGGLE = 3 # Variable c_int
|
||||||
|
SETRTS = 3
|
||||||
|
CLRRTS = 4
|
||||||
|
|
||||||
|
DTR_CONTROL_HANDSHAKE = 2 # Variable c_int
|
||||||
|
DTR_CONTROL_DISABLE = 0 # Variable c_int
|
||||||
|
DTR_CONTROL_ENABLE = 1 # Variable c_int
|
||||||
|
SETDTR = 5
|
||||||
|
CLRDTR = 6
|
||||||
|
|
||||||
|
MS_DSR_ON = 32 # Variable c_ulong
|
||||||
|
EV_RING = 256 # Variable c_int
|
||||||
|
EV_PERR = 512 # Variable c_int
|
||||||
|
EV_ERR = 128 # Variable c_int
|
||||||
|
SETXOFF = 1 # Variable c_int
|
||||||
|
EV_RXCHAR = 1 # Variable c_int
|
||||||
|
GENERIC_WRITE = 1073741824 # Variable c_long
|
||||||
|
PURGE_TXCLEAR = 4 # Variable c_int
|
||||||
|
FILE_FLAG_OVERLAPPED = 1073741824 # Variable c_int
|
||||||
|
EV_DSR = 16 # Variable c_int
|
||||||
|
MAXDWORD = 4294967295 # Variable c_uint
|
||||||
|
EV_RLSD = 32 # Variable c_int
|
||||||
|
|
||||||
|
ERROR_SUCCESS = 0
|
||||||
|
ERROR_NOT_ENOUGH_MEMORY = 8
|
||||||
|
ERROR_OPERATION_ABORTED = 995
|
||||||
|
ERROR_IO_INCOMPLETE = 996
|
||||||
|
ERROR_IO_PENDING = 997 # Variable c_long
|
||||||
|
ERROR_INVALID_USER_BUFFER = 1784
|
||||||
|
|
||||||
|
MS_CTS_ON = 16 # Variable c_ulong
|
||||||
|
EV_EVENT1 = 2048 # Variable c_int
|
||||||
|
EV_RX80FULL = 1024 # Variable c_int
|
||||||
|
PURGE_RXABORT = 2 # Variable c_int
|
||||||
|
FILE_ATTRIBUTE_NORMAL = 128 # Variable c_int
|
||||||
|
PURGE_TXABORT = 1 # Variable c_int
|
||||||
|
SETXON = 2 # Variable c_int
|
||||||
|
OPEN_EXISTING = 3 # Variable c_int
|
||||||
|
MS_RING_ON = 64 # Variable c_ulong
|
||||||
|
EV_TXEMPTY = 4 # Variable c_int
|
||||||
|
EV_RXFLAG = 2 # Variable c_int
|
||||||
|
MS_RLSD_ON = 128 # Variable c_ulong
|
||||||
|
GENERIC_READ = 2147483648 # Variable c_ulong
|
||||||
|
EV_EVENT2 = 4096 # Variable c_int
|
||||||
|
EV_CTS = 8 # Variable c_int
|
||||||
|
EV_BREAK = 64 # Variable c_int
|
||||||
|
PURGE_RXCLEAR = 8 # Variable c_int
|
||||||
|
INFINITE = 0xFFFFFFFF
|
||||||
|
|
||||||
|
CE_RXOVER = 0x0001
|
||||||
|
CE_OVERRUN = 0x0002
|
||||||
|
CE_RXPARITY = 0x0004
|
||||||
|
CE_FRAME = 0x0008
|
||||||
|
CE_BREAK = 0x0010
|
||||||
|
|
||||||
|
|
||||||
|
class N11_OVERLAPPED4DOLLAR_48E(Union):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class N11_OVERLAPPED4DOLLAR_484DOLLAR_49E(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
N11_OVERLAPPED4DOLLAR_484DOLLAR_49E._fields_ = [
|
||||||
|
('Offset', DWORD),
|
||||||
|
('OffsetHigh', DWORD),
|
||||||
|
]
|
||||||
|
|
||||||
|
PVOID = c_void_p
|
||||||
|
|
||||||
|
N11_OVERLAPPED4DOLLAR_48E._anonymous_ = ['_0']
|
||||||
|
N11_OVERLAPPED4DOLLAR_48E._fields_ = [
|
||||||
|
('_0', N11_OVERLAPPED4DOLLAR_484DOLLAR_49E),
|
||||||
|
('Pointer', PVOID),
|
||||||
|
]
|
||||||
|
_OVERLAPPED._anonymous_ = ['_0']
|
||||||
|
_OVERLAPPED._fields_ = [
|
||||||
|
('Internal', ULONG_PTR),
|
||||||
|
('InternalHigh', ULONG_PTR),
|
||||||
|
('_0', N11_OVERLAPPED4DOLLAR_48E),
|
||||||
|
('hEvent', HANDLE),
|
||||||
|
]
|
||||||
|
_SECURITY_ATTRIBUTES._fields_ = [
|
||||||
|
('nLength', DWORD),
|
||||||
|
('lpSecurityDescriptor', LPVOID),
|
||||||
|
('bInheritHandle', BOOL),
|
||||||
|
]
|
||||||
|
_COMSTAT._fields_ = [
|
||||||
|
('fCtsHold', DWORD, 1),
|
||||||
|
('fDsrHold', DWORD, 1),
|
||||||
|
('fRlsdHold', DWORD, 1),
|
||||||
|
('fXoffHold', DWORD, 1),
|
||||||
|
('fXoffSent', DWORD, 1),
|
||||||
|
('fEof', DWORD, 1),
|
||||||
|
('fTxim', DWORD, 1),
|
||||||
|
('fReserved', DWORD, 25),
|
||||||
|
('cbInQue', DWORD),
|
||||||
|
('cbOutQue', DWORD),
|
||||||
|
]
|
||||||
|
_DCB._fields_ = [
|
||||||
|
('DCBlength', DWORD),
|
||||||
|
('BaudRate', DWORD),
|
||||||
|
('fBinary', DWORD, 1),
|
||||||
|
('fParity', DWORD, 1),
|
||||||
|
('fOutxCtsFlow', DWORD, 1),
|
||||||
|
('fOutxDsrFlow', DWORD, 1),
|
||||||
|
('fDtrControl', DWORD, 2),
|
||||||
|
('fDsrSensitivity', DWORD, 1),
|
||||||
|
('fTXContinueOnXoff', DWORD, 1),
|
||||||
|
('fOutX', DWORD, 1),
|
||||||
|
('fInX', DWORD, 1),
|
||||||
|
('fErrorChar', DWORD, 1),
|
||||||
|
('fNull', DWORD, 1),
|
||||||
|
('fRtsControl', DWORD, 2),
|
||||||
|
('fAbortOnError', DWORD, 1),
|
||||||
|
('fDummy2', DWORD, 17),
|
||||||
|
('wReserved', WORD),
|
||||||
|
('XonLim', WORD),
|
||||||
|
('XoffLim', WORD),
|
||||||
|
('ByteSize', BYTE),
|
||||||
|
('Parity', BYTE),
|
||||||
|
('StopBits', BYTE),
|
||||||
|
('XonChar', c_char),
|
||||||
|
('XoffChar', c_char),
|
||||||
|
('ErrorChar', c_char),
|
||||||
|
('EofChar', c_char),
|
||||||
|
('EvtChar', c_char),
|
||||||
|
('wReserved1', WORD),
|
||||||
|
]
|
||||||
|
_COMMTIMEOUTS._fields_ = [
|
||||||
|
('ReadIntervalTimeout', DWORD),
|
||||||
|
('ReadTotalTimeoutMultiplier', DWORD),
|
||||||
|
('ReadTotalTimeoutConstant', DWORD),
|
||||||
|
('WriteTotalTimeoutMultiplier', DWORD),
|
||||||
|
('WriteTotalTimeoutConstant', DWORD),
|
||||||
|
]
|
||||||
|
__all__ = ['GetLastError', 'MS_CTS_ON', 'FILE_ATTRIBUTE_NORMAL',
|
||||||
|
'DTR_CONTROL_ENABLE', '_COMSTAT', 'MS_RLSD_ON',
|
||||||
|
'GetOverlappedResult', 'SETXON', 'PURGE_TXABORT',
|
||||||
|
'PurgeComm', 'N11_OVERLAPPED4DOLLAR_48E', 'EV_RING',
|
||||||
|
'ONESTOPBIT', 'SETXOFF', 'PURGE_RXABORT', 'GetCommState',
|
||||||
|
'RTS_CONTROL_ENABLE', '_DCB', 'CreateEvent',
|
||||||
|
'_COMMTIMEOUTS', '_SECURITY_ATTRIBUTES', 'EV_DSR',
|
||||||
|
'EV_PERR', 'EV_RXFLAG', 'OPEN_EXISTING', 'DCB',
|
||||||
|
'FILE_FLAG_OVERLAPPED', 'EV_CTS', 'SetupComm',
|
||||||
|
'LPOVERLAPPED', 'EV_TXEMPTY', 'ClearCommBreak',
|
||||||
|
'LPSECURITY_ATTRIBUTES', 'SetCommBreak', 'SetCommTimeouts',
|
||||||
|
'COMMTIMEOUTS', 'ODDPARITY', 'EV_RLSD',
|
||||||
|
'GetCommModemStatus', 'EV_EVENT2', 'PURGE_TXCLEAR',
|
||||||
|
'EV_BREAK', 'EVENPARITY', 'LPCVOID', 'COMSTAT', 'ReadFile',
|
||||||
|
'PVOID', '_OVERLAPPED', 'WriteFile', 'GetCommTimeouts',
|
||||||
|
'ResetEvent', 'EV_RXCHAR', 'LPCOMSTAT', 'ClearCommError',
|
||||||
|
'ERROR_IO_PENDING', 'EscapeCommFunction', 'GENERIC_READ',
|
||||||
|
'RTS_CONTROL_HANDSHAKE', 'OVERLAPPED',
|
||||||
|
'DTR_CONTROL_HANDSHAKE', 'PURGE_RXCLEAR', 'GENERIC_WRITE',
|
||||||
|
'LPDCB', 'CreateEventW', 'SetCommMask', 'EV_EVENT1',
|
||||||
|
'SetCommState', 'LPVOID', 'CreateFileW', 'LPDWORD',
|
||||||
|
'EV_RX80FULL', 'TWOSTOPBITS', 'LPCOMMTIMEOUTS', 'MAXDWORD',
|
||||||
|
'MS_DSR_ON', 'MS_RING_ON',
|
||||||
|
'N11_OVERLAPPED4DOLLAR_484DOLLAR_49E', 'EV_ERR',
|
||||||
|
'ULONG_PTR', 'CreateFile', 'NOPARITY', 'CloseHandle']
|
||||||
471
gui.py
Normal file
471
gui.py
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inkex.gui import Gtk, GLib
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
from plotters import plotters
|
||||||
|
|
||||||
|
|
||||||
|
class KMPlotGUI:
|
||||||
|
def build_window(self):
|
||||||
|
self.window = Gtk.Window(title="KM Plot")
|
||||||
|
self.window.set_border_width(10)
|
||||||
|
self.window.set_default_size(500, 500)
|
||||||
|
|
||||||
|
root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
|
|
||||||
|
self.status_label = Gtk.Label(label="No supported plotter detected.")
|
||||||
|
self.status_label.set_xalign(0.5)
|
||||||
|
|
||||||
|
self.device_label = Gtk.Label(label="Device: not connected")
|
||||||
|
self.device_label.set_xalign(0.5)
|
||||||
|
|
||||||
|
self.device_image = Gtk.Image()
|
||||||
|
self.device_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
|
||||||
|
self.device_image.set_pixel_size(150)
|
||||||
|
self.device_image.set_halign(Gtk.Align.CENTER)
|
||||||
|
self.device_image.set_valign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
self.port_info_label = Gtk.Label(label="")
|
||||||
|
self.port_info_label.set_xalign(0.5)
|
||||||
|
self.port_info_label.set_halign(Gtk.Align.CENTER)
|
||||||
|
self.port_info_label.set_justify(Gtk.Justification.CENTER)
|
||||||
|
self.port_info_label.set_line_wrap(True)
|
||||||
|
self.port_info_label.set_margin_top(14)
|
||||||
|
self.port_info_label.set_margin_bottom(14)
|
||||||
|
self.port_info_label.set_margin_start(12)
|
||||||
|
self.port_info_label.set_margin_end(12)
|
||||||
|
self.port_info_frame = Gtk.Frame(label="Driver")
|
||||||
|
self.port_info_frame.set_shadow_type(Gtk.ShadowType.IN)
|
||||||
|
self.port_info_frame.set_halign(Gtk.Align.CENTER)
|
||||||
|
self.port_info_frame.set_margin_top(12)
|
||||||
|
self.port_info_frame.set_margin_bottom(12)
|
||||||
|
self.port_info_frame.set_margin_start(12)
|
||||||
|
self.port_info_frame.set_margin_end(12)
|
||||||
|
self.port_info_frame.set_size_request(250, -1)
|
||||||
|
self.port_info_frame.add(self.port_info_label)
|
||||||
|
|
||||||
|
self.port_store = Gtk.ListStore(str, str, str) # device, vidpid, display
|
||||||
|
self.port_combo = Gtk.ComboBox.new_with_model(self.port_store)
|
||||||
|
port_renderer = Gtk.CellRendererText()
|
||||||
|
self.port_combo.pack_start(port_renderer, True)
|
||||||
|
self.port_combo.add_attribute(port_renderer, "text", 2)
|
||||||
|
self.port_combo.set_sensitive(False)
|
||||||
|
self.port_combo.connect("changed", self.on_port_changed)
|
||||||
|
self.port_combo.set_halign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
self.cut_button = Gtk.Button(label="Send to plotter")
|
||||||
|
self.cut_button.set_sensitive(False)
|
||||||
|
self.cut_button.connect("clicked", self.on_cut_clicked)
|
||||||
|
|
||||||
|
self.status_bar = Gtk.Label(label="Searching for devices...")
|
||||||
|
self.status_bar.set_xalign(0)
|
||||||
|
|
||||||
|
notebook = Gtk.Notebook()
|
||||||
|
notebook.set_tab_pos(Gtk.PositionType.TOP)
|
||||||
|
|
||||||
|
# Main tab
|
||||||
|
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
|
main_box.set_hexpand(True)
|
||||||
|
main_box.set_vexpand(True)
|
||||||
|
main_box.set_valign(Gtk.Align.CENTER)
|
||||||
|
main_box.set_halign(Gtk.Align.CENTER)
|
||||||
|
port_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
port_row.set_halign(Gtk.Align.CENTER)
|
||||||
|
port_row.pack_start(self.port_combo, False, False, 0)
|
||||||
|
main_box.pack_start(port_row, False, False, 0)
|
||||||
|
main_box.pack_start(self.port_info_frame, False, False, 10)
|
||||||
|
main_box.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 12)
|
||||||
|
main_box.pack_start(self.device_image, False, False, 6)
|
||||||
|
|
||||||
|
notebook.append_page(main_box, Gtk.Label(label="Device"))
|
||||||
|
|
||||||
|
# Connection settings tab
|
||||||
|
conn_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
|
conn_box.set_hexpand(True)
|
||||||
|
conn_box.set_vexpand(True)
|
||||||
|
conn_box.set_halign(Gtk.Align.CENTER)
|
||||||
|
conn_box.set_margin_top(12)
|
||||||
|
conn_box.set_margin_bottom(12)
|
||||||
|
conn_grid = Gtk.Grid(column_spacing=8, row_spacing=6)
|
||||||
|
conn_grid.set_column_homogeneous(False)
|
||||||
|
conn_grid.set_halign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
# Plot settings tab
|
||||||
|
plot_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
|
plot_box.set_hexpand(True)
|
||||||
|
plot_box.set_vexpand(True)
|
||||||
|
plot_box.set_halign(Gtk.Align.CENTER)
|
||||||
|
plot_box.set_margin_top(12)
|
||||||
|
plot_box.set_margin_bottom(12)
|
||||||
|
plot_grid = Gtk.Grid(column_spacing=8, row_spacing=6)
|
||||||
|
plot_grid.set_column_homogeneous(False)
|
||||||
|
plot_grid.set_halign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
self.adv_controls = {}
|
||||||
|
|
||||||
|
conn_row = 0
|
||||||
|
plot_row = 0
|
||||||
|
|
||||||
|
def add_conn_row(label_text, widget):
|
||||||
|
nonlocal conn_row
|
||||||
|
label = Gtk.Label(label=label_text)
|
||||||
|
label.set_xalign(0)
|
||||||
|
conn_grid.attach(label, 0, conn_row, 1, 1)
|
||||||
|
conn_grid.attach(widget, 1, conn_row, 1, 1)
|
||||||
|
conn_row += 1
|
||||||
|
|
||||||
|
def add_plot_row(label_text, widget):
|
||||||
|
nonlocal plot_row
|
||||||
|
label = Gtk.Label(label=label_text)
|
||||||
|
label.set_xalign(0)
|
||||||
|
plot_grid.attach(label, 0, plot_row, 1, 1)
|
||||||
|
plot_grid.attach(widget, 1, plot_row, 1, 1)
|
||||||
|
plot_row += 1
|
||||||
|
|
||||||
|
# Dropdown helpers
|
||||||
|
def combo(options, active_value):
|
||||||
|
store = Gtk.ListStore(str, str)
|
||||||
|
for key, text in options:
|
||||||
|
store.append([key, text])
|
||||||
|
combo_box = Gtk.ComboBox.new_with_model(store)
|
||||||
|
renderer_text = Gtk.CellRendererText()
|
||||||
|
combo_box.pack_start(renderer_text, True)
|
||||||
|
combo_box.add_attribute(renderer_text, "text", 1)
|
||||||
|
# set active
|
||||||
|
for i, row_data in enumerate(store):
|
||||||
|
if row_data[0] == active_value:
|
||||||
|
combo_box.set_active(i)
|
||||||
|
break
|
||||||
|
return combo_box
|
||||||
|
|
||||||
|
# Spin helpers
|
||||||
|
def spin_int(value, min_val, max_val, step):
|
||||||
|
adj = Gtk.Adjustment(value=value, lower=min_val, upper=max_val, step_increment=step)
|
||||||
|
return Gtk.SpinButton(adjustment=adj, climb_rate=1, digits=0)
|
||||||
|
|
||||||
|
def spin_float(value, min_val, max_val, step, digits=1):
|
||||||
|
adj = Gtk.Adjustment(value=value, lower=min_val, upper=max_val, step_increment=step)
|
||||||
|
return Gtk.SpinButton(adjustment=adj, climb_rate=1, digits=digits)
|
||||||
|
|
||||||
|
# Build widgets using current options/defaults
|
||||||
|
baud_spin = spin_int(int(getattr(self.options, "serialBaudRate", 9600)), 1200, 115200, 100)
|
||||||
|
bytesize_combo = combo(
|
||||||
|
[("five", "5"), ("six", "6"), ("seven", "7"), ("eight", "8")],
|
||||||
|
str(getattr(self.options, "serialByteSize", "eight")).lower(),
|
||||||
|
)
|
||||||
|
stopbits_combo = combo(
|
||||||
|
[("one", "1"), ("onepointfive", "1.5"), ("two", "2")],
|
||||||
|
str(getattr(self.options, "serialStopBits", "one")).lower(),
|
||||||
|
)
|
||||||
|
parity_combo = combo(
|
||||||
|
[("none", "None"), ("even", "Even"), ("odd", "Odd"), ("mark", "Mark"), ("space", "Space")],
|
||||||
|
str(getattr(self.options, "serialParity", "none")).lower(),
|
||||||
|
)
|
||||||
|
flow_combo = combo(
|
||||||
|
[("xonxoff", "XON/XOFF"), ("rtscts", "RTS/CTS"), ("dsrdtrrtscts", "DSR/DTR+RTS/CTS"), ("none", "None")],
|
||||||
|
str(getattr(self.options, "serialFlowControl", "xonxoff")).lower(),
|
||||||
|
)
|
||||||
|
resx_spin = spin_float(float(getattr(self.options, "resolutionX", 1016.0)), 10, 5000, 10, digits=1)
|
||||||
|
resy_spin = spin_float(float(getattr(self.options, "resolutionY", 1016.0)), 10, 5000, 10, digits=1)
|
||||||
|
pen_spin = spin_int(int(getattr(self.options, "pen", 1)), 0, 10, 1)
|
||||||
|
force_spin = spin_int(int(getattr(self.options, "force", 0)), 0, 1000, 1)
|
||||||
|
speed_spin = spin_int(int(getattr(self.options, "speed", 0)), 0, 1000, 1)
|
||||||
|
orientation_combo = combo(
|
||||||
|
[("0", "0°"), ("90", "90°"), ("180", "180°"), ("270", "270°")],
|
||||||
|
str(getattr(self.options, "orientation", "0")),
|
||||||
|
)
|
||||||
|
mirrorx_check = Gtk.CheckButton(label="Mirror X")
|
||||||
|
mirrorx_check.set_active(bool(getattr(self.options, "mirrorX", False)))
|
||||||
|
mirrory_check = Gtk.CheckButton(label="Mirror Y")
|
||||||
|
mirrory_check.set_active(bool(getattr(self.options, "mirrorY", False)))
|
||||||
|
center_check = Gtk.CheckButton(label="Center zero point")
|
||||||
|
center_check.set_active(bool(getattr(self.options, "center", False)))
|
||||||
|
precut_check = Gtk.CheckButton(label="Use precut")
|
||||||
|
precut_check.set_active(bool(getattr(self.options, "precut", True)))
|
||||||
|
autoalign_check = Gtk.CheckButton(label="Auto align")
|
||||||
|
autoalign_check.set_active(bool(getattr(self.options, "autoAlign", True)))
|
||||||
|
overcut_spin = spin_float(float(getattr(self.options, "overcut", 1.0)), 0, 10, 0.1, digits=2)
|
||||||
|
flat_spin = spin_float(float(getattr(self.options, "flat", 1.2)), 0.1, 10, 0.1, digits=2)
|
||||||
|
tool_spin = spin_float(float(getattr(self.options, "toolOffset", 0.25)), 0, 10, 0.05, digits=2)
|
||||||
|
|
||||||
|
def set_tip(widget, text):
|
||||||
|
try:
|
||||||
|
widget.set_tooltip_text(text)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Tooltips for plot settings to guide users.
|
||||||
|
set_tip(resx_spin, "Horizontal resolution in dots per inch (higher is finer).")
|
||||||
|
set_tip(resy_spin, "Vertical resolution in dots per inch (higher is finer).")
|
||||||
|
set_tip(pen_spin, "Pen number to use when cutting/drawing.")
|
||||||
|
set_tip(force_spin, "Downward force in grams; higher presses harder.")
|
||||||
|
set_tip(speed_spin, "Movement speed in cm/s; higher is faster.")
|
||||||
|
set_tip(orientation_combo, "Rotate the plot by the selected angle.")
|
||||||
|
set_tip(mirrorx_check, "Flip the design horizontally.")
|
||||||
|
set_tip(mirrory_check, "Flip the design vertically.")
|
||||||
|
set_tip(center_check, "Center the zero point on the page.")
|
||||||
|
set_tip(precut_check, "Use a small precut to help corners release cleanly.")
|
||||||
|
set_tip(autoalign_check, "Attempt to auto-align the plot to the material.")
|
||||||
|
set_tip(overcut_spin, "Extend cuts past corners to ensure complete separation.")
|
||||||
|
set_tip(flat_spin, "Flatness compensation factor.")
|
||||||
|
set_tip(tool_spin, "Offset distance for the tool tip (in mm).")
|
||||||
|
|
||||||
|
add_conn_row("Baud rate", baud_spin); self.adv_controls["serialBaudRate"] = baud_spin
|
||||||
|
add_conn_row("Byte size", bytesize_combo); self.adv_controls["serialByteSize"] = bytesize_combo
|
||||||
|
add_conn_row("Stop bits", stopbits_combo); self.adv_controls["serialStopBits"] = stopbits_combo
|
||||||
|
add_conn_row("Parity", parity_combo); self.adv_controls["serialParity"] = parity_combo
|
||||||
|
add_conn_row("Flow control", flow_combo); self.adv_controls["serialFlowControl"] = flow_combo
|
||||||
|
add_plot_row("Resolution X (dpi)", resx_spin); self.adv_controls["resolutionX"] = resx_spin
|
||||||
|
add_plot_row("Resolution Y (dpi)", resy_spin); self.adv_controls["resolutionY"] = resy_spin
|
||||||
|
add_plot_row("Pen number", pen_spin); self.adv_controls["pen"] = pen_spin
|
||||||
|
add_plot_row("Force (g)", force_spin); self.adv_controls["force"] = force_spin
|
||||||
|
add_plot_row("Speed (cm/s)", speed_spin); self.adv_controls["speed"] = speed_spin
|
||||||
|
add_plot_row("Orientation", orientation_combo); self.adv_controls["orientation"] = orientation_combo
|
||||||
|
add_plot_row("Mirror X", mirrorx_check); self.adv_controls["mirrorX"] = mirrorx_check
|
||||||
|
add_plot_row("Mirror Y", mirrory_check); self.adv_controls["mirrorY"] = mirrory_check
|
||||||
|
add_plot_row("Center", center_check); self.adv_controls["center"] = center_check
|
||||||
|
add_plot_row("Precut", precut_check); self.adv_controls["precut"] = precut_check
|
||||||
|
add_plot_row("Auto align", autoalign_check); self.adv_controls["autoAlign"] = autoalign_check
|
||||||
|
add_plot_row("Overcut (mm)", overcut_spin); self.adv_controls["overcut"] = overcut_spin
|
||||||
|
add_plot_row("Flatness", flat_spin); self.adv_controls["flat"] = flat_spin
|
||||||
|
add_plot_row("Tool offset (mm)", tool_spin); self.adv_controls["toolOffset"] = tool_spin
|
||||||
|
|
||||||
|
# Connect change handlers to keep self.options in sync.
|
||||||
|
baud_spin.connect("value-changed", lambda w: self.update_option("serialBaudRate", int(w.get_value())))
|
||||||
|
resx_spin.connect("value-changed", lambda w: self.update_option("resolutionX", float(w.get_value())))
|
||||||
|
resy_spin.connect("value-changed", lambda w: self.update_option("resolutionY", float(w.get_value())))
|
||||||
|
pen_spin.connect("value-changed", lambda w: self.update_option("pen", int(w.get_value())))
|
||||||
|
force_spin.connect("value-changed", lambda w: self.update_option("force", int(w.get_value())))
|
||||||
|
speed_spin.connect("value-changed", lambda w: self.update_option("speed", int(w.get_value())))
|
||||||
|
overcut_spin.connect("value-changed", lambda w: self.update_option("overcut", float(w.get_value())))
|
||||||
|
flat_spin.connect("value-changed", lambda w: self.update_option("flat", float(w.get_value())))
|
||||||
|
tool_spin.connect("value-changed", lambda w: self.update_option("toolOffset", float(w.get_value())))
|
||||||
|
|
||||||
|
bytesize_combo.connect(
|
||||||
|
"changed",
|
||||||
|
lambda w: self.update_option_from_combo("serialByteSize", w, default="eight"),
|
||||||
|
)
|
||||||
|
stopbits_combo.connect(
|
||||||
|
"changed",
|
||||||
|
lambda w: self.update_option_from_combo("serialStopBits", w, default="one"),
|
||||||
|
)
|
||||||
|
parity_combo.connect(
|
||||||
|
"changed", lambda w: self.update_option_from_combo("serialParity", w, default="none")
|
||||||
|
)
|
||||||
|
flow_combo.connect(
|
||||||
|
"changed",
|
||||||
|
lambda w: self.update_option_from_combo("serialFlowControl", w, default="xonxoff"),
|
||||||
|
)
|
||||||
|
orientation_combo.connect(
|
||||||
|
"changed", lambda w: self.update_option_from_combo("orientation", w, default="0")
|
||||||
|
)
|
||||||
|
|
||||||
|
mirrorx_check.connect("toggled", lambda w: self.update_option("mirrorX", w.get_active()))
|
||||||
|
mirrory_check.connect("toggled", lambda w: self.update_option("mirrorY", w.get_active()))
|
||||||
|
center_check.connect("toggled", lambda w: self.update_option("center", w.get_active()))
|
||||||
|
precut_check.connect("toggled", lambda w: self.update_option("precut", w.get_active()))
|
||||||
|
autoalign_check.connect("toggled", lambda w: self.update_option("autoAlign", w.get_active()))
|
||||||
|
|
||||||
|
conn_box.pack_start(conn_grid, False, False, 0)
|
||||||
|
conn_scroller = Gtk.ScrolledWindow()
|
||||||
|
conn_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
conn_scroller.set_hexpand(True)
|
||||||
|
conn_scroller.set_vexpand(True)
|
||||||
|
conn_scroller.add(conn_box)
|
||||||
|
|
||||||
|
plot_box.pack_start(plot_grid, False, False, 0)
|
||||||
|
plot_scroller = Gtk.ScrolledWindow()
|
||||||
|
plot_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
|
plot_scroller.set_hexpand(True)
|
||||||
|
plot_scroller.set_vexpand(True)
|
||||||
|
plot_scroller.add(plot_box)
|
||||||
|
|
||||||
|
notebook.append_page(conn_scroller, Gtk.Label(label="Connection Settings"))
|
||||||
|
notebook.append_page(plot_scroller, Gtk.Label(label="Plot Settings"))
|
||||||
|
|
||||||
|
# Footer with status and send button at the bottom.
|
||||||
|
footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||||
|
footer.pack_start(self.status_bar, True, True, 0)
|
||||||
|
footer.pack_end(self.cut_button, False, False, 0)
|
||||||
|
|
||||||
|
root.pack_start(notebook, True, True, 0)
|
||||||
|
root.pack_end(footer, False, False, 0)
|
||||||
|
|
||||||
|
self.window.add(root)
|
||||||
|
self.window.connect("destroy", self.on_window_close)
|
||||||
|
self.window.show_all()
|
||||||
|
|
||||||
|
def poll_devices(self):
|
||||||
|
if getattr(self, "sending", False):
|
||||||
|
return True
|
||||||
|
self.debug("Polling for devices...")
|
||||||
|
self.update_status_bar("Searching for devices...")
|
||||||
|
devices = list(self.enumerate_with_serial())
|
||||||
|
self.port_entries = []
|
||||||
|
for device_entry in devices:
|
||||||
|
device_path = device_entry["device"]
|
||||||
|
vidpid = device_entry["vidpid"]
|
||||||
|
info_text = device_entry["info"]
|
||||||
|
plotter_info = plotters.get(vidpid)
|
||||||
|
if isinstance(plotter_info, dict):
|
||||||
|
name = plotter_info.get("name", vidpid)
|
||||||
|
icon_key = plotter_info.get("icon")
|
||||||
|
elif plotter_info:
|
||||||
|
name = str(plotter_info)
|
||||||
|
icon_key = None
|
||||||
|
else:
|
||||||
|
name = "Unknown device"
|
||||||
|
icon_key = None
|
||||||
|
display = f"{device_path} ({vidpid})" if vidpid else device_path
|
||||||
|
self.port_entries.append(
|
||||||
|
{
|
||||||
|
"device": device_path,
|
||||||
|
"vidpid": vidpid,
|
||||||
|
"name": name,
|
||||||
|
"display": display,
|
||||||
|
"info": info_text,
|
||||||
|
"icon": icon_key,
|
||||||
|
"supported": plotter_info is not None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.port_store:
|
||||||
|
self.port_store.clear()
|
||||||
|
for entry in self.port_entries:
|
||||||
|
self.port_store.append([entry["device"], entry["vidpid"] or "", entry["display"]])
|
||||||
|
|
||||||
|
if not self.port_entries:
|
||||||
|
self.debug("No ports detected this cycle.")
|
||||||
|
if self.port_combo:
|
||||||
|
self.port_combo.set_sensitive(False)
|
||||||
|
self.port_combo.hide()
|
||||||
|
self.current_device = None
|
||||||
|
self.current_vidpid = None
|
||||||
|
if self.port_info_label:
|
||||||
|
self.port_info_label.set_text("No Serial Devices Found")
|
||||||
|
self.port_info_frame.set_visible(True)
|
||||||
|
self.set_device_icon(None, fallback="noport")
|
||||||
|
self.cut_button.set_sensitive(False)
|
||||||
|
self.update_status_bar("Waiting for a supported plotter...")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.port_combo:
|
||||||
|
self.port_combo.set_sensitive(True)
|
||||||
|
self.port_combo.show()
|
||||||
|
if self.port_info_frame:
|
||||||
|
self.port_info_frame.set_visible(True)
|
||||||
|
|
||||||
|
active_idx = 0
|
||||||
|
if self.current_device:
|
||||||
|
for idx, entry in enumerate(self.port_entries):
|
||||||
|
if entry["device"] == self.current_device:
|
||||||
|
active_idx = idx
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.port_combo:
|
||||||
|
self.port_combo.handler_block_by_func(self.on_port_changed)
|
||||||
|
self.port_combo.set_active(active_idx)
|
||||||
|
self.port_combo.handler_unblock_by_func(self.on_port_changed)
|
||||||
|
|
||||||
|
self.apply_port_entry(self.port_entries[active_idx], update_status=not getattr(self, "sending", False))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def apply_port_entry(self, entry, update_status=False):
|
||||||
|
self.current_device = entry["device"]
|
||||||
|
self.current_vidpid = entry["vidpid"]
|
||||||
|
detail = entry["vidpid"] or "unknown VID/PID"
|
||||||
|
if self.port_info_label:
|
||||||
|
info_text = entry.get("info") or ""
|
||||||
|
self.port_info_label.set_text(info_text)
|
||||||
|
self.set_device_icon(entry.get("icon"))
|
||||||
|
self.cut_button.set_sensitive(True)
|
||||||
|
if update_status:
|
||||||
|
self.update_status_bar("Ready")
|
||||||
|
|
||||||
|
def on_port_changed(self, _combo):
|
||||||
|
idx = self.port_combo.get_active() if self.port_combo else -1
|
||||||
|
if idx is None or idx < 0 or idx >= len(self.port_entries):
|
||||||
|
return
|
||||||
|
self.apply_port_entry(self.port_entries[idx], update_status=True)
|
||||||
|
|
||||||
|
def on_window_close(self, *_args):
|
||||||
|
if self.poll_id:
|
||||||
|
GLib.source_remove(self.poll_id)
|
||||||
|
Gtk.main_quit()
|
||||||
|
|
||||||
|
def on_cut_clicked(self, _button):
|
||||||
|
if not self.current_device:
|
||||||
|
self.show_dialog("No plotter detected yet.", Gtk.MessageType.ERROR)
|
||||||
|
self.update_status_bar("No plotter detected.", error=True)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.sending = True
|
||||||
|
self.update_status_bar("Sending to plotter...")
|
||||||
|
self.flush_gui()
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.perform_cut(self.current_device)
|
||||||
|
self.show_dialog(
|
||||||
|
f"Sent the document to {self.current_device}.", Gtk.MessageType.INFO
|
||||||
|
)
|
||||||
|
self.update_status_bar("Sent")
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
self.show_dialog(f"Cut failed: {exc}", Gtk.MessageType.ERROR)
|
||||||
|
self.update_status_bar(f"Cut failed: {exc}", error=True)
|
||||||
|
finally:
|
||||||
|
self.sending = False
|
||||||
|
|
||||||
|
def show_dialog(self, message, level):
|
||||||
|
dialog = Gtk.MessageDialog(
|
||||||
|
transient_for=self.window,
|
||||||
|
flags=0,
|
||||||
|
message_type=level,
|
||||||
|
buttons=Gtk.ButtonsType.OK,
|
||||||
|
text=message,
|
||||||
|
)
|
||||||
|
dialog.run()
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
def update_status_bar(self, message, error=False):
|
||||||
|
if not self.status_bar:
|
||||||
|
return
|
||||||
|
prefix = "Error: " if error else ""
|
||||||
|
self.status_bar.set_text(f"{prefix}{message}")
|
||||||
|
|
||||||
|
def set_device_icon(self, icon_key, fallback=None):
|
||||||
|
if not self.device_image:
|
||||||
|
return
|
||||||
|
target_px = 150
|
||||||
|
base_dir = getattr(self, "icons_dir", None)
|
||||||
|
if base_dir is None:
|
||||||
|
base_dir = Path(__file__).resolve().parent / "icons"
|
||||||
|
|
||||||
|
def load_icon(name):
|
||||||
|
icon_path = base_dir / f"{name}.png"
|
||||||
|
if icon_path.exists():
|
||||||
|
try:
|
||||||
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||||
|
str(icon_path), width=target_px, height=target_px, preserve_aspect_ratio=True
|
||||||
|
)
|
||||||
|
self.device_image.set_from_pixbuf(pixbuf)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
try:
|
||||||
|
self.debug(f"Failed to load icon {icon_path}: {exc}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
if icon_key and load_icon(icon_key):
|
||||||
|
return
|
||||||
|
if fallback and load_icon(fallback):
|
||||||
|
return
|
||||||
|
if load_icon("unknown"):
|
||||||
|
return
|
||||||
|
self.device_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
|
||||||
|
self.device_image.set_pixel_size(target_px)
|
||||||
|
|
||||||
|
def flush_gui(self):
|
||||||
|
"""Process pending GTK events so label updates are shown before blocking work."""
|
||||||
|
while Gtk.events_pending():
|
||||||
|
Gtk.main_iteration_do(False)
|
||||||
BIN
icons/noport.png
Normal file
BIN
icons/noport.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
icons/unknown.png
Normal file
BIN
icons/unknown.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
icons/vinyl1.png
Normal file
BIN
icons/vinyl1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
icons/vinyl2.png
Normal file
BIN
icons/vinyl2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
14
kmplot.inx
Normal file
14
kmplot.inx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>KM Plot</name>
|
||||||
|
<id>org.knoxmakers.kmplot</id>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">kmplot.py</command>
|
||||||
|
</script>
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu _name="Knox Makers" />
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
</inkscape-extension>
|
||||||
167
kmplot.py
Normal file
167
kmplot.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Make bundled deps (e.g., pyserial) importable before loading inkex.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DEPS_DIR = BASE_DIR / "deps"
|
||||||
|
if DEPS_DIR.exists():
|
||||||
|
sys.path.insert(0, str(DEPS_DIR))
|
||||||
|
|
||||||
|
# Enable stderr logging when set True (or via KM_PLOT_DEBUG=1 environment variable).
|
||||||
|
DEBUG = os.environ.get("KM_PLOT_DEBUG", "").lower() in {"1", "true", "yes"}
|
||||||
|
|
||||||
|
import inkex
|
||||||
|
from gui import KMPlotGUI, Gtk, GLib
|
||||||
|
from plot import PlotEngine
|
||||||
|
from plotters import plotters
|
||||||
|
|
||||||
|
try:
|
||||||
|
import serial.tools.list_ports # type: ignore
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
serial_ports = None
|
||||||
|
serial_import_error = repr(exc)
|
||||||
|
else:
|
||||||
|
serial_ports = serial.tools.list_ports
|
||||||
|
serial_import_error = None
|
||||||
|
|
||||||
|
|
||||||
|
class KMPlot(KMPlotGUI, inkex.EffectExtension):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.window = None
|
||||||
|
self.status_label = None
|
||||||
|
self.device_label = None
|
||||||
|
self.port_info_label = None
|
||||||
|
self.port_combo = None
|
||||||
|
self.port_store = None
|
||||||
|
self.cut_button = None
|
||||||
|
self.status_bar = None
|
||||||
|
self.current_device = None
|
||||||
|
self.current_vidpid = None
|
||||||
|
self.poll_id = None
|
||||||
|
self.port_entries = []
|
||||||
|
self.sending = False
|
||||||
|
self.device_image = None
|
||||||
|
self.icons_dir = BASE_DIR / "icons"
|
||||||
|
self.plot_engine = PlotEngine(self)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
self.debug("KM Plot extension starting; setting up window.")
|
||||||
|
self.build_window()
|
||||||
|
interval = 2
|
||||||
|
self.debug(f"Using poll interval: {interval} seconds")
|
||||||
|
self.update_status_bar("Searching for devices...")
|
||||||
|
self.poll_id = GLib.timeout_add_seconds(interval, self.poll_devices)
|
||||||
|
self.poll_devices()
|
||||||
|
Gtk.main()
|
||||||
|
|
||||||
|
def find_matching_device(self):
|
||||||
|
devices = list(self.enumerate_with_serial())
|
||||||
|
if not devices:
|
||||||
|
self.debug("pyserial enumeration returned no devices.")
|
||||||
|
for entry in devices:
|
||||||
|
vidpid = entry["vidpid"]
|
||||||
|
if vidpid in plotters:
|
||||||
|
return entry["device"], vidpid, plotters[vidpid]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def enumerate_with_serial(self):
|
||||||
|
if not serial_ports:
|
||||||
|
reason = (
|
||||||
|
f" import error: {serial_import_error}"
|
||||||
|
if serial_import_error
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
self.debug(
|
||||||
|
f"pyserial not available; skipping VID/PID enumeration.{reason}"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
devices = []
|
||||||
|
try:
|
||||||
|
ports = list(serial_ports.comports())
|
||||||
|
except Exception as exc:
|
||||||
|
self.debug(f"serial.tools.list_ports.comports() failed: {exc!r}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not ports:
|
||||||
|
self.debug("serial.tools.list_ports reported no ports; retrying with links.")
|
||||||
|
try:
|
||||||
|
ports = list(serial_ports.comports(include_links=True))
|
||||||
|
except Exception as exc:
|
||||||
|
self.debug(
|
||||||
|
f"serial.tools.list_ports(include_links=True) failed: {exc!r}"
|
||||||
|
)
|
||||||
|
ports = []
|
||||||
|
|
||||||
|
if not ports:
|
||||||
|
self.debug("serial.tools.list_ports still returned no ports.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
for port in ports:
|
||||||
|
device = (
|
||||||
|
getattr(port, "device", None)
|
||||||
|
or getattr(port, "name", None)
|
||||||
|
or getattr(port, "path", None)
|
||||||
|
or getattr(port, "port", None)
|
||||||
|
or (port if isinstance(port, str) else None)
|
||||||
|
)
|
||||||
|
vid = getattr(port, "vid", None) or getattr(port, "vendor_id", None)
|
||||||
|
pid = getattr(port, "pid", None) or getattr(port, "product_id", None)
|
||||||
|
vidpid = getattr(port, "vidpid", None)
|
||||||
|
manufacturer = getattr(port, "manufacturer", None)
|
||||||
|
product = getattr(port, "product", None)
|
||||||
|
|
||||||
|
if vidpid:
|
||||||
|
vidpid = str(vidpid).lower()
|
||||||
|
elif vid is not None and pid is not None:
|
||||||
|
try:
|
||||||
|
vidpid = f"{int(vid):04x}:{int(pid):04x}".lower()
|
||||||
|
except Exception:
|
||||||
|
vidpid = f"{str(vid).lower()}:{str(pid).lower()}"
|
||||||
|
|
||||||
|
self.debug(
|
||||||
|
f"Serial port candidate: device={device}, vid={vid}, pid={pid}, vidpid={vidpid}"
|
||||||
|
)
|
||||||
|
if device and vid is not None and pid is not None and vidpid:
|
||||||
|
info_lines = []
|
||||||
|
if manufacturer:
|
||||||
|
info_lines.append(str(manufacturer))
|
||||||
|
if product:
|
||||||
|
info_lines.append(str(product))
|
||||||
|
devices.append(
|
||||||
|
{
|
||||||
|
"device": device,
|
||||||
|
"vidpid": vidpid,
|
||||||
|
"info": "\n".join(info_lines),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return devices
|
||||||
|
|
||||||
|
def perform_cut(self, device_path):
|
||||||
|
return self.plot_engine.perform_cut(device_path)
|
||||||
|
|
||||||
|
def update_option(self, key, value):
|
||||||
|
setattr(self.options, key, value)
|
||||||
|
|
||||||
|
def update_option_from_combo(self, key, combo, default):
|
||||||
|
model = combo.get_model()
|
||||||
|
active_iter = combo.get_active_iter()
|
||||||
|
if active_iter:
|
||||||
|
value = model[active_iter][0]
|
||||||
|
else:
|
||||||
|
value = default
|
||||||
|
setattr(self.options, key, value)
|
||||||
|
|
||||||
|
def debug(self, message):
|
||||||
|
text = f"[KMPlot] {message}"
|
||||||
|
if DEBUG:
|
||||||
|
print(text, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
KMPlot().run()
|
||||||
125
plot.py
Normal file
125
plot.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
class PlotEngine:
|
||||||
|
|
||||||
|
def __init__(self, extension):
|
||||||
|
self.ext = extension
|
||||||
|
|
||||||
|
def perform_cut(self, device_path):
|
||||||
|
self.ext.debug(f"Generating HPGL and sending to {device_path}")
|
||||||
|
hpgl = self.generate_hpgl()
|
||||||
|
self.send_hpgl_serial(device_path, hpgl)
|
||||||
|
|
||||||
|
def ensure_plotter_defaults(self):
|
||||||
|
defaults = {
|
||||||
|
"serialBaudRate": "9600",
|
||||||
|
"serialByteSize": "eight",
|
||||||
|
"serialStopBits": "one",
|
||||||
|
"serialParity": "none",
|
||||||
|
"serialFlowControl": "xonxoff",
|
||||||
|
"resolutionX": 1016.0,
|
||||||
|
"resolutionY": 1016.0,
|
||||||
|
"pen": 1,
|
||||||
|
"force": 0,
|
||||||
|
"speed": 0,
|
||||||
|
"orientation": "0",
|
||||||
|
"mirrorX": False,
|
||||||
|
"mirrorY": False,
|
||||||
|
"center": False,
|
||||||
|
"overcut": 1.0,
|
||||||
|
"precut": True,
|
||||||
|
"flat": 1.2,
|
||||||
|
"autoAlign": True,
|
||||||
|
"toolOffset": 0.25,
|
||||||
|
}
|
||||||
|
for key, value in defaults.items():
|
||||||
|
if not hasattr(self.ext.options, key):
|
||||||
|
setattr(self.ext.options, key, value)
|
||||||
|
|
||||||
|
def generate_hpgl(self):
|
||||||
|
self.ensure_plotter_defaults()
|
||||||
|
try:
|
||||||
|
import hpgl_encoder
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"hpgl_encoder not available: {exc}") from exc
|
||||||
|
|
||||||
|
if self.ext.svg.xpath("//use|//flowRoot|//text") is not None:
|
||||||
|
self.preprocess(["flowRoot", "text"])
|
||||||
|
encoder = hpgl_encoder.hpglEncoder(self.ext)
|
||||||
|
try:
|
||||||
|
hpgl = encoder.getHpgl()
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"HPGL generation failed: {exc}") from exc
|
||||||
|
return self.convert_hpgl(hpgl)
|
||||||
|
|
||||||
|
def convert_hpgl(self, hpgl):
|
||||||
|
init = "IN"
|
||||||
|
return init + hpgl + ";PU0,0;SP0;IN; "
|
||||||
|
|
||||||
|
def send_hpgl_serial(self, device_path, hpgl):
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"pyserial not available: {exc}") from exc
|
||||||
|
|
||||||
|
baud = int(getattr(self.ext.options, "serialBaudRate", 9600))
|
||||||
|
byte_size = str(getattr(self.ext.options, "serialByteSize", "eight")).lower()
|
||||||
|
stop_bits = str(getattr(self.ext.options, "serialStopBits", "one")).lower()
|
||||||
|
parity = str(getattr(self.ext.options, "serialParity", "none")).lower()
|
||||||
|
flow = str(getattr(self.ext.options, "serialFlowControl", "xonxoff")).lower()
|
||||||
|
|
||||||
|
size_map = {
|
||||||
|
"5": serial.FIVEBITS,
|
||||||
|
"five": serial.FIVEBITS,
|
||||||
|
"6": serial.SIXBITS,
|
||||||
|
"six": serial.SIXBITS,
|
||||||
|
"7": serial.SEVENBITS,
|
||||||
|
"seven": serial.SEVENBITS,
|
||||||
|
"8": serial.EIGHTBITS,
|
||||||
|
"eight": serial.EIGHTBITS,
|
||||||
|
}
|
||||||
|
stop_map = {
|
||||||
|
"1": serial.STOPBITS_ONE,
|
||||||
|
"one": serial.STOPBITS_ONE,
|
||||||
|
"1.5": serial.STOPBITS_ONE_POINT_FIVE,
|
||||||
|
"onepointfive": serial.STOPBITS_ONE_POINT_FIVE,
|
||||||
|
"2": serial.STOPBITS_TWO,
|
||||||
|
"two": serial.STOPBITS_TWO,
|
||||||
|
}
|
||||||
|
parity_map = {
|
||||||
|
"none": serial.PARITY_NONE,
|
||||||
|
"even": serial.PARITY_EVEN,
|
||||||
|
"odd": serial.PARITY_ODD,
|
||||||
|
"mark": serial.PARITY_MARK,
|
||||||
|
"space": serial.PARITY_SPACE,
|
||||||
|
}
|
||||||
|
|
||||||
|
ser = serial.Serial()
|
||||||
|
ser.port = device_path
|
||||||
|
ser.baudrate = baud
|
||||||
|
ser.bytesize = size_map.get(byte_size, serial.EIGHTBITS)
|
||||||
|
ser.stopbits = stop_map.get(stop_bits, serial.STOPBITS_ONE)
|
||||||
|
ser.parity = parity_map.get(parity, serial.PARITY_NONE)
|
||||||
|
ser.timeout = 1
|
||||||
|
ser.xonxoff = flow == "xonxoff"
|
||||||
|
ser.rtscts = flow in ("rtscts", "dsrdtrrtscts")
|
||||||
|
ser.dsrdtr = flow == "dsrdtrrtscts"
|
||||||
|
|
||||||
|
self.ext.debug(
|
||||||
|
f"Opening serial port {device_path} baud={baud} size={ser.bytesize} "
|
||||||
|
f"stop={ser.stopbits} parity={ser.parity} flow={flow}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ser.open()
|
||||||
|
try:
|
||||||
|
ser.write(hpgl.encode("utf8"))
|
||||||
|
try:
|
||||||
|
ser.read(2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
def preprocess(self, convert):
|
||||||
|
try:
|
||||||
|
self.ext.svg.convert_to_paths(convert)
|
||||||
|
except Exception as exc:
|
||||||
|
self.ext.debug(f"Preprocess convert_to_paths failed: {exc}")
|
||||||
10
plotters.py
Normal file
10
plotters.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
plotters = {
|
||||||
|
# vid:pid -> {"name": "wat is this", "icon": "iconname"}
|
||||||
|
"0403:6001": {"name": "FTDI FT232RL", "icon": "vinyl"},
|
||||||
|
"1A86:7523": {"name": "QinHeng CH340/CH341", "icon": "vinyl"},
|
||||||
|
"067B:2303": {"name": "Prolific PL2303", "icon": "vinyl"},
|
||||||
|
"04D8:000A": {"name": "Microchip CDC", "icon": "vinyl"},
|
||||||
|
"7447:5419": {"name": "Generic", "icon": "vinyl"},
|
||||||
|
"0483:5740": {"name": "STMicroelectronics", "icon": "vinyl2"},
|
||||||
|
#"": {"name": "", "icon": "vinyl"},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user