add a class managing the server side of RFC2217
diff --git a/pyserial/serial/rfc2217.py b/pyserial/serial/rfc2217.py
index df01174..e4a4e8d 100644
--- a/pyserial/serial/rfc2217.py
+++ b/pyserial/serial/rfc2217.py
@@ -195,20 +195,21 @@
PARITY_MARK: 4,
PARITY_SPACE: 5,
}
+RFC2217_REVERSE_PARITY_MAP = dict((v,k) for k,v in RFC2217_PARITY_MAP.items())
RFC2217_STOPBIT_MAP = {
STOPBITS_ONE: 1,
STOPBITS_ONE_POINT_FIVE: 3,
STOPBITS_TWO: 2,
}
+RFC2217_REVERSE_STOPBIT_MAP = dict((v,k) for k,v in RFC2217_STOPBIT_MAP.items())
-TELNET_ACTION_SUCCESS_MAP = {
- DO: (DO, WILL),
- WILL: (DO, WILL),
- DONT: (DONT, WONT),
- WONT: (DONT, WONT),
-}
+# Telnet filter states
+M_NORMAL = 0
+M_IAC_SEEN = 1
+M_NEGOTIATE = 2
+# TelnetOption and TelnetSubnegotiation states
REQUESTED = 'REQUESTED'
ACTIVE = 'ACTIVE'
INACTIVE = 'INACTIVE'
@@ -625,9 +626,6 @@
def _telnetReadLoop(self):
"""read loop for the socket"""
- M_NORMAL = 0
- M_IAC_SEEN = 1
- M_NEGOTIATE = 2
mode = M_NORMAL
suboption = None
try:
@@ -793,8 +791,289 @@
class Serial(RFC2217Serial, io.RawIOBase):
pass
+# ###
+# The following is code that helps implementing an RFC2217 server.
-# simple test
+class RFC2217Manager(object):
+ """This class manages the state of Telnet and RFC2217. It needs a serial
+ instance and a connection to work with. connection is expected to implement
+ a thread safe write function"""
+
+ def __init__(self, serial_port, connection, debug_output=False):
+ self.serial = serial_port
+ self.connection = connection
+ self.debug_output = debug_output
+
+ # 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),
+ TelnetOption(self, 'they-RFC2217', COM_PORT_OPTION, DO, DONT, WILL, WONT, INACTIVE),
+ ]
+
+ # negotiate Telnet/RFC2217 -> send initial requests
+ for option in self._telnet_options:
+ if option.state is REQUESTED:
+ self.telnetSendOption(option.send_yes, option.option)
+ # issue 1st modem state notification
+ # XXX should wait until negotiation is finished
+ self.check_modem_lines()
+
+ # - outgoing telnet commands and options
+
+ def telnetSendOption(self, action, option):
+ """Send DO, DONT, WILL, WONT"""
+ self.connection.write(to_bytes([IAC, action, option]))
+
+ def rfc2217SendSubnegotiation(self, option, value=[]):
+ """Subnegotiation of RFC2217 parameters"""
+ self.connection.write(to_bytes([IAC, SB, COM_PORT_OPTION, option] + list(value) + [IAC, SE]))
+
+ # - check modem lines, needs to be called periodically from user to
+ # establish polling
+
+ def check_modem_lines(self):
+ modemstate = (
+ (self.serial.getCTS() and MODEMSTATE_MASK_CTS) |
+ (self.serial.getDSR() and MODEMSTATE_MASK_DSR) |
+ (self.serial.getRI() and MODEMSTATE_MASK_RI) |
+ (self.serial.getCD() and MODEMSTATE_MASK_CD)
+ )
+ # XXX also assemble delta bits
+ modemstate &= self.modemstate_mask
+ if modemstate != self.last_modemstate:
+ self.rfc2217SendSubnegotiation(
+ SERVER_NOTIFY_MODEMSTATE,
+ to_bytes([modemstate])
+ )
+ self.last_modemstate = modemstate
+ if self.debug_output:
+ print "NOTIFY_MODEMSTATE: %s" % (modemstate,)
+
+ # - 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/RFC2217.
+
+ 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 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.append(byte)
+ else:
+ yield byte
+ elif self.mode == M_IAC_SEEN:
+ if byte == IAC:
+ # interpret as command doubled -> insert character
+ # itself
+ self._read_buffer.put(IAC)
+ 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._telnetProcessSubnegotiation(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._telnetProcessCommand(byte)
+ self.mode = M_NORMAL
+ elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following
+ self._telnetNegotiateOption(self.telnet_command, byte)
+ self.mode = M_NORMAL
+
+ # - incoming telnet commands and options
+
+ def _telnetProcessCommand(self, command):
+ """Process commands other than DO, DONT, WILL, WONT"""
+ # Currently none. RFC2217 only uses negotiation and subnegotiation.
+ #~ print "_telnetProcessCommand %r" % ord(command)
+
+ def _telnetNegotiateOption(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.telnetSendOption((command == WILL and DONT or WONT), option)
+
+
+ def _telnetProcessSubnegotiation(self, suboption):
+ """Process subnegotiation, the data between IAC SB and IAC SE"""
+ if suboption[0:1] == COM_PORT_OPTION:
+ if suboption[1:2] == SET_BAUDRATE:
+ backup = self.serial.baudrate
+ try:
+ (self.serial.baudrate,) = struct.unpack("!I", suboption[2:6])
+ except ValueError, e:
+ if self.debug_output:
+ print "failed to set baudrate: %s" % (e,)
+ self.serial.baudrate = backup
+ self.rfc2217SendSubnegotiation(SERVER_SET_BAUDRATE, struct.pack("!I", self.serial.baudrate))
+ elif suboption[1:2] == SET_DATASIZE:
+ backup = self.serial.bytesize
+ try:
+ (self.serial.bytesize,) = struct.unpack("!B", suboption[2:3])
+ except ValueError, e:
+ if self.debug_output:
+ print "failed to set datasize: %s" % (e,)
+ self.serial.bytesize = backup
+ self.rfc2217SendSubnegotiation(SERVER_SET_DATASIZE, struct.pack("!B", self.serial.bytesize))
+ elif suboption[1:2] == SET_PARITY:
+ backup = self.serial.parity
+ try:
+ self.serial.parity = RFC2217_REVERSE_PARITY_MAP[struct.unpack("!B", suboption[2:3])[0]]
+ except ValueError, e:
+ if self.debug_output:
+ print "failed to set parity: %s" % (e,)
+ self.serial.parity = backup
+ self.rfc2217SendSubnegotiation(
+ SERVER_SET_PARITY,
+ struct.pack("!B", RFC2217_PARITY_MAP[self.serial.parity])
+ )
+ elif suboption[1:2] == SET_STOPSIZE:
+ backup = self.serial.stopbits
+ try:
+ self.serial.stopbits = RFC2217_REVERSE_STOPBIT_MAP[struct.unpack("!B", suboption[2:3])[0]]
+ except ValueError, e:
+ if self.debug_output:
+ print "failed to set stopsize: %s" % (e,)
+ self.serial.stopbits = backup
+ self.rfc2217SendSubnegotiation(
+ SERVER_SET_STOPSIZE,
+ struct.pack("!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.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL)
+ elif self.serial.rtscts:
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL)
+ else:
+ self.rfc2217SendSubnegotiation(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
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_NO_FLOW_CONTROL)
+ elif suboption[2:3] == SET_CONTROL_USE_SW_FLOW_CONTROL:
+ self.serial.xonxoff = True
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_SW_FLOW_CONTROL)
+ elif suboption[2:3] == SET_CONTROL_USE_HW_FLOW_CONTROL:
+ self.serial.rtscts = True
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_USE_HW_FLOW_CONTROL)
+ elif suboption[2:3] == SET_CONTROL_REQ_BREAK_STATE:
+ pass # XXX needs cached value
+ elif suboption[2:3] == SET_CONTROL_BREAK_ON:
+ self.serial.setBreak(True)
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_ON)
+ elif suboption[2:3] == SET_CONTROL_BREAK_OFF:
+ self.serial.setBreak(False)
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_BREAK_OFF)
+ elif suboption[2:3] == SET_CONTROL_REQ_DTR:
+ pass # XXX needs cached value
+ elif suboption[2:3] == SET_CONTROL_DTR_ON:
+ self.serial.setDTR(True)
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_ON)
+ elif suboption[2:3] == SET_CONTROL_DTR_OFF:
+ self.serial.setDTR(False)
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_DTR_OFF)
+ elif suboption[2:3] == SET_CONTROL_REQ_RTS:
+ pass # XXX needs cached value
+ #~ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON)
+ elif suboption[2:3] == SET_CONTROL_RTS_ON:
+ self.serial.setRTS(True)
+ self.rfc2217SendSubnegotiation(SERVER_SET_CONTROL, SET_CONTROL_RTS_ON)
+ elif suboption[2:3] == SET_CONTROL_RTS_OFF:
+ self.serial.setRTS(False)
+ self.rfc2217SendSubnegotiation(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:
+ pass
+ elif suboption[1:2] == NOTIFY_MODEMSTATE:
+ pass
+ elif suboption[1:2] == FLOWCONTROL_SUSPEND:
+ self._remote_suspend_flow = True
+ elif suboption[1:2] == FLOWCONTROL_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
+ elif suboption[1:2] == SET_MODEMSTATE_MASK:
+ self.modemstate_mask = ord(suboption[2:3]) # ensure it is a number
+ elif suboption[1:2] == PURGE_DATA:
+ if suboption[2:3] == PURGE_RECEIVE_BUFFER:
+ self.serial.flushInput()
+ self.rfc2217SendSubnegotiation(SERVER_PURGE_DATA, PURGE_RECEIVE_BUFFER)
+ elif suboption[2:3] == PURGE_TRANSMIT_BUFFER:
+ self.serial.flushOutput()
+ self.rfc2217SendSubnegotiation(SERVER_PURGE_DATA, PURGE_TRANSMIT_BUFFER)
+ elif suboption[2:3] == PURGE_BOTH_BUFFERS:
+ self.serial.flushInput()
+ self.serial.flushOutput()
+ self.rfc2217SendSubnegotiation(SERVER_PURGE_DATA, PURGE_BOTH_BUFFERS)
+ else:
+ if self.debug_output:
+ print "undefined PURGE_DATA: %r" % list(suboption[2:])
+ else:
+ if self.debug_output:
+ print "undefined COM_PORT_OPTION: %r" % list(suboption[1:])
+ else:
+ pass
+ #~ print "_telnetProcessSubnegotiation unknown %r" % suboption
+
+
+# simple client test
if __name__ == '__main__':
import sys
s = Serial('rfc2217://localhost:7000', 115200)