servod: Use the Linux i2c-pseudo driver to implement Servo/DUT I2C adapter.
This uses non-blocking I/O with the epoll(7) interface for interfacing with
the i2c-pseudo controller device. All I/O, both with the controller and
with the I2C bus, is peformed in a single thread dedicated to the pseudo
controller.
Things that can and should be improved in followup changes:
- There should be a dut-control command to expose the Servo/DUT
I2C adapter number. The adapter number is already tracked internally.
- There is a circular reference between BaseI2CBus and the new
I2cPseudoAdapter class. An actual circular dependency is avoided using
weakref, however it's still ugly. A cleaner way should be found to
integrate I2cPseudoAdapter with the rest of servod. (Could have
BaseI2CBus inherit from I2cPseudoAdapter, but that doesn't feel
quite right either.)
BRANCH=none
BUG=b:79684405,b:124388894
CQ-DEPEND=CL:1274778
TEST=Issued I2C xfer requests via i2c-tools -> i2c-dev. Used CL:1301154
to reflash ITE EC on bip and ampton.
Change-Id: Ic206095e8fe37dc1261ab68542e70ab8b5302895
Signed-off-by: Matthew Blecker <matthewb@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/1282275
Reviewed-by: Ruben Rodriguez Buchillon <coconutruben@chromium.org>
diff --git a/servo/bbi2c.py b/servo/bbi2c.py
index fdec002..f482fba 100644
--- a/servo/bbi2c.py
+++ b/servo/bbi2c.py
@@ -38,30 +38,6 @@
if bbmux_controller.use_omapmux():
self._bus_num += 1
- def open(self):
- """Opens access to FTDI interface as a i2c (MPSSE mode) interface.
-
- Raises:
- BBi2cError: If open fails
- """
- pass
-
- def close(self):
- """Close connection to i2c through beaglebone and cleanup.
-
- Raises:
- BBi2cError: If close fails
- """
- pass
-
- def setclock(self, speed=100000):
- """Sets i2c clock speed.
-
- Args:
- speed: clock speed in hertz. Default is 100kHz
- """
- pass
-
def _write(self, slv, address, wlist):
"""Preform a single i2cset write command.
diff --git a/servo/ftdii2c.py b/servo/ftdii2c.py
index 01fa870..cfabb2e 100644
--- a/servo/ftdii2c.py
+++ b/servo/ftdii2c.py
@@ -86,8 +86,9 @@
Calls close to release device
"""
- if not self._is_closed:
+ if not getattr(self, '_is_closed', True):
self.close()
+ super(Fi2c, self).__del__()
def open(self):
"""Opens access to FTDI interface as a i2c (MPSSE mode) interface.
@@ -111,6 +112,7 @@
if err:
raise Fi2cError('fi2c_close', err)
self._is_closed = True
+ super(Fi2c, self).close()
def init(self):
"""Initialize i2c interface.
@@ -118,10 +120,10 @@
Raises:
Fi2cError: If init fails
"""
+ super(Fi2c, self).init()
err = self._flib.ftdi_init(ctypes.byref(self._fc))
if err:
raise Fi2cError('ftdi_init', err)
-
err = self._lib.fi2c_init(ctypes.byref(self._fic), ctypes.byref(self._fc))
if err:
raise Fi2cError('fi2c_init', err)
@@ -148,6 +150,12 @@
list of c_ubyte's read from i2c device.
"""
self._logger.debug('')
+
+ if wlist is None:
+ wlist = []
+ if rcnt is None:
+ rcnt = 0
+
self._fic.slv = slv
wcnt = len(wlist)
wbuf_type = ctypes.c_ubyte * wcnt
diff --git a/servo/i2c_base.py b/servo/i2c_base.py
index 57ba3f5..934d1b6 100644
--- a/servo/i2c_base.py
+++ b/servo/i2c_base.py
@@ -3,12 +3,34 @@
# found in the LICENSE file.
"""Provides a base class for I2C bus implementations."""
+import logging
+import os
import threading
+import weakref
+
+import i2c_pseudo
+
+
+def _format_write_list(write_list):
+ """Format a BaseI2CBus.wr_rd() write_list arg for logging.
+
+ Args:
+ write_list: list of output byte values [0~255], or None for no write
+
+ Returns:
+ str
+ """
+ if write_list is None:
+ return str(None)
+ return '[%s]' % (', '.join('0x%02X' % (value,) for value in write_list),)
class BaseI2CBus(object):
"""Base class for all I2c bus classes.
+ Thread safety:
+ All public methods are safe to invoke concurrently from multiple threads.
+
Usage:
class MyI2CBus(BaseI2CBus):
def _raw_wr_rd(self, slave_address, write_list, read_count):
@@ -17,7 +39,33 @@
def __init__(self):
"""Initializer."""
+ self.__logger = logging.getLogger('i2c_base')
self.__lock = threading.Lock()
+ self.__pseudo_adap = None
+ self.reinit()
+
+ # This exists to reinitialize the I2C pseudo controller after FTDI I2C
+ # reinitialization, which is a hack supported for iteflash using Servo v2.
+ def init(self):
+ self.reinit()
+
+ def reinit(self):
+ with self.__lock:
+ if self.__pseudo_adap is not None:
+ self.__do_close()
+ pseudo_ctrlr_path = i2c_pseudo.default_controller_path()
+ if not os.path.exists(pseudo_ctrlr_path):
+ self.__logger.info('path %r not found, not starting I2C pseudo adapter'
+ % (pseudo_ctrlr_path,))
+ return
+ # TODO(b/79684405): This circular reference is less than ideal. Find a
+ # better way to hook i2c_pseudo.I2cPseudoAdapter into servod. For now
+ # weakref is used to avoid a reference count cycle.
+ self.__logger.info('path %r found, starting I2C pseudo adapter' %
+ (pseudo_ctrlr_path,))
+ self.__pseudo_adap = i2c_pseudo.I2cPseudoAdapter(
+ pseudo_ctrlr_path, weakref.proxy(self))
+ self.__pseudo_adap.start()
def multi_wr_rd(self, transactions):
"""Allows for multiple write/read/write+read I2C transactions.
@@ -34,6 +82,17 @@
write_list: list of output byte values [0~255], or None for no write
read_count: number of byte values to read from device, or None for no
read
+
+ Returns:
+ [None or [int]] - A list of .wr_rd() return values, one for each
+ transaction.
+
+ Each item is a list of the bytes read from one transaction. If no
+ bytes were read, the item may be an empty list, or may be None instead
+ of a list.
+
+ Instead of int, another type that represents and acts as an integer
+ may be used, such as ctypes.c_ubyte.
"""
with self.__lock:
return [self._raw_wr_rd(*args) for args in transactions]
@@ -44,17 +103,30 @@
This function writes byte values list to I2C device (if given), then reads
byte values from the same device (if requested).
+ For a given I2C bus object, overlapping calls to this method will be
+ serialized by means of a mutex or equivalent, thus while one call is
+ executing, the rest will block.
+
Args:
slave_address: 7 bit I2C slave address.
write_list: list of output byte values [0~255], or None for no write
read_count: number of byte values to read from device, or None for no read
- For a given I2C bus object, overlapping calls to this method will be
- serialized by means of a mutex or equivalent, thus while one call is
- executing, the rest will block.
+ Returns:
+ None or [int] - A list of the bytes read. If no bytes were read, either
+ None or an empty list may be returned. Instead of int, another type
+ that represents and acts as an integer may be used, such as
+ ctypes.c_ubyte.
"""
with self.__lock:
- return self._raw_wr_rd(slave_address, write_list, read_count)
+ self.__logger.debug(
+ 'i2c_base.BaseI2CBus.wr_rd(0x%02X, %s, %s) called' %
+ (slave_address, _format_write_list(write_list), read_count))
+ retval = self._raw_wr_rd(slave_address, write_list, read_count)
+ self.__logger.debug(
+ 'i2c_base.BaseI2CBus.wr_rd(0x%02X, %r, %s) returning %s' %
+ (slave_address, _format_write_list(write_list), read_count, retval))
+ return retval
def _raw_wr_rd(self, slave_address, write_list, read_count):
"""Implements hdctools wr_rd() interface.
@@ -62,13 +134,33 @@
This function writes byte values list to I2C device (if given), then reads
byte values from the same device (if requested).
+ For a given I2C bus object, there should never be overlapping calls to this
+ method. Implementations should therefore make no special effort to handle
+ calls from multiple threads.
+
Args:
slave_address: 7 bit I2C slave address.
write_list: list of output byte values [0~255], or None for no write
read_count: number of byte values to read from device, or None for no read
- For a given I2C bus object, there will never be overlapping calls to this
- method. Implementations should therefore make no special effort to handle
- calls from multiple threads.
+ Returns:
+ None or [int] - A list of the bytes read. If no bytes were read, either
+ None or an empty list may be returned. Instead of int, another type
+ that represents and acts as an integer may be used, such as
+ ctypes.c_ubyte.
"""
raise NotImplementedError
+
+ def close(self):
+ """Stop the I2C pseudo interface, if it was started."""
+ with self.__lock:
+ if self.__pseudo_adap is not None:
+ self.__do_close()
+
+ # self.__lock must be held and self.__pseudo_adap must not be None.
+ def __do_close(self):
+ self.__pseudo_adap.shutdown(2)
+ # Break the circular reference.
+ # TODO(b/79684405): This circular reference is less than ideal. Find a
+ # better way to fit i2c_pseudo.I2cPseudoAdapter into servod.
+ self.__pseudo_adap = None
diff --git a/servo/i2c_pseudo.py b/servo/i2c_pseudo.py
new file mode 100644
index 0000000..f0460b1
--- /dev/null
+++ b/servo/i2c_pseudo.py
@@ -0,0 +1,561 @@
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+This uses the i2c-pseudo driver to implement a local Linux I2C adapter of a
+Servo/DUT I2C bus.
+"""
+
+import collections
+import errno
+import logging
+import os
+import select
+import threading
+
+_CONTROLLER_DEVICE_PATH = b'/dev/i2c-pseudo-controller'
+_CMD_END_CHAR = b'\n'
+_HEADER_SEP_CHAR = b' '
+_DATA_SEP_CHAR = b':'
+# This is a printf()-style format string for the i2c-pseudo I2C_XFER_REPLY
+# write command.
+#
+# The positional fields are, in order:
+# xfer_id: int or ctypes.c_ubyte or equivalent
+# msg_id: int or ctypes.c_ubyte or equivalent
+# addr: int or ctypes.c_ubyte or equivalent
+# flags: int or ctypes.c_ubyte or equivalent
+# errno: int or ctypes.c_ubyte or equivalent
+#
+# See Documentation/i2c/pseudo-controller-interface from Linux for more details.
+_I2C_XFER_REPLY_FMT_STR = _HEADER_SEP_CHAR.join((
+ b'I2C_XFER_REPLY', b'%d', b'%d', b'0x%04X', b'0x%04X', b'%d'))
+_READ_SIZE = 1024
+_I2C_ADAPTER_TIMEOUT_MS = 5000
+_EPOLL_EVENTMASK_NO_WRITES = select.EPOLLIN | select.EPOLLERR | select.EPOLLHUP
+_EPOLL_EVENTMASK_WITH_WRITES = _EPOLL_EVENTMASK_NO_WRITES | select.EPOLLOUT
+
+# This value is guaranteed per include/uapi/linux/i2c.h
+I2C_M_RD = 0x0001
+# This value is implicitly subject to change.
+I2C_M_RECV_LEN = 0x0400
+
+
+def default_controller_path():
+ """Get the default i2c-pseudo controller device path.
+
+ Returns:
+ bytes - absolute path
+ """
+ path = _CONTROLLER_DEVICE_PATH
+ assert os.path.isabs(path)
+ return path
+
+
+class I2cPseudoAdapter(object):
+ """This class implements a Linux I2C adapter for the servo I2C bus.
+
+ This class is a controller for the i2c-pseudo Linux kernel module. See its
+ documentation for background.
+
+ Thread safety:
+ This class is internally multi-threaded.
+ It is safe to use the public interface from multiple threads concurrently.
+
+ Usage:
+ adap = I2cPseudoAdapter.make_with_default_path(servo_i2c_bus)
+ i2c_id = adap.start()
+ ...
+ adap.shutdown()
+ """
+
+ @staticmethod
+ def make_with_default_path(servo_i2c_bus):
+ """Make an instance using the default i2c-pseudo controller device path.
+
+ Args:
+ servo_i2c_bus: implementation of i2c_base.BaseI2CBus
+
+ Returns:
+ I2cPseudoAdapter
+ """
+ return I2cPseudoAdapter(default_controller_path(), servo_i2c_bus)
+
+ def __init__(self, controller_device_path, servo_i2c_bus):
+ """Initializer. Does NOT create the pseudo adapter.
+
+ Args:
+ controller_device_path: bytes or str - path to the i2c-pseudo controller
+ device file
+ servo_i2c_bus: implementation of i2c_base.BaseI2CBus
+ """
+ self._logger = logging.getLogger('i2c_pseudo')
+ self._logger.info(
+ 'attempting to initialize (not start yet!) I2C pseudo adapter '
+ 'controller_device_path=%r servo_i2c_bus=%r' %
+ (controller_device_path, servo_i2c_bus))
+
+ self._servo_i2c_bus = servo_i2c_bus
+ self._controller_device_path = controller_device_path
+
+ self._device_fd = None
+ self._i2c_pseudo_id = None
+ self._i2c_adapter_num = None
+
+ self._epoll = None
+ self._device_eventmask_lock = threading.Lock()
+ self._device_epoll_eventmask = _EPOLL_EVENTMASK_NO_WRITES
+
+ self._io_thread = None
+
+ self._xfer_reqs = []
+ self._in_tx = False
+
+ self._device_read_buffers = []
+ self._device_read_post_newline_idx = 0
+
+ self._device_write_lock = threading.Lock()
+ # self._device_write_lock must be held while popping items, processing
+ # items, or appending to the right side. That lock does NOT need to be held
+ # when appending items to the left side.
+ self._device_write_queue = collections.deque()
+
+ self._startstop_lock = threading.Lock()
+ self._started = False
+
+ self._logger.info(
+ 'finished initializing I2C pseudo adapter (not started yet!)')
+
+ def start(self):
+ """Create and start the i2c-pseudo adapter.
+
+ This method may be invoked repeatedly, including overlapping invocations
+ from multiple threads. Redundant invocations are a no-op. When any one
+ invocation has returned successfully (no exceptions), the I2C pseudo adapter
+ has been started.
+
+ If an invocation fails with an exception, the state of the object is
+ undefined, and it should be abandoned.
+
+ This MUST NOT be called during or after shutdown().
+ """
+ self._logger.info('attempting to start I2C pseudo adapter')
+
+ with self._startstop_lock:
+ assert self._started is not None
+
+ if self._started:
+ self._logger.warn('I2C pseudo adapter already started')
+ return
+
+ self._started = True
+ self._device_fd = os.open(self._controller_device_path,
+ os.O_RDWR | os.O_NONBLOCK)
+ self._epoll = select.epoll(sizehint=2)
+ self._epoll.register(self._device_fd, self._device_epoll_eventmask)
+
+ self._io_thread = threading.Thread(
+ name='I2C-Pseudo-PyID-0x%X' % (id(self),),
+ target=self._io_thread_run)
+ self._io_thread.daemon = True
+ self._io_thread.start()
+
+ self._enqueue_simple_ctrlr_cmd((b'GET_PSEUDO_ID',))
+ self._enqueue_simple_ctrlr_cmd((
+ b'SET_ADAPTER_NAME_SUFFIX', b'(servod pid %d)' % (os.getpid(),)))
+ self._enqueue_simple_ctrlr_cmd((
+ b'SET_ADAPTER_TIMEOUT_MS', b'%d' % (_I2C_ADAPTER_TIMEOUT_MS,)))
+ self._enqueue_simple_ctrlr_cmd((b'ADAPTER_START',))
+ self._enqueue_simple_ctrlr_cmd((b'GET_ADAPTER_NUM',))
+ self._do_device_writes()
+
+ self._logger.info('finished starting I2C pseudo adapter')
+
+ def servo_i2c_bus(self):
+ """Get the i2c_base.BaseI2CBus implementation this object is using.
+
+ Returns:
+ i2c_base.BaseI2CBus
+ """
+ return self._servo_i2c_bus
+
+ def controller_device_path(self):
+ """Get the i2c-pseudo controller device file this object is using.
+
+ Returns:
+ bytes or str - path to the i2c-pseudo controller device file
+ """
+ return self._controller_device_path
+
+ def i2c_pseudo_id(self):
+ """Get the i2c-pseudo controller ID.
+
+ Returns:
+ None or int - The i2c-pseudo controller ID, or None if start() has not
+ completed yet.
+ """
+ return self._i2c_pseudo_id
+
+ def i2c_adapter_num(self):
+ """Get the Linux I2C adapter number.
+
+ Returns:
+ None or int - The Linux I2C adapter number, or None if start() has not
+ completed yet.
+ """
+ return self._i2c_adapter_num
+
+ def is_running(self):
+ """Check whether the pseudo controller is running.
+
+ Returns:
+ bool - True if the pseudo controller and its I/O thread are running, False
+ otherwise, e.g. if the controller was either never started, or has
+ been shutdown.
+ """
+ return self._io_thread is not None and self._io_thread.is_alive()
+
+ def _reset_tx(self, in_tx):
+ """Delete any queued I2C transfer requests and reset transaction state.
+
+ Args:
+ in_tx: bool - The internal transaction state is set to this value.
+ """
+ del self._xfer_reqs[:]
+ self._in_tx = in_tx
+
+ def _cmd_i2c_begin_xfer(self, line):
+ """Allow queueing of I2C transfer requests.
+
+ This always resets the internal transaction state to be in a transaction and
+ have no I2C transfer requests queued.
+
+ Args:
+ line: str - The I2C_BEGIN_XFER line read from the i2c-pseudo device.
+ """
+ try:
+ assert not self._in_tx
+ finally:
+ self._reset_tx(True)
+
+ def _cmd_i2c_commit_xfer(self, line):
+ """Perform the queued I2C transaction.
+
+ This always resets the internal transaction state to not be in a transaction
+ and have no I2C transfer requests queued.
+
+ Args:
+ line: str - The I2C_COMMIT_XFER line read from the i2c-pseudo device.
+ """
+ try:
+ self._cmd_i2c_commit_xfer_internal(line)
+ finally:
+ self._reset_tx(False)
+
+ def _cmd_i2c_commit_xfer_internal(self, line):
+ """Perform the queued I2C transaction.
+
+ Invocations to this should be wrapped in try:/finally: to always reset the
+ internal transaction state, regardless of success or failure.
+
+ Args:
+ line: str - The I2C_COMMIT_XFER line read from the i2c-pseudo device.
+ """
+ assert self._in_tx
+ if not self._xfer_reqs:
+ return
+ assert len(self._xfer_reqs) <= 2
+ assert len(set(xfer_id for xfer_id, _, _, _, _, _ in self._xfer_reqs)) == 1
+ assert len(set(addr for _, _, addr, _, _, _ in self._xfer_reqs)) == 1
+
+ write_idx = None
+ write_list = None
+ read_idx = None
+ read_count = None
+ read_flags = 0
+ retval = None
+ errnum = 0
+
+ for xfer_id, idx, addr, flags, length, data in self._xfer_reqs:
+ # This option is not supported by the self._servo_i2c_bus interface.
+ assert not flags & I2C_M_RECV_LEN
+ if flags & I2C_M_RD:
+ read_idx = idx
+ read_count = length
+ read_flags = flags
+ else:
+ write_idx = idx
+ # TODO(b/79684405): This is silly, wr_rd() often/always converts back to
+ # byte array, i.e. Python 2 str / Python 3 bytes, using chr(). Update
+ # servo I2C bus interface to accept byte array.
+ write_list = [ord(c) for c in data]
+ write_flags = flags
+
+ try:
+ retval = self._servo_i2c_bus.wr_rd(addr, write_list, read_count)
+ except (OSError, IOError) as error:
+ self._logger.exception(
+ 'self._servo_i2c_bus.wr_rd() raised %s' % (error,))
+ errnum = error.errno or 1
+
+ writes = []
+ if write_idx is not None:
+ writes.append(_I2C_XFER_REPLY_FMT_STR % (
+ xfer_id, write_idx, addr, write_flags, errnum))
+ writes.append(_CMD_END_CHAR)
+
+ if read_idx is not None:
+ writes.append(_I2C_XFER_REPLY_FMT_STR % (
+ xfer_id, read_idx, addr, read_flags, errnum))
+ if retval:
+ writes.append(_HEADER_SEP_CHAR)
+ writes.append(_DATA_SEP_CHAR.join(b'%02X' % (b,) for b in retval))
+ writes.append(_CMD_END_CHAR)
+
+ if writes:
+ self._device_write_queue.extend(writes)
+ self._do_device_writes()
+
+ def _cmd_i2c_xfer_req(self, line):
+ """Queue an I2C transfer request. Must already be in a transaction.
+
+ Args:
+ line: str - The I2C_XFER_REQ line read from the i2c-pseudo device.
+ """
+ assert self._in_tx
+
+ xfer_id, idx, addr, flags, length = line.split(_HEADER_SEP_CHAR, 6)[1:6]
+ xfer_id = int(xfer_id, base=0)
+ idx = int(idx, base=0)
+ addr = int(addr, base=0)
+ flags = int(flags, base=0)
+ length = int(length, base=0)
+
+ if flags & I2C_M_RD:
+ data = None
+ else:
+ parts = line.split(_HEADER_SEP_CHAR, 6)
+ if len(parts) < 5:
+ data = b''
+ else:
+ data = b''.join(
+ chr(int(hex_, 16)) for hex_ in parts[6].split(_DATA_SEP_CHAR))
+ self._xfer_reqs.append((xfer_id, idx, addr, flags, length, data))
+
+ def _cmd_i2c_adap_num(self, line):
+ """Record the I2C adapter number of this I2C pseudo controller.
+
+ Args:
+ line: str - The I2C_ADAPTER_NUM line read from the i2c-pseudo device.
+ """
+ self._i2c_adapter_num = int(line.split(_HEADER_SEP_CHAR, 2)[1])
+ self._logger.info('I2C adapter number: %d' % (self._i2c_adapter_num,))
+
+ def _cmd_i2c_pseudo_id(self, line):
+ """Record the I2C pseudo ID of this I2C pseudo controller.
+
+ Args:
+ line: str - The I2C_PSEUDO_ID line read from the i2c-pseudo device.
+ """
+ self._i2c_pseudo_id = int(line.split(_HEADER_SEP_CHAR, 2)[1])
+ self._logger.info('I2C pseudo ID: %d' % (self._i2c_pseudo_id,))
+
+ def _do_ctrlr_cmd(self, line):
+ """Dispatch an I2C pseudo controller command to the appropriate handler.
+
+ Args:
+ line: A full read command line from the i2c-pseudo controller device.
+ Must NOT contain the commmand-terminating character (_CMD_END_CHAR).
+ """
+ if not line:
+ return
+ assert _CMD_END_CHAR not in line
+ cmd_name = line.split(_HEADER_SEP_CHAR, 1)[0]
+ if cmd_name == b'I2C_BEGIN_XFER':
+ self._cmd_i2c_begin_xfer(line)
+ elif cmd_name == b'I2C_COMMIT_XFER':
+ self._cmd_i2c_commit_xfer(line)
+ elif cmd_name == b'I2C_XFER_REQ':
+ self._cmd_i2c_xfer_req(line)
+ elif cmd_name == b'I2C_ADAPTER_NUM':
+ self._cmd_i2c_adap_num(line)
+ elif cmd_name == b'I2C_PSEUDO_ID':
+ self._cmd_i2c_pseudo_id(line)
+ else:
+ self._logger.warn(
+ 'unrecognized I2C pseudo controller device command name %r' %
+ (cmd_name,))
+
+ def _do_device_reads(self):
+ """Read commands from the controller fd.
+
+ This is NOT thread-safe. Do not make multiple concurrent calls to this.
+
+ This will read until EOF or an error is returned.
+
+ Returns:
+ bool - True if EOF was encountered, False otherwise.
+ """
+ eof = False
+
+ while True:
+ try:
+ data = os.read(self._device_fd, _READ_SIZE)
+ except EOFError:
+ eof = True
+ break
+ except (OSError, IOError) as error:
+ if error.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
+ break
+ raise
+
+ if not data:
+ eof = True
+ break
+
+ while data:
+ data_newline_idx = data.find(_CMD_END_CHAR)
+ if data_newline_idx < 0:
+ self._device_read_buffers.append(data)
+ break
+ self._device_read_buffers.append(data[:data_newline_idx])
+ self._do_ctrlr_cmd(b''.join(self._device_read_buffers))
+ del self._device_read_buffers[:]
+ data = data[data_newline_idx + 1:]
+
+ return eof
+
+ def _do_device_writes(self):
+ """Write all buffers in self._device_write_lock to controller fd.
+
+ This is NOT guaranteed to empty self._device_write_queue. This will return
+ early if the write would block, or if EOF is encountered.
+
+ Returns:
+ bool - True if EOF was encountered, False otherwise.
+ """
+ eof = False
+
+ with self._device_write_lock:
+ while self._device_write_queue:
+ data = self._device_write_queue.popleft()
+ if not data:
+ continue
+ try:
+ count = os.write(self._device_fd, data)
+ # TODO(b/79684405): Should EOF i.e. zero return value from write(2) be
+ # considered transient instead of permanent?
+ except EOFError:
+ eof = True
+ break
+ except (OSError, IOError) as error:
+ if error.errno in (errno.EAGAIN, errno.EWOULDBLOCK):
+ break
+ raise
+ if count <= 0:
+ break
+ if count < len(data):
+ self._device_write_queue.appendleft(data[count:])
+
+ # self._device_write_queue may still have items if we had to break from
+ # the loop above.
+ self._set_device_eventmask(
+ _EPOLL_EVENTMASK_WITH_WRITES if self._device_write_queue else
+ _EPOLL_EVENTMASK_NO_WRITES)
+
+ return eof
+
+ def _set_device_eventmask(self, eventmask):
+ with self._device_eventmask_lock:
+ if eventmask != self._device_epoll_eventmask:
+ self._epoll.modify(self._device_fd, eventmask)
+ self._device_epoll_eventmask = eventmask
+
+ def _device_poll_handler(self, event):
+ """Handle a poll event from the I2C pseudo controller device.
+
+ Args:
+ event: int - epoll event mask
+
+ Returns:
+ bool - True if EOF was encountered, False otherwise.
+ """
+ eof = False
+ if event & _EPOLL_EVENTMASK_NO_WRITES:
+ eof |= self._do_device_reads()
+ if event & select.EPOLLOUT:
+ eof |= self._do_device_writes()
+ return eof
+
+ def _io_thread_run(self):
+ """Entry point for thread which handles all I2C pseudo controller I/O.
+
+ This handles both I/O with i2c-pseudo kernel module, and I/O with the Servo
+ I2C bus. Those two sides of this controller could be split into separate
+ threads, and the i2c-pseudo side could be further split into separate read
+ and write threads, however any performance difference is likely minimal and
+ would not justify the added complexity.
+ """
+ keep_looping = True
+ while keep_looping:
+ try:
+ epoll_ret = self._epoll.poll()
+ except (IOError, OSError) as error:
+ # Python 3.5+ will retry after EINTR automatically, delete this after
+ # upgrading.
+ if error.errno == errno.EINTR:
+ continue
+ raise
+ for fd, event in epoll_ret:
+ if fd == self._device_fd:
+ if self._device_poll_handler(event):
+ keep_looping = False
+
+ def _enqueue_simple_ctrlr_cmd(self, cmd_args):
+ """Add a I2C pseudo controller write command to the write queue.
+
+ Args:
+ cmd_ags: iterable of bytes - The header fields of the command, and
+ optional data field as final item.
+ """
+ self._device_write_queue.append(
+ _HEADER_SEP_CHAR.join(cmd_args) + _CMD_END_CHAR)
+
+ def shutdown(self, timeout):
+ """Shutdown the I2C pseudo adapter.
+
+ start() MUST NOT be called during or after this.
+
+ Args:
+ timeout: None, int, or float - Wait this many seconds for the I/O thread
+ to complete, or None to wait indefinitely. Use 0 or 0.0 to not wait.
+
+ Returns:
+ bool - True if the pseudo controller and its I/O thread stopped before
+ expiration of the timeout, False otherwise.
+ """
+ with self._startstop_lock:
+ started = self._started
+ self._started = None
+
+ if self._device_fd is not None:
+ self._enqueue_simple_ctrlr_cmd((b'ADAPTER_SHUTDOWN',))
+ self._do_device_writes()
+
+ if not started:
+ if self._device_fd is not None:
+ try:
+ os.close(self._device_fd)
+ finally:
+ self._device_fd = None
+
+ if self._io_thread is None:
+ return True
+
+ if timeout is None or timeout > 0:
+ self._io_thread.join(timeout=timeout)
+ return not self._io_thread.is_alive()
+
+ def __del__(self):
+ self.shutdown(timeout=0)
diff --git a/servo/stm32i2c.py b/servo/stm32i2c.py
index d29c0b1..1dc93f5 100644
--- a/servo/stm32i2c.py
+++ b/servo/stm32i2c.py
@@ -62,10 +62,6 @@
self._logger.debug('Set up stm32 i2c')
- def __del__(self):
- """Si2c destructor."""
- self._logger.debug('Close')
-
def reinitialize(self):
"""Reinitialize the usb endpoint"""
self._susb.reset_usb()
@@ -155,3 +151,4 @@
"""
self._logger.info('Turning down STM32i2c interface.')
del self._susb
+ super(Si2cBus, self).close()