# coding: utf-8
from __future__ import unicode_literals
import codecs
import re
from collections import OrderedDict
from os import SEEK_CUR
from sys import platform
from serial import Serial, SerialBase, serial_for_url
from serial.rs485 import RS485Settings
from serial.serialutil import LF
from serial.tools.list_ports import comports, grep
from serial.tools import hexlify_codec
from robot.api import logger
from robot.utils import asserts, is_truthy, is_string
__version__ = (0, 4, 0)
if platform == 'win32':
import ntpath as ospath
else:
import os.path as ospath
abspath = ospath.abspath
isabs = ospath.isabs
join = ospath.join
# unicode type hack
unicode_ = type('')
# add hexlify to codecs
def hexlify_decode_plus(data, errors='strict'):
udata, length = hexlify_codec.hex_decode(data, errors)
return (udata.rstrip(), length)
hexlify_codec_plus = codecs.CodecInfo(
name='hexlify',
encode=hexlify_codec.hex_encode,
decode=hexlify_decode_plus,
incrementalencoder=hexlify_codec.IncrementalEncoder,
incrementaldecoder=hexlify_codec.IncrementalDecoder,
streamwriter=hexlify_codec.StreamWriter,
streamreader=hexlify_codec.StreamReader)
codecs.register(lambda c: hexlify_codec_plus if c == 'hexlify' else None)
DEFAULT_SETTINGS = SerialBase(
timeout=1.0, write_timeout=1.0, inter_byte_timeout=0.0).get_settings()
def is_truthy_on_off(item):
if is_string(item):
item = item.strip()
if item.isdigit():
return bool(int(item))
return item.strip().upper() not in ['FALSE', 'NO', '0', 'OFF', '']
return bool(item)
def to_on_off(value):
return 'On' if bool(value) is True else 'Off'
class SerialLibrary:
"""Robot Framework test library for manipulating serial ports
Using Library
--------------
Most simple use is to just import library and add port::
*** settings ***
Library SerialLibrary
*** test cases ***
Hello serial test
Add Port loop://
Write Data Hello World encoding=ascii
Read Data Should Be Hello World encoding=ascii
Or, if you play with only one port and send ascii-only, more simply::
*** settings ***
Library SerialLibrary loop:// encoding=ascii
*** test cases ***
Hello serial test
Write Data Hello World
Read Data Should Be Hello World
Default Parameters
-------------------
Default parameter values except timeouts are set as same as SerialBase.
Default value or timeout and writer_timeout are set to 1.0.
Current port and port names
----------------------------
You may have several ports in a library instance simultaneously.
All ports added to the instance can be identified by its name, which
is taken from port name given at importing library or Add Port keyword
(Thus you cannot maintain multiple ports with same name).
Those ports are maintained in an internal dictionary and lives until
explicitly deleted from the library instance using Delete (All) Port(s).
Most of keywords in the library have `port_locator` parameter which
can be used to specify the port to manipulate.
The library has an idea of current port. Current port is the default
port used if `port_locator` parameter is omitted. The first port added
to the library instance always becomes current port and you may freely
switch it with Switch Port keyword. If current port is deleted, most
recently added port is made current.
Port timeouts
--------------
Default (read/write) timeouts are set to 1.0 while pySerial defaults
are None (blocking). This is because there is no way to abort blocked
port from the library at this moment. If you really want blocking
port, carefully design your test to avoid infinite execution blocking.
"""
ROBOT_LIBRARY_SCOPE = 'GLOBAL'
ROBOT_LIBRARY_VERSION = __version__
LOGGER_MAP = dict(INFO=logger.info, DEBUG=logger.debug, WARN=logger.warn)
def __init__(self, port_locator=None, encoding='hexlify', **kwargs):
"""
Import Library.
If library is imported without parameters, no ports are prepared.
Importing library with port_locator parameter will add new port
with given name and make it as default port (current port).
If `encoding` is specified, all messages sent to serial port
will be encoded by specified encoding, also all bytes read back
from the port will be decoded as same way.
Default value is 'hexlify' that formats a byte sequence with
space-separated hex strings. Setting any other encoding is
possible, but please note:
- If your device dumps non-decodable byte sequence, read-related
keywords will fail to decode bytes.
- If your device is too slow to read, read-related keywords
may return incomplete byte sequence that unable to decode.
`kwargs` can be used to set library-instance-wide default value
to create new port instance internally. The 'default' default
values are taken from serial.SerialBase, except timeout/write_timeout
that are set to 1.0/1.0, respectively.
"""
self._encoding = encoding
self._ports = OrderedDict()
self._defaults = dict(DEFAULT_SETTINGS)
self.set_default_parameters(kwargs)
self._current_port_locator = None
if port_locator is not None:
self.add_port(port_locator)
self._current_port_str = port_locator
def _encode(self, ustring, encoding=None, encoding_mode=None):
"""
Encode (unicode) string into raw bytes.
If encoding is not specified, instance's default encoding will be used.
"""
encoding_mode = encoding_mode or 'strict'
encoding = encoding or self._encoding or 'hexlify'
return ustring.encode(encoding, encoding_mode)
def _decode(self, bstring, encoding=None, encoding_mode=None):
"""
Decode raw bytes to (unicode) string.
"""
encoding_mode = encoding_mode or 'replace'
encoding = encoding or self._encoding or 'hexlify'
return bstring.decode(encoding, encoding_mode)
def _port(self, port_locator=None, fail=True):
"""
Lookup port by name.
If port_locator is None or string '_', current port
is returned.
If specified port is not found, exception is raised
when fail is True, otherwise None is returned silently.
"""
if port_locator in [None, '_']:
port_locator = self._current_port_locator
port = self._ports.get(port_locator, None)
if port is None and is_truthy(fail) is True:
asserts.fail('No such port.')
return port
def get_encoding(self):
"""
Returns default encoding for the library instance.
"""
return self._encoding
def set_encoding(self, encoding=None):
"""
Sets default encoding for the library instance.
Returns previous encoding.
If encoding is set to None, just returns current encoding.
"""
prev_encoding = self._encoding
if encoding:
self._encoding = encoding
return prev_encoding
def list_com_ports(self):
"""
Returns list of com ports found on the system.
This is thin-wrapper of serial.tools.list_ports.
Returned list consists of possible ListPortInfo instances.
You may access attributes of ListPortInfo by extended variable
syntax, e.g.::
@{ports} = List Com Ports
Log ${ports[0].device}
"""
return comports()
def list_com_port_names(self):
"""
Returns list of device names for com ports found on the system.
Items are sorted in dictionary order.