Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
9838c8136f | |||
7c536a2d6b | |||
8075e9fe34 | |||
44f431db13 | |||
598345b792 | |||
0df36bb543 | |||
87a956e72c | |||
cfd9bec445 | |||
6756a87def | |||
ff441d2b22 | |||
b765576283 | |||
6c5b85b618 | |||
b029c79e63 | |||
3839c00406 | |||
090959a958 | |||
e1a57cfc35 | |||
bd4a7642a9 |
13
TODO.md
Normal file
13
TODO.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
- serial.write() may block for reasons beyond our control. For
|
||||||
|
example, CDC-ACM devices may do flow control internally and block on
|
||||||
|
USB transfers; the CDC-ACM interface does not expose flow control
|
||||||
|
settings to the PC, and writes will hang indefinitely even if
|
||||||
|
--flow is not used. Since polling the keyboard is in the write loop,
|
||||||
|
this kills us.
|
||||||
|
|
||||||
|
- Support some other escape character so ^C can be optionally used.
|
||||||
|
Maybe even remove ^C as an option, use ^] exclusively?
|
||||||
|
|
||||||
|
- Support reconnection
|
||||||
|
|
||||||
|
- See patches from "Guyzmo"
|
225
terminal.py
225
terminal.py
|
@ -21,17 +21,22 @@ import threading
|
||||||
import traceback
|
import traceback
|
||||||
import time
|
import time
|
||||||
import signal
|
import signal
|
||||||
|
import fcntl
|
||||||
|
import string
|
||||||
|
import re
|
||||||
|
|
||||||
# Need OS-specific method for getting keyboard input.
|
# Need OS-specific method for getting keyboard input.
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
import msvcrt
|
import msvcrt
|
||||||
class Console:
|
class Console:
|
||||||
def __init__(self):
|
def __init__(self, bufsize = 1):
|
||||||
pass
|
# Buffer size > 1 not supported on Windows
|
||||||
|
self.tty = True
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
pass
|
pass
|
||||||
def getkey(self):
|
def getkey(self):
|
||||||
while 1:
|
start = time.time()
|
||||||
|
while True:
|
||||||
z = msvcrt.getch()
|
z = msvcrt.getch()
|
||||||
if z == '\0' or z == '\xe0': # function keys
|
if z == '\0' or z == '\xe0': # function keys
|
||||||
msvcrt.getch()
|
msvcrt.getch()
|
||||||
|
@ -39,37 +44,58 @@ if os.name == 'nt':
|
||||||
if z == '\r':
|
if z == '\r':
|
||||||
return '\n'
|
return '\n'
|
||||||
return z
|
return z
|
||||||
|
if (time.time() - start) > 0.1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class MySerial(serial.Serial):
|
||||||
|
def nonblocking_read(self, size=1):
|
||||||
|
# Buffer size > 1 not supported on Windows
|
||||||
|
return self.read(1)
|
||||||
elif os.name == 'posix':
|
elif os.name == 'posix':
|
||||||
import termios, select
|
import termios, select, errno
|
||||||
class Console:
|
class Console:
|
||||||
def __init__(self):
|
def __init__(self, bufsize = 65536):
|
||||||
|
self.bufsize = bufsize
|
||||||
self.fd = sys.stdin.fileno()
|
self.fd = sys.stdin.fileno()
|
||||||
try:
|
if os.isatty(self.fd):
|
||||||
|
self.tty = True
|
||||||
self.old = termios.tcgetattr(self.fd)
|
self.old = termios.tcgetattr(self.fd)
|
||||||
tc = termios.tcgetattr(self.fd)
|
tc = termios.tcgetattr(self.fd)
|
||||||
tc[3] = tc[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
|
tc[3] = tc[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
|
||||||
tc[6][termios.VMIN] = 1
|
tc[6][termios.VMIN] = 1
|
||||||
tc[6][termios.VTIME] = 0
|
tc[6][termios.VTIME] = 0
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, tc)
|
termios.tcsetattr(self.fd, termios.TCSANOW, tc)
|
||||||
except termios.error:
|
else:
|
||||||
# ignore errors, so we can pipe stuff to this script
|
self.tty = False
|
||||||
pass
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
try:
|
if self.tty:
|
||||||
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
|
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
|
||||||
except:
|
|
||||||
# ignore errors, so we can pipe stuff to this script
|
|
||||||
pass
|
|
||||||
def getkey(self):
|
def getkey(self):
|
||||||
# Return -1 if we don't get input in 0.1 seconds, so that
|
# Return -1 if we don't get input in 0.1 seconds, so that
|
||||||
# the main code can check the "alive" flag and respond to SIGINT.
|
# the main code can check the "alive" flag and respond to SIGINT.
|
||||||
[r, w, x] = select.select([self.fd], [], [self.fd], 0.1)
|
[r, w, x] = select.select([self.fd], [], [self.fd], 0.1)
|
||||||
if r:
|
if r:
|
||||||
return os.read(self.fd, 1)
|
return os.read(self.fd, self.bufsize)
|
||||||
elif x:
|
elif x:
|
||||||
return ''
|
return ''
|
||||||
else:
|
else:
|
||||||
return -1
|
return None
|
||||||
|
|
||||||
|
class MySerial(serial.Serial):
|
||||||
|
def nonblocking_read(self, size=1):
|
||||||
|
[r, w, x] = select.select([self.fd], [], [self.fd], self._timeout)
|
||||||
|
if r:
|
||||||
|
try:
|
||||||
|
return os.read(self.fd, size)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.EAGAIN:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
elif x:
|
||||||
|
raise SerialException("exception (device disconnected?)")
|
||||||
|
else:
|
||||||
|
return None # timeout
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ("Sorry, no terminal implementation for your platform (%s) "
|
raise ("Sorry, no terminal implementation for your platform (%s) "
|
||||||
"available." % sys.platform)
|
"available." % sys.platform)
|
||||||
|
@ -104,33 +130,44 @@ class Jimterm:
|
||||||
suppress_read_firstnull = True,
|
suppress_read_firstnull = True,
|
||||||
transmit_all = False,
|
transmit_all = False,
|
||||||
add_cr = False,
|
add_cr = False,
|
||||||
raw = False,
|
send_cr = False,
|
||||||
color = True):
|
raw = True,
|
||||||
|
color = True,
|
||||||
|
bufsize = 65536):
|
||||||
|
|
||||||
self.color = JimtermColor()
|
self.color = JimtermColor()
|
||||||
if color:
|
if color:
|
||||||
self.color.setup(len(serials))
|
self.color.setup(len(serials))
|
||||||
|
|
||||||
self.serials = serials
|
self.serials = serials
|
||||||
self.suppress_write_bytes = suppress_write_bytes or ""
|
self.suppress_write_bytes = suppress_write_bytes
|
||||||
self.suppress_read_firstnull = suppress_read_firstnull
|
self.suppress_read_firstnull = suppress_read_firstnull
|
||||||
self.last_color = ""
|
self.last_color = ""
|
||||||
self.threads = []
|
self.threads = []
|
||||||
self.transmit_all = transmit_all
|
self.transmit_all = transmit_all
|
||||||
self.add_cr = add_cr
|
self.add_cr = add_cr
|
||||||
|
self.send_cr = send_cr
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
|
self.bufsize = bufsize
|
||||||
|
self.quote_re = None
|
||||||
|
self.output_lock = threading.Lock()
|
||||||
|
|
||||||
def print_header(self, nodes, bauds):
|
def print_header(self, nodes, bauds, output = sys.stdout):
|
||||||
for (n, (node, baud)) in enumerate(zip(nodes, bauds)):
|
for (n, (node, baud)) in enumerate(zip(nodes, bauds)):
|
||||||
print (self.color.code(n)
|
output.write(self.color.code(n)
|
||||||
+ node + ", " + str(baud) + " baud"
|
+ node + ", " + str(baud) + " baud"
|
||||||
+ self.color.reset)
|
+ self.color.reset + "\n")
|
||||||
print "^C to exit"
|
if sys.stdin.isatty():
|
||||||
print "----------"
|
output.write("^C to exit\n")
|
||||||
|
output.write("----------\n")
|
||||||
|
output.flush()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.alive = True
|
self.alive = True
|
||||||
|
|
||||||
|
# Set up console
|
||||||
|
self.console = Console(self.bufsize)
|
||||||
|
|
||||||
# serial->console, all devices
|
# serial->console, all devices
|
||||||
for (n, serial) in enumerate(self.serials):
|
for (n, serial) in enumerate(self.serials):
|
||||||
self.threads.append(threading.Thread(
|
self.threads.append(threading.Thread(
|
||||||
|
@ -139,7 +176,6 @@ class Jimterm:
|
||||||
))
|
))
|
||||||
|
|
||||||
# console->serial
|
# console->serial
|
||||||
self.console = Console()
|
|
||||||
self.threads.append(threading.Thread(target = self.writer))
|
self.threads.append(threading.Thread(target = self.writer))
|
||||||
|
|
||||||
# start all threads
|
# start all threads
|
||||||
|
@ -155,91 +191,128 @@ class Jimterm:
|
||||||
while thread.isAlive():
|
while thread.isAlive():
|
||||||
thread.join(0.1)
|
thread.join(0.1)
|
||||||
|
|
||||||
|
def quote_raw(self, data):
|
||||||
|
if self.quote_re is None:
|
||||||
|
matcher = '[^%s]' % re.escape(string.printable + "\b")
|
||||||
|
if sys.version_info < (3,):
|
||||||
|
self.quote_re = re.compile(matcher)
|
||||||
|
qf = lambda x: ("\\x%02x" % ord(x.group(0)))
|
||||||
|
else:
|
||||||
|
self.quote_re = re.compile(matcher.encode('ascii'))
|
||||||
|
qf = lambda x: ("\\x%02x" % ord(x.group(0))).encode('ascii')
|
||||||
|
self.quote_func = qf
|
||||||
|
return self.quote_re.sub(self.quote_func, data)
|
||||||
|
|
||||||
def reader(self, serial, color):
|
def reader(self, serial, color):
|
||||||
"""loop and copy serial->console"""
|
"""loop and copy serial->console"""
|
||||||
first = True
|
first = True
|
||||||
try:
|
try:
|
||||||
|
if (sys.version_info < (3,)):
|
||||||
|
null = '\x00'
|
||||||
|
else:
|
||||||
|
null = b'\x00'
|
||||||
while self.alive:
|
while self.alive:
|
||||||
data = serial.read(1)
|
data = serial.nonblocking_read(self.bufsize)
|
||||||
if not data:
|
if data is None:
|
||||||
continue
|
continue
|
||||||
|
if not len(data):
|
||||||
|
raise Exception("read returned EOF")
|
||||||
|
|
||||||
# don't print a NULL if it's the first character we
|
# don't print a NULL if it's the first character we
|
||||||
# read. This hides startup/port-opening glitches with
|
# read. This hides startup/port-opening glitches with
|
||||||
# some serial devices.
|
# some serial devices.
|
||||||
if self.suppress_read_firstnull and first and data == '\0':
|
if self.suppress_read_firstnull and first and data[0] == null:
|
||||||
first = False
|
first = False
|
||||||
continue
|
data = data[1:]
|
||||||
first = False
|
first = False
|
||||||
|
|
||||||
|
self.output_lock.acquire()
|
||||||
|
|
||||||
if color != self.last_color:
|
if color != self.last_color:
|
||||||
self.last_color = color
|
self.last_color = color
|
||||||
sys.stdout.write(color)
|
os.write(sys.stdout.fileno(), color)
|
||||||
|
|
||||||
if (self.raw or
|
if self.add_cr:
|
||||||
(ord(data) >= 32 and ord(data) < 128) or
|
if sys.version_info < (3,):
|
||||||
data == '\r' or data == '\n' or data == '\t'):
|
data = data.replace('\n', '\r\n')
|
||||||
if self.add_cr and data == '\n':
|
|
||||||
sys.stdout.write('\r' + data)
|
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(data)
|
data = data.replace(b'\n', b'\r\n')
|
||||||
else:
|
|
||||||
sys.stdout.write('\\x'+("0"+hex(ord(data))[2:])[-2:])
|
if not self.raw:
|
||||||
|
data = self.quote_raw(data)
|
||||||
|
|
||||||
|
os.write(sys.stdout.fileno(), data)
|
||||||
|
|
||||||
|
self.output_lock.release()
|
||||||
|
|
||||||
sys.stdout.flush()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.console.cleanup()
|
||||||
sys.stdout.write(color)
|
sys.stdout.write(color)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.stdout.write(self.color.reset)
|
sys.stdout.write(self.color.reset)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
self.console.cleanup()
|
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
def writer(self):
|
def writer(self):
|
||||||
"""loop and copy console->serial until ^C"""
|
"""loop and copy console->serial until ^C"""
|
||||||
try:
|
try:
|
||||||
|
if (sys.version_info < (3,)):
|
||||||
|
ctrlc = '\x03'
|
||||||
|
else:
|
||||||
|
ctrlc = b'\x03'
|
||||||
while self.alive:
|
while self.alive:
|
||||||
try:
|
try:
|
||||||
c = self.console.getkey()
|
c = self.console.getkey()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
c = '\x03'
|
|
||||||
if c == '\x03':
|
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
elif c == -1:
|
if c is None:
|
||||||
# No input, try again
|
# No input, try again.
|
||||||
continue
|
continue
|
||||||
|
elif self.console.tty and ctrlc in c:
|
||||||
|
# Try to catch ^C that didn't trigger KeyboardInterrupt
|
||||||
|
self.stop()
|
||||||
|
return
|
||||||
elif c == '':
|
elif c == '':
|
||||||
# EOF on input. Wait a tiny bit so we can
|
# Probably EOF on input. Wait a tiny bit so we can
|
||||||
# flush the remaining input, then stop.
|
# flush the remaining input, then stop.
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
self.stop()
|
self.stop()
|
||||||
return
|
return
|
||||||
elif c in self.suppress_write_bytes:
|
|
||||||
# Don't send these bytes
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
# send character
|
# Remove bytes we don't want to send
|
||||||
|
if self.suppress_write_bytes is not None:
|
||||||
|
c = c.translate(None, self.suppress_write_bytes)
|
||||||
|
|
||||||
|
if self.send_cr and c == '\n':
|
||||||
|
c = '\r'
|
||||||
|
# Send character
|
||||||
if self.transmit_all:
|
if self.transmit_all:
|
||||||
for serial in self.serials:
|
for serial in self.serials:
|
||||||
serial.write(c)
|
serial.write(c)
|
||||||
else:
|
else:
|
||||||
self.serials[0].write(c)
|
self.serials[0].write(c)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.console.cleanup()
|
||||||
sys.stdout.write(self.color.reset)
|
sys.stdout.write(self.color.reset)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.console.cleanup()
|
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# Set all serial port timeouts to 0.1 sec
|
# Set all serial port timeouts to 0.1 sec
|
||||||
saved_timeouts = []
|
saved_timeouts = []
|
||||||
for (n, serial) in enumerate(self.serials):
|
for serial in self.serials:
|
||||||
saved_timeouts.append(serial.timeout)
|
saved_timeouts.append(serial.timeout)
|
||||||
serial.timeout = 0.1
|
serial.timeout = 0.1
|
||||||
|
|
||||||
|
# Work around https://sourceforge.net/p/pyserial/bugs/151/
|
||||||
|
saved_writeTimeouts = []
|
||||||
|
for serial in self.serials:
|
||||||
|
saved_writeTimeouts.append(serial.writeTimeout)
|
||||||
|
serial.writeTimeout = 1000000
|
||||||
|
|
||||||
# Handle SIGINT gracefully
|
# Handle SIGINT gracefully
|
||||||
signal.signal(signal.SIGINT, lambda *args: self.stop())
|
signal.signal(signal.SIGINT, lambda *args: self.stop())
|
||||||
|
|
||||||
|
@ -248,11 +321,13 @@ class Jimterm:
|
||||||
self.join()
|
self.join()
|
||||||
|
|
||||||
# Restore serial port timeouts
|
# Restore serial port timeouts
|
||||||
for (n, serial) in enumerate(self.serials):
|
for (serial, saved) in zip(self.serials, saved_timeouts):
|
||||||
serial.timeout = saved_timeouts[n]
|
serial.timeout = saved
|
||||||
|
for (serial, saved) in zip(self.serials, saved_writeTimeouts):
|
||||||
|
serial.writeTimeout = saved
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
print self.color.reset # and a newline
|
sys.stdout.write(self.color.reset + "\n")
|
||||||
self.console.cleanup()
|
self.console.cleanup()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -272,23 +347,42 @@ if __name__ == "__main__":
|
||||||
"per-device baudrates.")
|
"per-device baudrates.")
|
||||||
|
|
||||||
parser.add_argument("--quiet", "-q", action="store_true",
|
parser.add_argument("--quiet", "-q", action="store_true",
|
||||||
help="Less verbose output (omit header)")
|
help="Don't print header")
|
||||||
|
|
||||||
parser.add_argument("--baudrate", "-b", metavar="BAUD", type=int,
|
parser.add_argument("--baudrate", "-b", metavar="BAUD", type=int,
|
||||||
help="Default baudrate for all devices", default=115200)
|
help="Default baudrate for all devices", default=115200)
|
||||||
parser.add_argument("--crlf", "-c", action="store_true",
|
parser.add_argument("--crlf", "-c", action="store_true",
|
||||||
help="Add CR before incoming LF")
|
help="Add CR before incoming LF")
|
||||||
|
parser.add_argument("--lfcr", "-C", action="store_true",
|
||||||
|
help="Send CR instead of LF on output")
|
||||||
parser.add_argument("--all", "-a", action="store_true",
|
parser.add_argument("--all", "-a", action="store_true",
|
||||||
help="Send keystrokes to all devices, not just "
|
help="Send keystrokes to all devices, not just "
|
||||||
"the first one")
|
"the first one")
|
||||||
parser.add_argument("--mono", "-m", action="store_true",
|
parser.add_argument("--mono", "-m", action="store_true",
|
||||||
help="Don't use colors in output")
|
help="Don't use colors in output")
|
||||||
parser.add_argument("--raw", "-r", action="store_true",
|
|
||||||
help="Don't escape unprintable characters")
|
|
||||||
parser.add_argument("--flow", "-f", action="store_true",
|
parser.add_argument("--flow", "-f", action="store_true",
|
||||||
help="Enable RTS/CTS flow control")
|
help="Enable RTS/CTS flow control")
|
||||||
|
parser.add_argument("--esp", "-e", action="store_true",
|
||||||
|
help="Force RTS and DTR high, for ESP boards. Note that"
|
||||||
|
" the lines may still glitch low at startup.")
|
||||||
|
parser.add_argument("--bufsize", "-z", metavar="SIZE", type=int,
|
||||||
|
help="Buffer size for reads and writes", default=65536)
|
||||||
|
|
||||||
|
group = parser.add_mutually_exclusive_group(required = False)
|
||||||
|
group.add_argument("--raw", "-r", action="store_true",
|
||||||
|
default=argparse.SUPPRESS,
|
||||||
|
help="Output characters directly "
|
||||||
|
"(default, if stdout is not a tty)")
|
||||||
|
group.add_argument("--no-raw", "-R", action="store_true",
|
||||||
|
default=argparse.SUPPRESS,
|
||||||
|
help="Quote unprintable characters "
|
||||||
|
"(default, if stdout is a tty)")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
piped = not sys.stdout.isatty()
|
||||||
|
raw = "raw" in args or (piped and "no_raw" not in args)
|
||||||
|
|
||||||
devs = []
|
devs = []
|
||||||
nodes = []
|
nodes = []
|
||||||
bauds = []
|
bauds = []
|
||||||
|
@ -304,7 +398,13 @@ if __name__ == "__main__":
|
||||||
sys.stderr.write("error: %s specified more than once\n" % node)
|
sys.stderr.write("error: %s specified more than once\n" % node)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
try:
|
try:
|
||||||
dev = serial.Serial(node, baud, rtscts = args.flow)
|
dev = MySerial(None, baud, rtscts = args.flow)
|
||||||
|
dev.port = node
|
||||||
|
if args.esp:
|
||||||
|
# Force DTR and RTS high by setting to false
|
||||||
|
dev.dtr = False
|
||||||
|
dev.rts = False
|
||||||
|
dev.open()
|
||||||
except serial.serialutil.SerialException:
|
except serial.serialutil.SerialException:
|
||||||
sys.stderr.write("error opening %s\n" % node)
|
sys.stderr.write("error opening %s\n" % node)
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
|
@ -315,8 +415,11 @@ if __name__ == "__main__":
|
||||||
term = Jimterm(devs,
|
term = Jimterm(devs,
|
||||||
transmit_all = args.all,
|
transmit_all = args.all,
|
||||||
add_cr = args.crlf,
|
add_cr = args.crlf,
|
||||||
raw = args.raw,
|
send_cr = args.lfcr,
|
||||||
color = (os.name == "posix" and not args.mono))
|
raw = raw,
|
||||||
|
color = (os.name == "posix" and not args.mono),
|
||||||
|
bufsize = args.bufsize)
|
||||||
if not args.quiet:
|
if not args.quiet:
|
||||||
term.print_header(nodes, bauds)
|
term.print_header(nodes, bauds, sys.stderr)
|
||||||
|
|
||||||
term.run()
|
term.run()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user