1352 lines
58 KiB
Python
1352 lines
58 KiB
Python
|
#! python
|
||
|
#
|
||
|
# This module implements a RFC2217 compatible client. RF2217 descibes a
|
||
|
# protocol to access serial ports over TCP/IP and allows setting the baud rate,
|
||
|
# modem control lines etc.
|
||
|
#
|
||
|
# 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
|
||
|
|
||
|
# TODO:
|
||
|
# - setting control line -> answer is not checked (had problems with one of the
|
||
|
# severs). consider implementing a compatibility mode flag to make check
|
||
|
# conditional
|
||
|
# - write timeout not implemented at all
|
||
|
|
||
|
# ###########################################################################
|
||
|
# observations and issues with servers
|
||
|
# ===========================================================================
|
||
|
# sredird V2.2.1
|
||
|
# - http://www.ibiblio.org/pub/Linux/system/serial/ sredird-2.2.2.tar.gz
|
||
|
# - does not acknowledge SET_CONTROL (RTS/DTR) correctly, always responding
|
||
|
# [105 1] instead of the actual value.
|
||
|
# - SET_BAUDRATE answer contains 4 extra null bytes -> probably for larger
|
||
|
# numbers than 2**32?
|
||
|
# - To get the signature [COM_PORT_OPTION 0] has to be sent.
|
||
|
# - run a server: while true; do nc -l -p 7000 -c "sredird debug /dev/ttyUSB0 /var/lock/sredir"; done
|
||
|
# ===========================================================================
|
||
|
# telnetcpcd (untested)
|
||
|
# - http://ftp.wayne.edu/kermit/sredird/telnetcpcd-1.09.tar.gz
|
||
|
# - To get the signature [COM_PORT_OPTION] w/o data has to be sent.
|
||
|
# ===========================================================================
|
||
|
# ser2net
|
||
|
# - does not negotiate BINARY or COM_PORT_OPTION for his side but at least
|
||
|
# acknowledges that the client activates these options
|
||
|
# - The configuration may be that the server prints a banner. As this client
|
||
|
# implementation does a flushInput on connect, this banner is hidden from
|
||
|
# the user application.
|
||
|
# - NOTIFY_MODEMSTATE: the poll interval of the server seems to be one
|
||
|
# second.
|
||
|
# - To get the signature [COM_PORT_OPTION 0] has to be sent.
|
||
|
# - run a server: run ser2net daemon, in /etc/ser2net.conf:
|
||
|
# 2000:telnet:0:/dev/ttyS0:9600 remctl banner
|
||
|
# ###########################################################################
|
||
|
|
||
|
# How to identify ports? pySerial might want to support other protocols in the
|
||
|
# future, so lets use an URL scheme.
|
||
|
# for RFC2217 compliant servers we will use this:
|
||
|
# rfc2217://<host>:<port>[?option[&option...]]
|
||
|
#
|
||
|
# options:
|
||
|
# - "logging" set log level print diagnostic messages (e.g. "logging=debug")
|
||
|
# - "ign_set_control": do not look at the answers to SET_CONTROL
|
||
|
# - "poll_modem": issue NOTIFY_MODEMSTATE requests when CTS/DTR/RI/CD is read.
|
||
|
# Without this option it expects that the server sends notifications
|
||
|
# automatically on change (which most servers do and is according to the
|
||
|
# RFC).
|
||
|
# the order of the options is not relevant
|
||
|
|
||
|
from __future__ import absolute_import
|
||
|
|
||
|
import logging
|
||
|
import socket
|
||
|
import struct
|
||
|
import threading
|
||
|
import time
|
||
|
try:
|
||
|
import urlparse
|
||
|
except ImportError:
|
||
|
import urllib.parse as urlparse
|
||
|
try:
|
||
|
import Queue
|
||
|
except ImportError:
|
||
|
import queue as Queue
|
||
|
|
||
|
import serial
|
||
|
from serial.serialutil import SerialBase, SerialException, to_bytes, \
|
||
|
iterbytes, PortNotOpenError, Timeout
|
||
|
|
||
|
# port string is expected to be something like this:
|
||
|
# rfc2217://host:port
|
||
|
# host may be an IP or including domain, whatever.
|
||
|
# port is 0...65535
|
||
|
|
||
|
# map log level names to constants. used in from_url()
|
||
|
LOGGER_LEVELS = {
|
||
|
'debug': logging.DEBUG,
|
||
|
'info': logging.INFO,
|
||
|
'warning': logging.WARNING,
|
||
|
'error': logging.ERROR,
|
||
|
}
|
||
|
|
||
|
|
||
|
# telnet protocol characters
|
||
|
SE = b'\xf0' # Subnegotiation End
|
||
|
NOP = b'\xf1' # No Operation
|
||
|
DM = b'\xf2' # Data Mark
|
||
|
BRK = b'\xf3' # Break
|
||
|
IP = b'\xf4' # Interrupt process
|
||
|
AO = b'\xf5' # Abort output
|
||
|
AYT = b'\xf6' # Are You There
|
||
|
EC = b'\xf7' # Erase Character
|
||
|
EL = b'\xf8' # Erase Line
|
||
|
GA = b'\xf9' # Go Ahead
|
||
|
SB = b'\xfa' # Subnegotiation Begin
|
||
|
WILL = b'\xfb'
|
||
|
WONT = b'\xfc'
|
||
|
DO = b'\xfd'
|
||
|
DONT = b'\xfe'
|
||
|
IAC = b'\xff' # Interpret As Command
|
||
|
IAC_DOUBLED = b'\xff\xff'
|
||
|
|
||
|
# selected telnet options
|
||
|
BINARY = b'\x00' # 8-bit data path
|
||
|
ECHO = b'\x01' # echo
|
||
|
SGA = b'\x03' # suppress go ahead
|
||
|
|
||
|
# RFC2217
|
||
|
COM_PORT_OPTION = b'\x2c'
|
||
|
|
||
|
# Client to Access Server
|
||
|
SET_BAUDRATE = b'\x01'
|
||
|
SET_DATASIZE = b'\x02'
|
||
|
SET_PARITY = b'\x03'
|
||
|
SET_STOPSIZE = b'\x04'
|
||
|
SET_CONTROL = b'\x05'
|
||
|
NOTIFY_LINESTATE = b'\x06'
|
||
|
NOTIFY_MODEMSTATE = b'\x07'
|
||
|
FLOWCONTROL_SUSPEND = b'\x08'
|
||
|
FLOWCONTROL_RESUME = b'\x09'
|
||
|
SET_LINESTATE_MASK = b'\x0a'
|
||
|
SET_MODEMSTATE_MASK = b'\x0b'
|
||
|
PURGE_DATA = b'\x0c'
|
||
|
|
||
|
SERVER_SET_BAUDRATE = b'\x65'
|
||
|
SERVER_SET_DATASIZE = b'\x66'
|
||
|
SERVER_SET_PARITY = b'\x67'
|
||
|
SERVER_SET_STOPSIZE = b'\x68'
|
||
|
SERVER_SET_CONTROL = b'\x69'
|
||
|
SERVER_NOTIFY_LINESTATE = b'\x6a'
|
||
|
SERVER_NOTIFY_MODEMSTATE = b'\x6b'
|
||
|
SERVER_FLOWCONTROL_SUSPEND = b'\x6c'
|
||
|
SERVER_FLOWCONTROL_RESUME = b'\x6d'
|
||
|
SERVER_SET_LINESTATE_MASK = b'\x6e'
|
||
|
SERVER_SET_MODEMSTATE_MASK = b'\x6f'
|
||
|
SERVER_PURGE_DATA = b'\x70'
|
||
|
|
||
|
RFC2217_ANSWER_MAP = {
|
||
|
SET_BAUDRATE: SERVER_SET_BAUDRATE,
|
||
|
SET_DATASIZE: SERVER_SET_DATASIZE,
|
||
|
SET_PARITY: SERVER_SET_PARITY,
|
||
|
SET_STOPSIZE: SERVER_SET_STOPSIZE,
|
||
|
SET_CONTROL: SERVER_SET_CONTROL,
|
||
|
NOTIFY_LINESTATE: SERVER_NOTIFY_LINESTATE,
|
||
|
NOTIFY_MODEMSTATE: SERVER_NOTIFY_MODEMSTATE,
|
||
|
FLOWCONTROL_SUSPEND: SERVER_FLOWCONTROL_SUSPEND,
|
||
|
FLOWCONTROL_RESUME: SERVER_FLOWCONTROL_RESUME,
|
||
|
SET_LINESTATE_MASK: SERVER_SET_LINESTATE_MASK,
|
||
|
SET_MODEMSTATE_MASK: SERVER_SET_MODEMSTATE_MASK,
|
||
|
PURGE_DATA: SERVER_PURGE_DATA,
|
||
|
}
|
||
|
|
||
|
SET_CONTROL_REQ_FLOW_SETTING = b'\x00' # Request Com Port Flow Control Setting (outbound/both)
|
||
|
SET_CONTROL_USE_NO_FLOW_CONTROL = b'\x01' # Use No Flow Control (outbound/both)
|
||
|
SET_CONTROL_USE_SW_FLOW_CONTROL = b'\x02' # Use XON/XOFF Flow Control (outbound/both)
|
||
|
SET_CONTROL_USE_HW_FLOW_CONTROL = b'\x03' # Use HARDWARE Flow Control (outbound/both)
|
||
|
SET_CONTROL_REQ_BREAK_STATE = b'\x04' # Request BREAK State
|
||
|
SET_CONTROL_BREAK_ON = b'\x05' # Set BREAK State ON
|
||
|
SET_CONTROL_BREAK_OFF = b'\x06' # Set BREAK State OFF
|
||
|
SET_CONTROL_REQ_DTR = b'\x07' # Request DTR Signal State
|
||
|
SET_CONTROL_DTR_ON = b'\x08' # Set DTR Signal State ON
|
||
|
SET_CONTROL_DTR_OFF = b'\x09' # Set DTR Signal State OFF
|
||
|
SET_CONTROL_REQ_RTS = b'\x0a' # Request RTS Signal State
|
||
|
SET_CONTROL_RTS_ON = b'\x0b' # Set RTS Signal State ON
|
||
|
SET_CONTROL_RTS_OFF = b'\x0c' # Set RTS Signal State OFF
|
||
|
SET_CONTROL_REQ_FLOW_SETTING_IN = b'\x0d' # Request Com Port Flow Control Setting (inbound)
|
||
|
SET_CONTROL_USE_NO_FLOW_CONTROL_IN = b'\x0e' # Use No Flow Control (inbound)
|
||
|
SET_CONTROL_USE_SW_FLOW_CONTOL_IN = b'\x0f' # Use XON/XOFF Flow Control (inbound)
|
||
|
SET_CONTROL_USE_HW_FLOW_CONTOL_IN = b'\x10' # Use HARDWARE Flow Control (inbound)
|
||
|
SET_CONTROL_USE_DCD_FLOW_CONTROL = b'\x11' # Use DCD Flow Control (outbound/both)
|
||
|
SET_CONTROL_USE_DTR_FLOW_CONTROL = b'\x12' # Use DTR Flow Control (inbound)
|
||
|
SET_CONTROL_USE_DSR_FLOW_CONTROL = b'\x13' # Use DSR Flow Control (outbound/both)
|
||
|
|
||
|
LINESTATE_MASK_TIMEOUT = 128 # Time-out Error
|
||
|
LINESTATE_MASK_SHIFTREG_EMPTY = 64 # Transfer Shift Register Empty
|
||
|
LINESTATE_MASK_TRANSREG_EMPTY = 32 # Transfer Holding Register Empty
|
||
|
LINESTATE_MASK_BREAK_DETECT = 16 # Break-detect Error
|
||
|
LINESTATE_MASK_FRAMING_ERROR = 8 # Framing Error
|
||
|
LINESTATE_MASK_PARTIY_ERROR = 4 # Parity Error
|
||
|
LINESTATE_MASK_OVERRUN_ERROR = 2 # Overrun Error
|
||
|
LINESTATE_MASK_DATA_READY = 1 # Data Ready
|
||
|
|
||
|
MODEMSTATE_MASK_CD = 128 # Receive Line Signal Detect (also known as Carrier Detect)
|
||
|
MODEMSTATE_MASK_RI = 64 # Ring Indicator
|
||
|
MODEMSTATE_MASK_DSR = 32 # Data-Set-Ready Signal State
|
||
|
MODEMSTATE_MASK_CTS = 16 # Clear-To-Send Signal State
|
||
|
MODEMSTATE_MASK_CD_CHANGE = 8 # Delta Receive Line Signal Detect
|
||
|
MODEMSTATE_MASK_RI_CHANGE = 4 # Trailing-edge Ring Detector
|
||
|
MODEMSTATE_MASK_DSR_CHANGE = 2 # Delta Data-Set-Ready
|
||
|
MODEMSTATE_MASK_CTS_CHANGE = 1 # Delta Clear-To-Send
|
||
|
|
||
|
PURGE_RECEIVE_BUFFER = b'\x01' # Purge access server receive data buffer
|
||
|
PURGE_TRANSMIT_BUFFER = b'\x02' # Purge access server transmit data buffer
|
||
|
PURGE_BOTH_BUFFERS = b'\x03' # Purge both the access server receive data
|
||
|
# buffer and the access server transmit data buffer
|
||
|
|
||
|
|
||
|
RFC2217_PARITY_MAP = {
|
||
|
serial.PARITY_NONE: 1,
|
||
|
serial.PARITY_ODD: 2,
|
||
|
serial.PARITY_EVEN: 3,
|
||
|
serial.PARITY_MARK: 4,
|
||
|
serial.PARITY_SPACE: 5,
|
||
|
}
|
||
|
RFC2217_REVERSE_PARITY_MAP = dict((v, k) for k, v in RFC2217_PARITY_MAP.items())
|
||
|
|
||
|
RFC2217_STOPBIT_MAP = {
|
||
|
serial.STOPBITS_ONE: 1,
|
||
|
serial.STOPBITS_ONE_POINT_FIVE: 3,
|
||
|
serial.STOPBITS_TWO: 2,
|
||
|
}
|
||
|
RFC2217_REVERSE_STOPBIT_MAP = dict((v, k) for k, v in RFC2217_STOPBIT_MAP.items())
|
||
|
|
||
|
# Telnet filter states
|
||
|
M_NORMAL = 0
|
||
|
M_IAC_SEEN = 1
|
||
|
M_NEGOTIATE = 2
|
||
|
|
||
|
# TelnetOption and TelnetSubnegotiation states
|
||
|
REQUESTED = 'REQUESTED'
|
||
|
ACTIVE = 'ACTIVE'
|
||
|
INACTIVE = 'INACTIVE'
|
||
|
REALLY_INACTIVE = 'REALLY_INACTIVE'
|
||
|
|
||
|
|
||
|
class TelnetOption(object):
|
||
|
"""Manage a single telnet option, keeps track of DO/DONT WILL/WONT."""
|
||
|
|
||
|
def __init__(self, connection, name, option, send_yes, send_no, ack_yes,
|
||
|
ack_no, initial_state, activation_callback=None):
|
||
|
"""\
|
||
|
Initialize option.
|
||
|
:param connection: connection used to transmit answers
|
||
|
:param name: a readable name for debug outputs
|
||
|
:param send_yes: what to send when option is to be enabled.
|
||
|
:param send_no: what to send when option is to be disabled.
|
||
|
:param ack_yes: what to expect when remote agrees on option.
|
||
|
:param ack_no: what to expect when remote disagrees on option.
|
||
|
:param initial_state: options initialized with REQUESTED are tried to
|
||
|
be enabled on startup. use INACTIVE for all others.
|
||
|
"""
|
||
|
self.connection = connection
|
||
|
self.name = name
|
||
|
self.option = option
|
||
|
self.send_yes = send_yes
|
||
|
self.send_no = send_no
|
||
|
self.ack_yes = ack_yes
|
||
|
self.ack_no = ack_no
|
||
|
self.state = initial_state
|
||
|
self.active = False
|
||
|
self.activation_callback = activation_callback
|
||
|
|
||
|
def __repr__(self):
|
||
|
"""String for debug outputs"""
|
||
|
return "{o.name}:{o.active}({o.state})".format(o=self)
|
||
|
|
||
|
def process_incoming(self, command):
|
||
|
"""\
|
||
|
A DO/DONT/WILL/WONT was received for this option, update state and
|
||
|
answer when needed.
|
||
|
"""
|
||
|
if command == self.ack_yes:
|
||
|
if self.state is REQUESTED:
|
||
|
self.state = ACTIVE
|
||
|
self.active = True
|
||
|
if self.activation_callback is not None:
|
||
|
self.activation_callback()
|
||
|
elif self.state is ACTIVE:
|
||
|
pass
|
||
|
elif self.state is INACTIVE:
|
||
|
self.state = ACTIVE
|
||
|
self.connection.telnet_send_option(self.send_yes, self.option)
|
||
|
self.active = True
|
||
|
if self.activation_callback is not None:
|
||
|
self.activation_callback()
|
||
|
elif self.state is REALLY_INACTIVE:
|
||
|
self.connection.telnet_send_option(self.send_no, self.option)
|
||
|
else:
|
||
|
raise ValueError('option in illegal state {!r}'.format(self))
|
||
|
elif command == self.ack_no:
|
||
|
if self.state is REQUESTED:
|
||
|
self.state = INACTIVE
|
||
|
self.active = False
|
||
|
elif self.state is ACTIVE:
|
||
|
self.state = INACTIVE
|
||
|
self.connection.telnet_send_option(self.send_no, self.option)
|
||
|
self.active = False
|
||
|
elif self.state is INACTIVE:
|
||
|
pass
|
||
|
elif self.state is REALLY_INACTIVE:
|
||
|
pass
|
||
|
else:
|
||
|
raise ValueError('option in illegal state {!r}'.format(self))
|
||
|
|
||
|
|
||
|
class TelnetSubnegotiation(object):
|
||
|
"""\
|
||
|
A object to handle subnegotiation of options. In this case actually
|
||
|
sub-sub options for RFC 2217. It is used to track com port options.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, connection, name, option, ack_option=None):
|
||
|
if ack_option is None:
|
||
|
ack_option = option
|
||
|
self.connection = connection
|
||
|
self.name = name
|
||
|
self.option = option
|
||
|
self.value = None
|
||
|
self.ack_option = ack_option
|
||
|
self.state = INACTIVE
|
||
|
|
||
|
def __repr__(self):
|
||
|
"""String for debug outputs."""
|
||
|
return "{sn.name}:{sn.state}".format(sn=self)
|
||
|
|
||
|
def set(self, value):
|
||
|
"""\
|
||
|
Request a change of the value. a request is sent to the server. if
|
||
|
the client needs to know if the change is performed he has to check the
|
||
|
state of this object.
|
||
|
"""
|
||
|
self.value = value
|
||
|
self.state = REQUESTED
|
||
|
self.connection.rfc2217_send_subnegotiation(self.option, self.value)
|
||
|
if self.connection.logger:
|
||
|
self.connection.logger.debug("SB Requesting {} -> {!r}".format(self.name, self.value))
|
||
|
|
||
|
def is_ready(self):
|
||
|
"""\
|
||
|
Check if answer from server has been received. when server rejects
|
||
|
the change, raise a ValueError.
|
||
|
"""
|
||
|
if self.state == REALLY_INACTIVE:
|
||
|
raise ValueError("remote rejected value for option {!r}".format(self.name))
|
||
|
return self.state == ACTIVE
|
||
|
# add property to have a similar interface as TelnetOption
|
||
|
active = property(is_ready)
|
||
|
|
||
|
def wait(self, timeout=3):
|
||
|
"""\
|
||
|
Wait until the subnegotiation has been acknowledged or timeout. It
|
||
|
can also throw a value error when the answer from the server does not
|
||
|
match the value sent.
|
||
|
"""
|
||
|
timeout_timer = Timeout(timeout)
|
||
|
while not timeout_timer.expired():
|
||
|
time.sleep(0.05) # prevent 100% CPU load
|
||
|
if self.is_ready():
|
||
|
break
|
||
|
else:
|
||
|
raise SerialException("timeout while waiting for option {!r}".format(self.name))
|
||
|
|
||
|
def check_answer(self, suboption):
|
||
|
"""\
|
||
|
Check an incoming subnegotiation block. The parameter already has
|
||
|
cut off the header like sub option number and com port option value.
|
||
|
"""
|
||
|
if self.value == suboption[:len(self.value)]:
|
||
|
self.state = ACTIVE
|
||
|
else:
|
||
|
# error propagation done in is_ready
|
||
|
self.state = REALLY_INACTIVE
|
||
|
if self.connection.logger:
|
||
|
self.connection.logger.debug("SB Answer {} -> {!r} -> {}".format(self.name, suboption, self.state))
|
||
|
|
||
|
|
||
|
class Serial(SerialBase):
|
||
|
"""Serial port implementation for RFC 2217 remote serial ports."""
|
||
|
|
||
|
BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
|
||
|
9600, 19200, 38400, 57600, 115200)
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
self._thread = None
|
||
|
self._socket = None
|
||
|
self._linestate = 0
|
||
|
self._modemstate = None
|
||
|
self._modemstate_timeout = Timeout(-1)
|
||
|
self._remote_suspend_flow = False
|
||
|
self._write_lock = None
|
||
|
self.logger = None
|
||
|
self._ignore_set_control_answer = False
|
||
|
self._poll_modem_state = False
|
||
|
self._network_timeout = 3
|
||
|
self._telnet_options = None
|
||
|
self._rfc2217_port_settings = None
|
||
|
self._rfc2217_options = None
|
||
|
self._read_buffer = None
|
||
|
super(Serial, self).__init__(*args, **kwargs) # must be last call in case of auto-open
|
||
|
|
||
|
def open(self):
|
||
|
"""\
|
||
|
Open port with current settings. This may throw a SerialException
|
||
|
if the port cannot be opened.
|
||
|
"""
|
||
|
self.logger = None
|
||
|
self._ignore_set_control_answer = False
|
||
|
self._poll_modem_state = False
|
||
|
self._network_timeout = 3
|
||
|
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._socket = socket.create_connection(self.from_url(self.portstr), timeout=5) # XXX good value?
|
||
|
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||
|
except Exception as msg:
|
||
|
self._socket = None
|
||
|
raise SerialException("Could not open port {}: {}".format(self.portstr, msg))
|
||
|
|
||
|
# use a thread save queue as buffer. it also simplifies implementing
|
||
|
# the read timeout
|
||
|
self._read_buffer = Queue.Queue()
|
||
|
# to ensure that user writes does not interfere with internal
|
||
|
# telnet/rfc2217 options establish a lock
|
||
|
self._write_lock = threading.Lock()
|
||
|
# name the following separately so that, below, a check can be easily done
|
||
|
mandadory_options = [
|
||
|
TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE),
|
||
|
TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED),
|
||
|
]
|
||
|
# all supported telnet options
|
||
|
self._telnet_options = [
|
||
|
TelnetOption(self, 'ECHO', ECHO, DO, DONT, WILL, WONT, REQUESTED),
|
||
|
TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED),
|
||
|
TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, REQUESTED),
|
||
|
TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, INACTIVE),
|
||
|
TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, REQUESTED),
|
||
|
] + mandadory_options
|
||
|
# RFC 2217 specific states
|
||
|
# COM port settings
|
||
|
self._rfc2217_port_settings = {
|
||
|
'baudrate': TelnetSubnegotiation(self, 'baudrate', SET_BAUDRATE, SERVER_SET_BAUDRATE),
|
||
|
'datasize': TelnetSubnegotiation(self, 'datasize', SET_DATASIZE, SERVER_SET_DATASIZE),
|
||
|
'parity': TelnetSubnegotiation(self, 'parity', SET_PARITY, SERVER_SET_PARITY),
|
||
|
'stopsize': TelnetSubnegotiation(self, 'stopsize', SET_STOPSIZE, SERVER_SET_STOPSIZE),
|
||
|
}
|
||
|
# There are more subnegotiation objects, combine all in one dictionary
|
||
|
# for easy access
|
||
|
self._rfc2217_options = {
|
||
|
'purge': TelnetSubnegotiation(self, 'purge', PURGE_DATA, SERVER_PURGE_DATA),
|
||
|
'control': TelnetSubnegotiation(self, 'control', SET_CONTROL, SERVER_SET_CONTROL),
|
||
|
}
|
||
|
self._rfc2217_options.update(self._rfc2217_port_settings)
|
||
|
# cache for line and modem states that the server sends to us
|
||
|
self._linestate = 0
|
||
|
self._modemstate = None
|
||
|
self._modemstate_timeout = Timeout(-1)
|
||
|
# RFC 2217 flow control between server and client
|
||
|
self._remote_suspend_flow = False
|
||
|
|
||
|
self.is_open = True
|
||
|
self._thread = threading.Thread(target=self._telnet_read_loop)
|
||
|
self._thread.setDaemon(True)
|
||
|
self._thread.setName('pySerial RFC 2217 reader thread for {}'.format(self._port))
|
||
|
self._thread.start()
|
||
|
|
||
|
try: # must clean-up if open fails
|
||
|
# negotiate Telnet/RFC 2217 -> send initial requests
|
||
|
for option in self._telnet_options:
|
||
|
if option.state is REQUESTED:
|
||
|
self.telnet_send_option(option.send_yes, option.option)
|
||
|
# now wait until important options are negotiated
|
||
|
timeout = Timeout(self._network_timeout)
|
||
|
while not timeout.expired():
|
||
|
time.sleep(0.05) # prevent 100% CPU load
|
||
|
if sum(o.active for o in mandadory_options) == sum(o.state != INACTIVE for o in mandadory_options):
|
||
|
break
|
||
|
else:
|
||
|
raise SerialException(
|
||
|
"Remote does not seem to support RFC2217 or BINARY mode {!r}".format(mandadory_options))
|
||
|
if self.logger:
|
||
|
self.logger.info("Negotiated options: {}".format(self._telnet_options))
|
||
|
|
||
|
# fine, go on, set RFC 2217 specific things
|
||
|
self._reconfigure_port()
|
||
|
# all things set up get, now a clean start
|
||
|
if not self._dsrdtr:
|
||
|
self._update_dtr_state()
|
||
|
if not self._rtscts:
|
||
|
self._update_rts_state()
|
||
|
self.reset_input_buffer()
|
||
|
self.reset_output_buffer()
|
||
|
except:
|
||
|
self.close()
|
||
|
raise
|
||
|
|
||
|
def _reconfigure_port(self):
|
||
|
"""Set communication parameters on opened port."""
|
||
|
if self._socket is None:
|
||
|
raise SerialException("Can only operate on open ports")
|
||
|
|
||
|
# if self._timeout != 0 and self._interCharTimeout is not None:
|
||
|
# XXX
|
||
|
|
||
|
if self._write_timeout is not None:
|
||
|
raise NotImplementedError('write_timeout is currently not supported')
|
||
|
# XXX
|
||
|
|
||
|
# Setup the connection
|
||
|
# to get good performance, all parameter changes are sent first...
|
||
|
if not 0 < self._baudrate < 2 ** 32:
|
||
|
raise ValueError("invalid baudrate: {!r}".format(self._baudrate))
|
||
|
self._rfc2217_port_settings['baudrate'].set(struct.pack(b'!I', self._baudrate))
|
||
|
self._rfc2217_port_settings['datasize'].set(struct.pack(b'!B', self._bytesize))
|
||
|
self._rfc2217_port_settings['parity'].set(struct.pack(b'!B', RFC2217_PARITY_MAP[self._parity]))
|
||
|
self._rfc2217_port_settings['stopsize'].set(struct.pack(b'!B', RFC2217_STOPBIT_MAP[self._stopbits]))
|
||
|
|
||
|
# and now wait until parameters are active
|
||
|
items = self._rfc2217_port_settings.values()
|
||
|
if self.logger:
|
||
|
self.logger.debug("Negotiating settings: {}".format(items))
|
||
|
timeout = Timeout(self._network_timeout)
|
||
|
while not timeout.expired():
|
||
|
time.sleep(0.05) # prevent 100% CPU load
|
||
|
if sum(o.active for o in items) == len(items):
|
||
|
break
|
||
|
else:
|
||
|
raise SerialException("Remote does not accept parameter change (RFC2217): {!r}".format(items))
|
||
|
if self.logger:
|
||
|
self.logger.info("Negotiated settings: {}".format(items))
|
||
|
|
||
|
if self._rtscts and self._xonxoff:
|
||
|
raise ValueError('xonxoff and rtscts together are not supported')
|
||
|
elif self._rtscts:
|
||
|
self.rfc2217_set_control(SET_CONTROL_USE_HW_FLOW_CONTROL)
|
||
|
elif self._xonxoff:
|
||
|
self.rfc2217_set_control(SET_CONTROL_USE_SW_FLOW_CONTROL)
|
||
|
else:
|
||
|
self.rfc2217_set_control(SET_CONTROL_USE_NO_FLOW_CONTROL)
|
||
|
|
||
|
def close(self):
|
||
|
"""Close port"""
|
||
|
self.is_open = False
|
||
|
if self._socket:
|
||
|
try:
|
||
|
self._socket.shutdown(socket.SHUT_RDWR)
|
||
|
self._socket.close()
|
||
|
except:
|
||
|
# ignore errors.
|
||
|
pass
|
||
|
if self._thread:
|
||
|
self._thread.join(7) # XXX more than socket timeout
|
||
|
self._thread = None
|
||
|
# in case of quick reconnects, give the server some time
|
||
|
time.sleep(0.3)
|
||
|
self._socket = None
|
||
|
|
||
|
def from_url(self, url):
|
||
|
"""\
|
||
|
extract host and port from an URL string, other settings are extracted
|
||
|
an stored in instance
|
||
|
"""
|
||
|
parts = urlparse.urlsplit(url)
|
||
|
if parts.scheme != "rfc2217":
|
||
|
raise SerialException(
|
||
|
'expected a string in the form '
|
||
|
'"rfc2217://<host>:<port>[?option[&option...]]": '
|
||
|
'not starting with rfc2217:// ({!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.rfc2217')
|
||
|
self.logger.setLevel(LOGGER_LEVELS[values[0]])
|
||
|
self.logger.debug('enabled logging')
|
||
|
elif option == 'ign_set_control':
|
||
|
self._ignore_set_control_answer = True
|
||
|
elif option == 'poll_modem':
|
||
|
self._poll_modem_state = True
|
||
|
elif option == 'timeout':
|
||
|
self._network_timeout = float(values[0])
|
||
|
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 '
|
||
|
'"rfc2217://<host>:<port>[?option[&option...]]": {}'.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()
|
||
|
return self._read_buffer.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()
|
||
|
data = bytearray()
|
||
|
try:
|
||
|
timeout = Timeout(self._timeout)
|
||
|
while len(data) < size:
|
||
|
if self._thread is None or not self._thread.is_alive():
|
||
|
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):
|
||
|
"""\
|
||
|
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()
|
||
|
with self._write_lock:
|
||
|
try:
|
||
|
self._socket.sendall(to_bytes(data).replace(IAC, IAC_DOUBLED))
|
||
|
except socket.error as e:
|
||
|
raise SerialException("connection failed (socket error): {}".format(e))
|
||
|
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.rfc2217_send_purge(PURGE_RECEIVE_BUFFER)
|
||
|
# empty read buffer
|
||
|
while self._read_buffer.qsize():
|
||
|
self._read_buffer.get(False)
|
||
|
|
||
|
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.rfc2217_send_purge(PURGE_TRANSMIT_BUFFER)
|
||
|
|
||
|
def _update_break_state(self):
|
||
|
"""\
|
||
|
Set break: Controls TXD. When active, to transmitting is
|
||
|
possible.
|
||
|
"""
|
||
|
if not self.is_open:
|
||
|
raise PortNotOpenError()
|
||
|
if self.logger:
|
||
|
self.logger.info('set BREAK to {}'.format('active' if self._break_state else 'inactive'))
|
||
|
if self._break_state:
|
||
|
self.rfc2217_set_control(SET_CONTROL_BREAK_ON)
|
||
|
else:
|
||
|
self.rfc2217_set_control(SET_CONTROL_BREAK_OFF)
|
||
|
|
||
|
def _update_rts_state(self):
|
||
|
"""Set terminal status line: Request To Send."""
|
||
|
if not self.is_open:
|
||
|
raise PortNotOpenError()
|
||
|
if self.logger:
|
||
|
self.logger.info('set RTS to {}'.format('active' if self._rts_state else 'inactive'))
|
||
|
if self._rts_state:
|
||
|
self.rfc2217_set_control(SET_CONTROL_RTS_ON)
|
||
|
else:
|
||
|
self.rfc2217_set_control(SET_CONTROL_RTS_OFF)
|
||
|
|
||
|
def _update_dtr_state(self):
|
||
|
"""Set terminal status line: Data Terminal Ready."""
|
||
|
if not self.is_open:
|
||
|
raise PortNotOpenError()
|
||
|
if self.logger:
|
||
|
self.logger.info('set DTR to {}'.format('active' if self._dtr_state else 'inactive'))
|
||
|
if self._dtr_state:
|
||
|
self.rfc2217_set_control(SET_CONTROL_DTR_ON)
|
||
|
else:
|
||
|
self.rfc2217_set_control(SET_CONTROL_DTR_OFF)
|
||
|
|
||
|
@property
|
||
|
def cts(self):
|
||
|
"""Read terminal status line: Clear To Send."""
|
||
|
if not self.is_open:
|
||
|
raise PortNotOpenError()
|
||
|
return bool(self.get_modem_state() & MODEMSTATE_MASK_CTS)
|
||
|
|
||
|
@property
|
||
|
def dsr(self):
|
||
|
"""Read terminal status line: Data Set Ready."""
|
||
|
if not self.is_open:
|
||
|
raise PortNotOpenError()
|
||
|
return bool(self.get_modem_state() & MODEMSTATE_MASK_DSR)
|
||
|
|
||
|
@property
|
||
|
def ri(self):
|
||
|
"""Read terminal status line: Ring Indicator."""
|
||
|
if not self.is_open:
|
||
|
raise PortNotOpenError()
|
||
|
return bool(self.get_modem_state() & MODEMSTATE_MASK_RI)
|
||
|
|
||
|
@property
|
||
|
def cd(self):
|
||
|
"""Read terminal status line: Carrier Detect."""
|
||
|
if not self.is_open:
|
||
|
raise PortNotOpenError()
|
||
|
return bool(self.get_modem_state() & MODEMSTATE_MASK_CD)
|
||
|
|
||
|
# - - - platform specific - - -
|
||
|
# None so far
|
||
|
|
||
|
# - - - RFC2217 specific - - -
|
||
|
|
||
|
def _telnet_read_loop(self):
|
||
|
"""Read loop for the socket."""
|
||
|
mode = M_NORMAL
|
||
|
suboption = None
|
||
|
try:
|
||
|
while self.is_open:
|
||
|
try:
|
||
|
data = self._socket.recv(1024)
|
||
|
except socket.timeout:
|
||
|
# just need to get out of recv form time to time to check if
|
||
|
# still alive
|
||
|
continue
|
||
|
except socket.error as e:
|
||
|
# connection fails -> terminate loop
|
||
|
if self.logger:
|
||
|
self.logger.debug("socket error in reader thread: {}".format(e))
|
||
|
self._read_buffer.put(None)
|
||
|
break
|
||
|
if not data:
|
||
|
self._read_buffer.put(None)
|
||
|
break # lost connection
|
||
|
for byte in iterbytes(data):
|
||
|
if mode == M_NORMAL:
|
||
|
# interpret as command or as data
|
||
|
if byte == IAC:
|
||
|
mode = M_IAC_SEEN
|
||
|
else:
|
||
|
# store data in read buffer or sub option buffer
|
||
|
# depending on state
|
||
|
if suboption is not None:
|
||
|
suboption += byte
|
||
|
else:
|
||
|
self._read_buffer.put(byte)
|
||
|
elif mode == M_IAC_SEEN:
|
||
|
if byte == IAC:
|
||
|
# interpret as command doubled -> insert character
|
||
|
# itself
|
||
|
if suboption is not None:
|
||
|
suboption += IAC
|
||
|
else:
|
||
|
self._read_buffer.put(IAC)
|
||
|
mode = M_NORMAL
|
||
|
elif byte == SB:
|
||
|
# sub option start
|
||
|
suboption = bytearray()
|
||
|
mode = M_NORMAL
|
||
|
elif byte == SE:
|
||
|
# sub option end -> process it now
|
||
|
self._telnet_process_subnegotiation(bytes(suboption))
|
||
|
suboption = None
|
||
|
mode = M_NORMAL
|
||
|
elif byte in (DO, DONT, WILL, WONT):
|
||
|
# negotiation
|
||
|
telnet_command = byte
|
||
|
mode = M_NEGOTIATE
|
||
|
else:
|
||
|
# other telnet commands
|
||
|
self._telnet_process_command(byte)
|
||
|
mode = M_NORMAL
|
||
|
elif mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
|
||
|
self._telnet_negotiate_option(telnet_command, byte)
|
||
|
mode = M_NORMAL
|
||
|
finally:
|
||
|
if self.logger:
|
||
|
self.logger.debug("read thread terminated")
|
||
|
|
||
|
# - incoming telnet commands and options
|
||
|
|
||
|
def _telnet_process_command(self, command):
|
||
|
"""Process commands other than DO, DONT, WILL, WONT."""
|
||
|
# Currently none. RFC2217 only uses negotiation and subnegotiation.
|
||
|
if self.logger:
|
||
|
self.logger.warning("ignoring Telnet command: {!r}".format(command))
|
||
|
|
||
|
def _telnet_negotiate_option(self, command, option):
|
||
|
"""Process incoming DO, DONT, WILL, WONT."""
|
||
|
# check our registered telnet options and forward command to them
|
||
|
# they know themselves if they have to answer or not
|
||
|
known = False
|
||
|
for item in self._telnet_options:
|
||
|
# can have more than one match! as some options are duplicated for
|
||
|
# 'us' and 'them'
|
||
|
if item.option == option:
|
||
|
item.process_incoming(command)
|
||
|
known = True
|
||
|
if not known:
|
||
|
# handle unknown options
|
||
|
# only answer to positive requests and deny them
|
||
|
if command == WILL or command == DO:
|
||
|
self.telnet_send_option((DONT if command == WILL else WONT), option)
|
||
|
if self.logger:
|
||
|
self.logger.warning("rejected Telnet option: {!r}".format(option))
|
||
|
|
||
|
def _telnet_process_subnegotiation(self, suboption):
|
||
|
"""Process subnegotiation, the data between IAC SB and IAC SE."""
|
||
|
if suboption[0:1] == COM_PORT_OPTION:
|
||
|
if suboption[1:2] == SERVER_NOTIFY_LINESTATE and len(suboption) >= 3:
|
||
|
self._linestate = ord(suboption[2:3]) # ensure it is a number
|
||
|
if self.logger:
|
||
|
self.logger.info("NOTIFY_LINESTATE: {}".format(self._linestate))
|
||
|
elif suboption[1:2] == SERVER_NOTIFY_MODEMSTATE and len(suboption) >= 3:
|
||
|
self._modemstate = ord(suboption[2:3]) # ensure it is a number
|
||
|
if self.logger:
|
||
|
self.logger.info("NOTIFY_MODEMSTATE: {}".format(self._modemstate))
|
||
|
# update time when we think that a poll would make sense
|
||
|
self._modemstate_timeout.restart(0.3)
|
||
|
elif suboption[1:2] == FLOWCONTROL_SUSPEND:
|
||
|
self._remote_suspend_flow = True
|
||
|
elif suboption[1:2] == FLOWCONTROL_RESUME:
|
||
|
self._remote_suspend_flow = False
|
||
|
else:
|
||
|
for item in self._rfc2217_options.values():
|
||
|
if item.ack_option == suboption[1:2]:
|
||
|
#~ print "processing COM_PORT_OPTION: %r" % list(suboption[1:])
|
||
|
item.check_answer(bytes(suboption[2:]))
|
||
|
break
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.warning("ignoring COM_PORT_OPTION: {!r}".format(suboption))
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.warning("ignoring subnegotiation: {!r}".format(suboption))
|
||
|
|
||
|
# - outgoing telnet commands and options
|
||
|
|
||
|
def _internal_raw_write(self, data):
|
||
|
"""internal socket write with no data escaping. used to send telnet stuff."""
|
||
|
with self._write_lock:
|
||
|
self._socket.sendall(data)
|
||
|
|
||
|
def telnet_send_option(self, action, option):
|
||
|
"""Send DO, DONT, WILL, WONT."""
|
||
|
self._internal_raw_write(IAC + action + option)
|
||
|
|
||
|
def rfc2217_send_subnegotiation(self, option, value=b''):
|
||
|
"""Subnegotiation of RFC2217 parameters."""
|
||
|
value = value.replace(IAC, IAC_DOUBLED)
|
||
|
self._internal_raw_write(IAC + SB + COM_PORT_OPTION + option + value + IAC + SE)
|
||
|
|
||
|
def rfc2217_send_purge(self, value):
|
||
|
"""\
|
||
|
Send purge request to the remote.
|
||
|
(PURGE_RECEIVE_BUFFER / PURGE_TRANSMIT_BUFFER / PURGE_BOTH_BUFFERS)
|
||
|
"""
|
||
|
item = self._rfc2217_options['purge']
|
||
|
item.set(value) # transmit desired purge type
|
||
|
item.wait(self._network_timeout) # wait for acknowledge from the server
|
||
|
|
||
|
def rfc2217_set_control(self, value):
|
||
|
"""transmit change of control line to remote"""
|
||
|
item = self._rfc2217_options['control']
|
||
|
item.set(value) # transmit desired control type
|
||
|
if self._ignore_set_control_answer:
|
||
|
# answers are ignored when option is set. compatibility mode for
|
||
|
# servers that answer, but not the expected one... (or no answer
|
||
|
# at all) i.e. sredird
|
||
|
time.sleep(0.1) # this helps getting the unit tests passed
|
||
|
else:
|
||
|
item.wait(self._network_timeout) # wait for acknowledge from the server
|
||
|
|
||
|
def rfc2217_flow_server_ready(self):
|
||
|
"""\
|
||
|
check if server is ready to receive data. block for some time when
|
||
|
not.
|
||
|
"""
|
||
|
#~ if self._remote_suspend_flow:
|
||
|
#~ wait---
|
||
|
|
||
|
def get_modem_state(self):
|
||
|
"""\
|
||
|
get last modem state (cached value. If value is "old", request a new
|
||
|
one. This cache helps that we don't issue to many requests when e.g. all
|
||
|
status lines, one after the other is queried by the user (CTS, DSR
|
||
|
etc.)
|
||
|
"""
|
||
|
# active modem state polling enabled? is the value fresh enough?
|
||
|
if self._poll_modem_state and self._modemstate_timeout.expired():
|
||
|
if self.logger:
|
||
|
self.logger.debug('polling modem state')
|
||
|
# when it is older, request an update
|
||
|
self.rfc2217_send_subnegotiation(NOTIFY_MODEMSTATE)
|
||
|
timeout = Timeout(self._network_timeout)
|
||
|
while not timeout.expired():
|
||
|
time.sleep(0.05) # prevent 100% CPU load
|
||
|
# when expiration time is updated, it means that there is a new
|
||
|
# value
|
||
|
if not self._modemstate_timeout.expired():
|
||
|
break
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.warning('poll for modem state failed')
|
||
|
# even when there is a timeout, do not generate an error just
|
||
|
# return the last known value. this way we can support buggy
|
||
|
# servers that do not respond to polls, but send automatic
|
||
|
# updates.
|
||
|
if self._modemstate is not None:
|
||
|
if self.logger:
|
||
|
self.logger.debug('using cached modem state')
|
||
|
return self._modemstate
|
||
|
else:
|
||
|
# never received a notification from the server
|
||
|
raise SerialException("remote sends no NOTIFY_MODEMSTATE")
|
||
|
|
||
|
|
||
|
#############################################################################
|
||
|
# The following is code that helps implementing an RFC 2217 server.
|
||
|
|
||
|
class PortManager(object):
|
||
|
"""\
|
||
|
This class manages the state of Telnet and RFC 2217. It needs a serial
|
||
|
instance and a connection to work with. Connection is expected to implement
|
||
|
a (thread safe) write function, that writes the string to the network.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, serial_port, connection, logger=None):
|
||
|
self.serial = serial_port
|
||
|
self.connection = connection
|
||
|
self.logger = logger
|
||
|
self._client_is_rfc2217 = False
|
||
|
|
||
|
# filter state machine
|
||
|
self.mode = M_NORMAL
|
||
|
self.suboption = None
|
||
|
self.telnet_command = None
|
||
|
|
||
|
# states for modem/line control events
|
||
|
self.modemstate_mask = 255
|
||
|
self.last_modemstate = None
|
||
|
self.linstate_mask = 0
|
||
|
|
||
|
# all supported telnet options
|
||
|
self._telnet_options = [
|
||
|
TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED),
|
||
|
TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED),
|
||
|
TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE),
|
||
|
TelnetOption(self, 'we-BINARY', BINARY, WILL, WONT, DO, DONT, INACTIVE),
|
||
|
TelnetOption(self, 'they-BINARY', BINARY, DO, DONT, WILL, WONT, REQUESTED),
|
||
|
TelnetOption(self, 'we-RFC2217', COM_PORT_OPTION, WILL, WONT, DO, DONT, REQUESTED, self._client_ok),
|
||
|
TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, INACTIVE, self._client_ok),
|
||
|
]
|
||
|
|
||
|
# negotiate Telnet/RFC2217 -> send initial requests
|
||
|
if self.logger:
|
||
|
self.logger.debug("requesting initial Telnet/RFC 2217 options")
|
||
|
for option in self._telnet_options:
|
||
|
if option.state is REQUESTED:
|
||
|
self.telnet_send_option(option.send_yes, option.option)
|
||
|
# issue 1st modem state notification
|
||
|
|
||
|
def _client_ok(self):
|
||
|
"""\
|
||
|
callback of telnet option. It gets called when option is activated.
|
||
|
This one here is used to detect when the client agrees on RFC 2217. A
|
||
|
flag is set so that other functions like check_modem_lines know if the
|
||
|
client is OK.
|
||
|
"""
|
||
|
# The callback is used for we and they so if one party agrees, we're
|
||
|
# already happy. it seems not all servers do the negotiation correctly
|
||
|
# and i guess there are incorrect clients too.. so be happy if client
|
||
|
# answers one or the other positively.
|
||
|
self._client_is_rfc2217 = True
|
||
|
if self.logger:
|
||
|
self.logger.info("client accepts RFC 2217")
|
||
|
# this is to ensure that the client gets a notification, even if there
|
||
|
# was no change
|
||
|
self.check_modem_lines(force_notification=True)
|
||
|
|
||
|
# - outgoing telnet commands and options
|
||
|
|
||
|
def telnet_send_option(self, action, option):
|
||
|
"""Send DO, DONT, WILL, WONT."""
|
||
|
self.connection.write(IAC + action + option)
|
||
|
|
||
|
def rfc2217_send_subnegotiation(self, option, value=b''):
|
||
|
"""Subnegotiation of RFC 2217 parameters."""
|
||
|
value = value.replace(IAC, IAC_DOUBLED)
|
||
|
self.connection.write(IAC + SB + COM_PORT_OPTION + option + value + IAC + SE)
|
||
|
|
||
|
# - check modem lines, needs to be called periodically from user to
|
||
|
# establish polling
|
||
|
|
||
|
def check_modem_lines(self, force_notification=False):
|
||
|
"""\
|
||
|
read control lines from serial port and compare the last value sent to remote.
|
||
|
send updates on changes.
|
||
|
"""
|
||
|
modemstate = (
|
||
|
(self.serial.cts and MODEMSTATE_MASK_CTS) |
|
||
|
(self.serial.dsr and MODEMSTATE_MASK_DSR) |
|
||
|
(self.serial.ri and MODEMSTATE_MASK_RI) |
|
||
|
(self.serial.cd and MODEMSTATE_MASK_CD))
|
||
|
# check what has changed
|
||
|
deltas = modemstate ^ (self.last_modemstate or 0) # when last is None -> 0
|
||
|
if deltas & MODEMSTATE_MASK_CTS:
|
||
|
modemstate |= MODEMSTATE_MASK_CTS_CHANGE
|
||
|
if deltas & MODEMSTATE_MASK_DSR:
|
||
|
modemstate |= MODEMSTATE_MASK_DSR_CHANGE
|
||
|
if deltas & MODEMSTATE_MASK_RI:
|
||
|
modemstate |= MODEMSTATE_MASK_RI_CHANGE
|
||
|
if deltas & MODEMSTATE_MASK_CD:
|
||
|
modemstate |= MODEMSTATE_MASK_CD_CHANGE
|
||
|
# if new state is different and the mask allows this change, send
|
||
|
# notification. suppress notifications when client is not rfc2217
|
||
|
if modemstate != self.last_modemstate or force_notification:
|
||
|
if (self._client_is_rfc2217 and (modemstate & self.modemstate_mask)) or force_notification:
|
||
|
self.rfc2217_send_subnegotiation(
|
||
|
SERVER_NOTIFY_MODEMSTATE,
|
||
|
to_bytes([modemstate & self.modemstate_mask]))
|
||
|
if self.logger:
|
||
|
self.logger.info("NOTIFY_MODEMSTATE: {}".format(modemstate))
|
||
|
# save last state, but forget about deltas.
|
||
|
# otherwise it would also notify about changing deltas which is
|
||
|
# probably not very useful
|
||
|
self.last_modemstate = modemstate & 0xf0
|
||
|
|
||
|
# - outgoing data escaping
|
||
|
|
||
|
def escape(self, data):
|
||
|
"""\
|
||
|
This generator function is for the user. All outgoing data has to be
|
||
|
properly escaped, so that no IAC character in the data stream messes up
|
||
|
the Telnet state machine in the server.
|
||
|
|
||
|
socket.sendall(escape(data))
|
||
|
"""
|
||
|
for byte in iterbytes(data):
|
||
|
if byte == IAC:
|
||
|
yield IAC
|
||
|
yield IAC
|
||
|
else:
|
||
|
yield byte
|
||
|
|
||
|
# - incoming data filter
|
||
|
|
||
|
def filter(self, data):
|
||
|
"""\
|
||
|
Handle a bunch of incoming bytes. This is a generator. It will yield
|
||
|
all characters not of interest for Telnet/RFC 2217.
|
||
|
|
||
|
The idea is that the reader thread pushes data from the socket through
|
||
|
this filter:
|
||
|
|
||
|
for byte in filter(socket.recv(1024)):
|
||
|
# do things like CR/LF conversion/whatever
|
||
|
# and write data to the serial port
|
||
|
serial.write(byte)
|
||
|
|
||
|
(socket error handling code left as exercise for the reader)
|
||
|
"""
|
||
|
for byte in iterbytes(data):
|
||
|
if self.mode == M_NORMAL:
|
||
|
# interpret as command or as data
|
||
|
if byte == IAC:
|
||
|
self.mode = M_IAC_SEEN
|
||
|
else:
|
||
|
# store data in sub option buffer or pass it to our
|
||
|
# consumer depending on state
|
||
|
if self.suboption is not None:
|
||
|
self.suboption += byte
|
||
|
else:
|
||
|
yield byte
|
||
|
elif self.mode == M_IAC_SEEN:
|
||
|
if byte == IAC:
|
||
|
# interpret as command doubled -> insert character
|
||
|
# itself
|
||
|
if self.suboption is not None:
|
||
|
self.suboption += byte
|
||
|
else:
|
||
|
yield byte
|
||
|
self.mode = M_NORMAL
|
||
|
elif byte == SB:
|
||
|
# sub option start
|
||
|
self.suboption = bytearray()
|
||
|
self.mode = M_NORMAL
|
||
|
elif byte == SE:
|
||
|
# sub option end -> process it now
|
||
|
self._telnet_process_subnegotiation(bytes(self.suboption))
|
||
|
self.suboption = None
|
||
|
self.mode = M_NORMAL
|
||
|
elif byte in (DO, DONT, WILL, WONT):
|
||
|
# negotiation
|
||
|
self.telnet_command = byte
|
||
|
self.mode = M_NEGOTIATE
|
||
|
else:
|
||
|
# other telnet commands
|
||
|
self._telnet_process_command(byte)
|
||
|
self.mode = M_NORMAL
|
||
|
elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
|
||
|
self._telnet_negotiate_option(self.telnet_command, byte)
|
||
|
self.mode = M_NORMAL
|
||
|
|
||
|
# - incoming telnet commands and options
|
||
|
|
||
|
def _telnet_process_command(self, command):
|
||
|
"""Process commands other than DO, DONT, WILL, WONT."""
|
||
|
# Currently none. RFC2217 only uses negotiation and subnegotiation.
|
||
|
if self.logger:
|
||
|
self.logger.warning("ignoring Telnet command: {!r}".format(command))
|
||
|
|
||
|
def _telnet_negotiate_option(self, command, option):
|
||
|
"""Process incoming DO, DONT, WILL, WONT."""
|
||
|
# check our registered telnet options and forward command to them
|
||
|
# they know themselves if they have to answer or not
|
||
|
known = False
|
||
|
for item in self._telnet_options:
|
||
|
# can have more than one match! as some options are duplicated for
|
||
|
# 'us' and 'them'
|
||
|
if item.option == option:
|
||
|
item.process_incoming(command)
|
||
|
known = True
|
||
|
if not known:
|
||
|
# handle unknown options
|
||
|
# only answer to positive requests and deny them
|
||
|
if command == WILL or command == DO:
|
||
|
self.telnet_send_option((DONT if command == WILL else WONT), option)
|
||
|
if self.logger:
|
||
|
self.logger.warning("rejected Telnet option: {!r}".format(option))
|
||
|
|
||
|
def _telnet_process_subnegotiation(self, suboption):
|
||
|
"""Process subnegotiation, the data between IAC SB and IAC SE."""
|
||
|
if suboption[0:1] == COM_PORT_OPTION:
|
||
|
if self.logger:
|
||
|
self.logger.debug('received COM_PORT_OPTION: {!r}'.format(suboption))
|
||
|
if suboption[1:2] == SET_BAUDRATE:
|
||
|
backup = self.serial.baudrate
|
||
|
try:
|
||
|
(baudrate,) = struct.unpack(b"!I", suboption[2:6])
|
||
|
if baudrate != 0:
|
||
|
self.serial.baudrate = baudrate
|
||
|
except ValueError as e:
|
||
|
if self.logger:
|
||
|
self.logger.error("failed to set baud rate: {}".format(e))
|
||
|
self.serial.baudrate = backup
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.info("{} baud rate: {}".format('set' if baudrate else 'get', self.serial.baudrate))
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_BAUDRATE, struct.pack(b"!I", self.serial.baudrate))
|
||
|
elif suboption[1:2] == SET_DATASIZE:
|
||
|
backup = self.serial.bytesize
|
||
|
try:
|
||
|
(datasize,) = struct.unpack(b"!B", suboption[2:3])
|
||
|
if datasize != 0:
|
||
|
self.serial.bytesize = datasize
|
||
|
except ValueError as e:
|
||
|
if self.logger:
|
||
|
self.logger.error("failed to set data size: {}".format(e))
|
||
|
self.serial.bytesize = backup
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.info("{} data size: {}".format('set' if datasize else 'get', self.serial.bytesize))
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_DATASIZE, struct.pack(b"!B", self.serial.bytesize))
|
||
|
elif suboption[1:2] == SET_PARITY:
|
||
|
backup = self.serial.parity
|
||
|
try:
|
||
|
parity = struct.unpack(b"!B", suboption[2:3])[0]
|
||
|
if parity != 0:
|
||
|
self.serial.parity = RFC2217_REVERSE_PARITY_MAP[parity]
|
||
|
except ValueError as e:
|
||
|
if self.logger:
|
||
|
self.logger.error("failed to set parity: {}".format(e))
|
||
|
self.serial.parity = backup
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.info("{} parity: {}".format('set' if parity else 'get', self.serial.parity))
|
||
|
self.rfc2217_send_subnegotiation(
|
||
|
SERVER_SET_PARITY,
|
||
|
struct.pack(b"!B", RFC2217_PARITY_MAP[self.serial.parity]))
|
||
|
elif suboption[1:2] == SET_STOPSIZE:
|
||
|
backup = self.serial.stopbits
|
||
|
try:
|
||
|
stopbits = struct.unpack(b"!B", suboption[2:3])[0]
|
||
|
if stopbits != 0:
|
||
|
self.serial.stopbits = RFC2217_REVERSE_STOPBIT_MAP[stopbits]
|
||
|
except ValueError as e:
|
||
|
if self.logger:
|
||
|
self.logger.error("failed to set stop bits: {}".format(e))
|
||
|
self.serial.stopbits = backup
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.info("{} stop bits: {}".format('set' if stopbits else 'get', self.serial.stopbits))
|
||
|
self.rfc2217_send_subnegotiation(
|
||
|
SERVER_SET_STOPSIZE,
|
||
|
struct.pack(b"!B", RFC2217_STOPBIT_MAP[self.serial.stopbits]))
|
||
|
elif suboption[1:2] == SET_CONTROL:
|
||
|
if suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING:
|
||
|
if self.serial.xonxoff:
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL)
|
||
|
elif self.serial.rtscts:
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL)
|
||
|
else:
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL)
|
||
|
elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL:
|
||
|
self.serial.xonxoff = False
|
||
|
self.serial.rtscts = False
|
||
|
if self.logger:
|
||
|
self.logger.info("changed flow control to None")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL)
|
||
|
elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTROL:
|
||
|
self.serial.xonxoff = True
|
||
|
if self.logger:
|
||
|
self.logger.info("changed flow control to XON/XOFF")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL)
|
||
|
elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTROL:
|
||
|
self.serial.rtscts = True
|
||
|
if self.logger:
|
||
|
self.logger.info("changed flow control to RTS/CTS")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL)
|
||
|
elif suboption[2:3] == SET_CONTROL_REQ_BREAK_STATE:
|
||
|
if self.logger:
|
||
|
self.logger.warning("requested break state - not implemented")
|
||
|
pass # XXX needs cached value
|
||
|
elif suboption[2:3] == SET_CONTROL_BREAK_ON:
|
||
|
self.serial.break_condition = True
|
||
|
if self.logger:
|
||
|
self.logger.info("changed BREAK to active")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_ON)
|
||
|
elif suboption[2:3] == SET_CONTROL_BREAK_OFF:
|
||
|
self.serial.break_condition = False
|
||
|
if self.logger:
|
||
|
self.logger.info("changed BREAK to inactive")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_OFF)
|
||
|
elif suboption[2:3] == SET_CONTROL_REQ_DTR:
|
||
|
if self.logger:
|
||
|
self.logger.warning("requested DTR state - not implemented")
|
||
|
pass # XXX needs cached value
|
||
|
elif suboption[2:3] == SET_CONTROL_DTR_ON:
|
||
|
self.serial.dtr = True
|
||
|
if self.logger:
|
||
|
self.logger.info("changed DTR to active")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_ON)
|
||
|
elif suboption[2:3] == SET_CONTROL_DTR_OFF:
|
||
|
self.serial.dtr = False
|
||
|
if self.logger:
|
||
|
self.logger.info("changed DTR to inactive")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_OFF)
|
||
|
elif suboption[2:3] == SET_CONTROL_REQ_RTS:
|
||
|
if self.logger:
|
||
|
self.logger.warning("requested RTS state - not implemented")
|
||
|
pass # XXX needs cached value
|
||
|
#~ self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON)
|
||
|
elif suboption[2:3] == SET_CONTROL_RTS_ON:
|
||
|
self.serial.rts = True
|
||
|
if self.logger:
|
||
|
self.logger.info("changed RTS to active")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON)
|
||
|
elif suboption[2:3] == SET_CONTROL_RTS_OFF:
|
||
|
self.serial.rts = False
|
||
|
if self.logger:
|
||
|
self.logger.info("changed RTS to inactive")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_OFF)
|
||
|
#~ elif suboption[2:3] == SET_CONTROL_REQ_FLOW_SETTING_IN:
|
||
|
#~ elif suboption[2:3] == SET_CONTROL_USE_NO_FLOW_CONTROL_IN:
|
||
|
#~ elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTOL_IN:
|
||
|
#~ elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTOL_IN:
|
||
|
#~ elif suboption[2:3] == SET_CONTROL_USE_DCD_FLOW_CONTROL:
|
||
|
#~ elif suboption[2:3] == SET_CONTROL_USE_DTR_FLOW_CONTROL:
|
||
|
#~ elif suboption[2:3] == SET_CONTROL_USE_DSR_FLOW_CONTROL:
|
||
|
elif suboption[1:2] == NOTIFY_LINESTATE:
|
||
|
# client polls for current state
|
||
|
self.rfc2217_send_subnegotiation(
|
||
|
SERVER_NOTIFY_LINESTATE,
|
||
|
to_bytes([0])) # sorry, nothing like that implemented
|
||
|
elif suboption[1:2] == NOTIFY_MODEMSTATE:
|
||
|
if self.logger:
|
||
|
self.logger.info("request for modem state")
|
||
|
# client polls for current state
|
||
|
self.check_modem_lines(force_notification=True)
|
||
|
elif suboption[1:2] == FLOWCONTROL_SUSPEND:
|
||
|
if self.logger:
|
||
|
self.logger.info("suspend")
|
||
|
self._remote_suspend_flow = True
|
||
|
elif suboption[1:2] == FLOWCONTROL_RESUME:
|
||
|
if self.logger:
|
||
|
self.logger.info("resume")
|
||
|
self._remote_suspend_flow = False
|
||
|
elif suboption[1:2] == SET_LINESTATE_MASK:
|
||
|
self.linstate_mask = ord(suboption[2:3]) # ensure it is a number
|
||
|
if self.logger:
|
||
|
self.logger.info("line state mask: 0x{:02x}".format(self.linstate_mask))
|
||
|
elif suboption[1:2] == SET_MODEMSTATE_MASK:
|
||
|
self.modemstate_mask = ord(suboption[2:3]) # ensure it is a number
|
||
|
if self.logger:
|
||
|
self.logger.info("modem state mask: 0x{:02x}".format(self.modemstate_mask))
|
||
|
elif suboption[1:2] == PURGE_DATA:
|
||
|
if suboption[2:3] == PURGE_RECEIVE_BUFFER:
|
||
|
self.serial.reset_input_buffer()
|
||
|
if self.logger:
|
||
|
self.logger.info("purge in")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_RECEIVE_BUFFER)
|
||
|
elif suboption[2:3] == PURGE_TRANSMIT_BUFFER:
|
||
|
self.serial.reset_output_buffer()
|
||
|
if self.logger:
|
||
|
self.logger.info("purge out")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_TRANSMIT_BUFFER)
|
||
|
elif suboption[2:3] == PURGE_BOTH_BUFFERS:
|
||
|
self.serial.reset_input_buffer()
|
||
|
self.serial.reset_output_buffer()
|
||
|
if self.logger:
|
||
|
self.logger.info("purge both")
|
||
|
self.rfc2217_send_subnegotiation(SERVER_PURGE_DATA, PURGE_BOTH_BUFFERS)
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.error("undefined PURGE_DATA: {!r}".format(list(suboption[2:])))
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.error("undefined COM_PORT_OPTION: {!r}".format(list(suboption[1:])))
|
||
|
else:
|
||
|
if self.logger:
|
||
|
self.logger.warning("unknown subnegotiation: {!r}".format(suboption))
|
||
|
|
||
|
|
||
|
# simple client test
|
||
|
if __name__ == '__main__':
|
||
|
import sys
|
||
|
s = Serial('rfc2217://localhost:7000', 115200)
|
||
|
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()
|