# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import Queue import struct import sys import threading import time import traceback import uuid import weakref from cobs import cobs import serial from . import exceptions import stm32_crc logger = logging.getLogger(__name__) try: import pyftdi.serialext except ImportError: pass DBGSERIAL_PORT_SETTINGS = dict(baudrate=230400, timeout=0.1, interCharTimeout=0.01) def get_dbgserial_tty(): # Local import so that we only depend on this package if we're attempting # to autodetect the TTY. This package isn't always available (e.g., MFG), # so we don't want it to be required. try: import pebble_tty return pebble_tty.find_dbgserial_tty() except ImportError: raise exceptions.TTYAutodetectionUnavailable def frame_splitter(istream, size=1024, timeout=1, delimiter='\0'): '''Returns an iterator which yields complete frames.''' partial = [] start_time = time.time() while not istream.closed: data = istream.read(size) logger.debug('frame_splitter: received %r', data) while True: left, delim, data = data.partition(delimiter) if left: partial.append(left) if delim: frame = ''.join(partial) partial = [] if frame: yield frame if not data: break if timeout > 0 and time.time() > start_time + timeout: yield def decode_frame(frame): '''Decodes a PULSE frame. Returns a tuple (protocol, payload) of the decoded frame. Raises FrameDecodeError if the frame is not valid. ''' try: data = cobs.decode(frame) except cobs.DecodeError, e: raise exceptions.FrameDecodeError(e.message) if len(data) < 5: raise exceptions.FrameDecodeError('frame too short') fcs = struct.unpack('. ''' try: getattr(cls, name) except AttributeError: cls.EXTENSIONS[name] = factory else: raise ValueError('extension name %r clashes with existing attribute' % (name,)) @classmethod def open_dbgserial(cls, url=None, infinite_reconnect=False): if url is None: url = get_dbgserial_tty() if url == "qemu": url = 'socket://localhost:12345' ser = serial.serial_for_url(url, **DBGSERIAL_PORT_SETTINGS) if url.startswith('socket://'): # Socket class for PySerial does some pointless buffering # setting a very small timeout effectively negates it ser._timeout = 0.00001 return cls(ser, infinite_reconnect=infinite_reconnect) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def __del__(self): self.close() def send(self, protocol, payload): if self.closed: raise exceptions.PulseError('I/O operation on closed connection') frame = ''.join(('\0', encode_frame(protocol, payload), '\0')) logger.debug('Connection: sending %r', frame) with self.send_lock: self.iostream.write(frame) def run_receive_thread(self): logger.debug('Connection: receive thread started') receiver = frame_splitter(self.iostream, timeout=0) while True: try: protocol, payload = decode_frame(next(receiver)) except exceptions.FrameDecodeError: continue except: # Probably a PySerial exception complaining about reading from a # closed port. Eat the exception and shut down the thread; users # don't need to see the stack trace. logger.debug('Connection: exception in receive thread:\n%s', traceback.format_exc()) break logger.debug('Connection:run_receive_thread: ' 'protocol=%d payload=%r', protocol, payload) if protocol == self.PROTOCOL_LLC: # LLC can't be overridden self.llc_handler(payload) continue try: handler = self.protocol_handlers[protocol] except KeyError: self.default_receiver(protocol, payload) else: handler.on_receive(payload) logger.debug('Connection: receive thread exiting') def default_receiver(self, protocol, frame): logger.info('Connection:default_receiver: received frame ' 'with protocol %d: %r', protocol, frame) def register_protocol_handler(self, protocol, handler): '''Register a handler for frames bearing the specified protocol number. handler.on_receive(payload) is called for each frame received with the protocol number. Protocol handlers can be unregistered by calling this function with a handler of None. ''' if not handler: try: del self.protocol_handlers[protocol] except KeyError: pass return if protocol in self.protocol_handlers: raise exceptions.ProtocolAlreadyRegistered( 'Protocol %d is already registered by %r' % ( protocol, self.protocol_handlers[protocol])) if not hasattr(handler, 'on_receive'): raise ValueError('%r does not have an on_receive method') self.protocol_handlers[protocol] = handler def llc_handler(self, frame): opcode = ord(frame[0]) if opcode == self.LLC_LINK_OPENED: # MTU and MRU are from the perspective of this side of the # connection version, mru, mtu, timeout = struct.unpack('