blob: aaacdb23f842018a53b310223f945e123fdce70c [file] [log] [blame]
cliechti576de252002-02-28 23:54:44 +00001#!/usr/bin/env python
Chris Liechtifbdd8a02015-08-09 02:37:45 +02002#
cliechtia128a702004-07-21 22:13:31 +00003# Very simple serial terminal
Chris Liechtifbdd8a02015-08-09 02:37:45 +02004#
Chris Liechti3e02f702015-12-16 23:06:04 +01005# This file is part of pySerial. https://github.com/pyserial/pyserial
Chris Liechti68340d72015-08-03 14:15:48 +02006# (C)2002-2015 Chris Liechti <cliechti@gmx.net>
Chris Liechtifbdd8a02015-08-09 02:37:45 +02007#
8# SPDX-License-Identifier: BSD-3-Clause
cliechtifc9eb382002-03-05 01:12:29 +00009
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020010import codecs
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020011import os
12import sys
13import threading
cliechti576de252002-02-28 23:54:44 +000014
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020015import serial
Chris Liechti55ba7d92015-08-15 16:33:51 +020016from serial.tools.list_ports import comports
Chris Liechti168704f2015-09-30 16:50:29 +020017from serial.tools import hexlify_codec
18
Chris Liechtia887c932016-02-13 23:10:14 +010019# pylint: disable=wrong-import-order,wrong-import-position
20
Chris Liechti168704f2015-09-30 16:50:29 +020021codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None)
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020022
Chris Liechti68340d72015-08-03 14:15:48 +020023try:
24 raw_input
25except NameError:
Chris Liechtia887c932016-02-13 23:10:14 +010026 # pylint: disable=redefined-builtin,invalid-name
Chris Liechti68340d72015-08-03 14:15:48 +020027 raw_input = input # in python3 it's "raw"
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020028 unichr = chr
Chris Liechti68340d72015-08-03 14:15:48 +020029
cliechti6c8eb2f2009-07-08 02:10:46 +000030
31def key_description(character):
32 """generate a readable description for a key"""
33 ascii_code = ord(character)
34 if ascii_code < 32:
Chris Liechtic8f3f822016-06-08 03:35:28 +020035 return 'Ctrl+{:c}'.format(ord('@') + ascii_code)
cliechti6c8eb2f2009-07-08 02:10:46 +000036 else:
37 return repr(character)
38
cliechti91165532011-03-18 02:02:52 +000039
Chris Liechti9a720852015-08-25 00:20:38 +020040# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020041class ConsoleBase(object):
Chris Liechti397cf412016-02-11 00:11:48 +010042 """OS abstraction for console (input/output codec, no echo)"""
43
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020044 def __init__(self):
45 if sys.version_info >= (3, 0):
46 self.byte_output = sys.stdout.buffer
47 else:
48 self.byte_output = sys.stdout
49 self.output = sys.stdout
cliechtif467aa82013-10-13 21:36:49 +000050
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020051 def setup(self):
Chris Liechti397cf412016-02-11 00:11:48 +010052 """Set console to read single characters, no echo"""
cliechtif467aa82013-10-13 21:36:49 +000053
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020054 def cleanup(self):
Chris Liechti397cf412016-02-11 00:11:48 +010055 """Restore default console settings"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020056
57 def getkey(self):
Chris Liechti397cf412016-02-11 00:11:48 +010058 """Read a single key from the console"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020059 return None
60
Chris Liechtia887c932016-02-13 23:10:14 +010061 def write_bytes(self, byte_string):
Chris Liechti397cf412016-02-11 00:11:48 +010062 """Write bytes (already encoded)"""
Chris Liechtia887c932016-02-13 23:10:14 +010063 self.byte_output.write(byte_string)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020064 self.byte_output.flush()
65
Chris Liechtia887c932016-02-13 23:10:14 +010066 def write(self, text):
Chris Liechti397cf412016-02-11 00:11:48 +010067 """Write string"""
Chris Liechtia887c932016-02-13 23:10:14 +010068 self.output.write(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020069 self.output.flush()
70
Chris Liechti1eb3f6b2016-04-27 02:12:50 +020071 def cancel(self):
72 """Cancel getkey operation"""
73
Chris Liechti269f77b2015-08-24 01:31:42 +020074 # - - - - - - - - - - - - - - - - - - - - - - - -
75 # context manager:
76 # switch terminal temporary to normal mode (e.g. to get user input)
77
78 def __enter__(self):
79 self.cleanup()
80 return self
81
82 def __exit__(self, *args, **kwargs):
83 self.setup()
84
cliechti9c592b32008-06-16 22:00:14 +000085
Chris Liechtiba45c522016-02-06 23:53:23 +010086if os.name == 'nt': # noqa
cliechti576de252002-02-28 23:54:44 +000087 import msvcrt
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020088 import ctypes
Chris Liechti9cc696b2015-08-28 00:54:22 +020089
90 class Out(object):
Chris Liechtia887c932016-02-13 23:10:14 +010091 """file-like wrapper that uses os.write"""
92
Chris Liechti9cc696b2015-08-28 00:54:22 +020093 def __init__(self, fd):
94 self.fd = fd
95
96 def flush(self):
97 pass
98
99 def write(self, s):
100 os.write(self.fd, s)
101
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200102 class Console(ConsoleBase):
Chris Liechticbb00b22015-08-13 22:58:49 +0200103 def __init__(self):
104 super(Console, self).__init__()
Chris Liechti1df28272015-08-27 23:37:38 +0200105 self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
106 self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
Chris Liechticbb00b22015-08-13 22:58:49 +0200107 ctypes.windll.kernel32.SetConsoleOutputCP(65001)
108 ctypes.windll.kernel32.SetConsoleCP(65001)
Chris Liechti9cc696b2015-08-28 00:54:22 +0200109 self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
110 # the change of the code page is not propagated to Python, manually fix it
111 sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
112 sys.stdout = self.output
Chris Liechti168704f2015-09-30 16:50:29 +0200113 self.output.encoding = 'UTF-8' # needed for input
Chris Liechticbb00b22015-08-13 22:58:49 +0200114
Chris Liechti1df28272015-08-27 23:37:38 +0200115 def __del__(self):
116 ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
117 ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
118
cliechti3a8bf092008-09-17 11:26:53 +0000119 def getkey(self):
cliechti91165532011-03-18 02:02:52 +0000120 while True:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200121 z = msvcrt.getwch()
Chris Liechti9f398812015-09-13 18:50:44 +0200122 if z == unichr(13):
123 return unichr(10)
124 elif z in (unichr(0), unichr(0x0e)): # functions keys, ignore
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200125 msvcrt.getwch()
cliechti9c592b32008-06-16 22:00:14 +0000126 else:
cliechti9c592b32008-06-16 22:00:14 +0000127 return z
cliechti53edb472009-02-06 21:18:46 +0000128
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200129 def cancel(self):
Chris Liechtic20c3732016-05-14 02:25:13 +0200130 # CancelIo, CancelSynchronousIo do not seem to work when using
131 # getwch, so instead, send a key to the window with the console
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200132 hwnd = ctypes.windll.kernel32.GetConsoleWindow()
133 ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
134
cliechti576de252002-02-28 23:54:44 +0000135elif os.name == 'posix':
Chris Liechtia1d5c6d2015-08-07 14:41:24 +0200136 import atexit
137 import termios
Chris Liechticab3dab2016-12-07 01:27:41 +0100138 import fcntl
Chris Liechti9cc696b2015-08-28 00:54:22 +0200139
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200140 class Console(ConsoleBase):
cliechti9c592b32008-06-16 22:00:14 +0000141 def __init__(self):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200142 super(Console, self).__init__()
cliechti9c592b32008-06-16 22:00:14 +0000143 self.fd = sys.stdin.fileno()
Chris Liechti4d989c22015-08-24 00:24:49 +0200144 self.old = termios.tcgetattr(self.fd)
Chris Liechti89eb2472015-08-08 17:06:25 +0200145 atexit.register(self.cleanup)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200146 if sys.version_info < (3, 0):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200147 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
148 else:
149 self.enc_stdin = sys.stdin
cliechti9c592b32008-06-16 22:00:14 +0000150
151 def setup(self):
cliechti9c592b32008-06-16 22:00:14 +0000152 new = termios.tcgetattr(self.fd)
153 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
154 new[6][termios.VMIN] = 1
155 new[6][termios.VTIME] = 0
156 termios.tcsetattr(self.fd, termios.TCSANOW, new)
cliechti53edb472009-02-06 21:18:46 +0000157
cliechti9c592b32008-06-16 22:00:14 +0000158 def getkey(self):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200159 c = self.enc_stdin.read(1)
Chris Liechti9f398812015-09-13 18:50:44 +0200160 if c == unichr(0x7f):
161 c = unichr(8) # map the BS key (which yields DEL) to backspace
Chris Liechti9a720852015-08-25 00:20:38 +0200162 return c
cliechti53edb472009-02-06 21:18:46 +0000163
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200164 def cancel(self):
Chris Liechticab3dab2016-12-07 01:27:41 +0100165 fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200166
cliechti9c592b32008-06-16 22:00:14 +0000167 def cleanup(self):
Chris Liechti4d989c22015-08-24 00:24:49 +0200168 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
cliechti9c592b32008-06-16 22:00:14 +0000169
cliechti576de252002-02-28 23:54:44 +0000170else:
Chris Liechti397cf412016-02-11 00:11:48 +0100171 raise NotImplementedError(
172 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
cliechti576de252002-02-28 23:54:44 +0000173
cliechti6fa76fb2009-07-08 23:53:39 +0000174
Chris Liechti9a720852015-08-25 00:20:38 +0200175# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200176
177class Transform(object):
Chris Liechticbb00b22015-08-13 22:58:49 +0200178 """do-nothing: forward all data unchanged"""
Chris Liechtid698af72015-08-24 20:24:55 +0200179 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200180 """text received from serial port"""
181 return text
182
Chris Liechtid698af72015-08-24 20:24:55 +0200183 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200184 """text to be sent to serial port"""
185 return text
186
187 def echo(self, text):
188 """text to be sent but displayed on console"""
189 return text
190
Chris Liechti442bf512015-08-15 01:42:24 +0200191
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200192class CRLF(Transform):
193 """ENTER sends CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200194
Chris Liechtid698af72015-08-24 20:24:55 +0200195 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200196 return text.replace('\n', '\r\n')
197
Chris Liechti442bf512015-08-15 01:42:24 +0200198
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200199class CR(Transform):
200 """ENTER sends CR"""
Chris Liechtid698af72015-08-24 20:24:55 +0200201
202 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200203 return text.replace('\r', '\n')
204
Chris Liechtid698af72015-08-24 20:24:55 +0200205 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200206 return text.replace('\n', '\r')
207
Chris Liechti442bf512015-08-15 01:42:24 +0200208
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200209class LF(Transform):
210 """ENTER sends LF"""
211
212
213class NoTerminal(Transform):
214 """remove typical terminal control codes from input"""
Chris Liechti9a720852015-08-25 00:20:38 +0200215
216 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t')
Chris Liechtiba45c522016-02-06 23:53:23 +0100217 REPLACEMENT_MAP.update(
218 {
Chris Liechti033f17c2015-08-30 21:28:04 +0200219 0x7F: 0x2421, # DEL
220 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100221 })
Chris Liechti9a720852015-08-25 00:20:38 +0200222
Chris Liechtid698af72015-08-24 20:24:55 +0200223 def rx(self, text):
Chris Liechti9a720852015-08-25 00:20:38 +0200224 return text.translate(self.REPLACEMENT_MAP)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200225
Chris Liechtid698af72015-08-24 20:24:55 +0200226 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200227
228
Chris Liechti9a720852015-08-25 00:20:38 +0200229class NoControls(NoTerminal):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200230 """Remove all control codes, incl. CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200231
Chris Liechti9a720852015-08-25 00:20:38 +0200232 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
Chris Liechtiba45c522016-02-06 23:53:23 +0100233 REPLACEMENT_MAP.update(
234 {
Chris Liechtia887c932016-02-13 23:10:14 +0100235 0x20: 0x2423, # visual space
Chris Liechti033f17c2015-08-30 21:28:04 +0200236 0x7F: 0x2421, # DEL
237 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100238 })
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200239
240
241class Printable(Transform):
Chris Liechtid698af72015-08-24 20:24:55 +0200242 """Show decimal code for all non-ASCII characters and replace most control codes"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200243
Chris Liechtid698af72015-08-24 20:24:55 +0200244 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200245 r = []
Chris Liechtia887c932016-02-13 23:10:14 +0100246 for c in text:
247 if ' ' <= c < '\x7f' or c in '\r\n\b\t':
248 r.append(c)
249 elif c < ' ':
250 r.append(unichr(0x2400 + ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200251 else:
Chris Liechtia887c932016-02-13 23:10:14 +0100252 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200253 r.append(' ')
254 return ''.join(r)
255
Chris Liechtid698af72015-08-24 20:24:55 +0200256 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200257
258
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200259class Colorize(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200260 """Apply different colors for received and echo"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200261
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200262 def __init__(self):
263 # XXX make it configurable, use colorama?
264 self.input_color = '\x1b[37m'
265 self.echo_color = '\x1b[31m'
266
Chris Liechtid698af72015-08-24 20:24:55 +0200267 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200268 return self.input_color + text
269
270 def echo(self, text):
271 return self.echo_color + text
272
Chris Liechti442bf512015-08-15 01:42:24 +0200273
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200274class DebugIO(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200275 """Print what is sent and received"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200276
Chris Liechtid698af72015-08-24 20:24:55 +0200277 def rx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200278 sys.stderr.write(' [RX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200279 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200280 return text
281
Chris Liechtid698af72015-08-24 20:24:55 +0200282 def tx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200283 sys.stderr.write(' [TX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200284 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200285 return text
286
Chris Liechti442bf512015-08-15 01:42:24 +0200287
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200288# other ideas:
289# - add date/time for each newline
290# - insert newline after: a) timeout b) packet end character
291
Chris Liechtib3df13e2015-08-25 02:20:09 +0200292EOL_TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100293 'crlf': CRLF,
294 'cr': CR,
295 'lf': LF,
296}
Chris Liechtib3df13e2015-08-25 02:20:09 +0200297
298TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100299 'direct': Transform, # no transformation
300 'default': NoTerminal,
301 'nocontrol': NoControls,
302 'printable': Printable,
303 'colorize': Colorize,
304 'debug': DebugIO,
305}
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200306
307
Chris Liechti033f17c2015-08-30 21:28:04 +0200308# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti89313c92015-09-01 02:33:13 +0200309def ask_for_port():
310 """\
311 Show a list of ports and ask the user for a choice. To make selection
312 easier on systems with long device names, also allow the input of an
313 index.
314 """
315 sys.stderr.write('\n--- Available ports:\n')
316 ports = []
317 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
Chris Liechti8b0eaf22017-07-19 22:59:57 +0200318 sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
Chris Liechti89313c92015-09-01 02:33:13 +0200319 ports.append(port)
320 while True:
321 port = raw_input('--- Enter port index or full name: ')
322 try:
323 index = int(port) - 1
324 if not 0 <= index < len(ports):
325 sys.stderr.write('--- Invalid index!\n')
326 continue
327 except ValueError:
328 pass
329 else:
330 port = ports[index]
331 return port
cliechti1351dde2012-04-12 16:47:47 +0000332
333
cliechti8c2ea842011-03-18 01:51:46 +0000334class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200335 """\
336 Terminal application. Copy data from serial port to console and vice versa.
337 Handle special keys from the console to show menu etc.
338 """
339
Chris Liechti3b454802015-08-26 23:39:59 +0200340 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200341 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200342 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000343 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200344 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200345 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200346 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200347 self.eol = eol
348 self.filters = filters
349 self.update_transformations()
Carlos52bfe352018-03-16 13:27:19 +0000350 self.exit_character = unichr(0x1d) # GS/CTRL+]
351 self.menu_character = unichr(0x14) # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100352 self.alive = None
353 self._reader_alive = None
354 self.receiver_thread = None
355 self.rx_decoder = None
356 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000357
cliechti8c2ea842011-03-18 01:51:46 +0000358 def _start_reader(self):
359 """Start reader thread"""
360 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000361 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200362 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
363 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000364 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000365
366 def _stop_reader(self):
367 """Stop reader thread only, wait for clean exit of thread"""
368 self._reader_alive = False
Chris Liechti933a5172016-05-04 16:12:15 +0200369 if hasattr(self.serial, 'cancel_read'):
370 self.serial.cancel_read()
cliechti8c2ea842011-03-18 01:51:46 +0000371 self.receiver_thread.join()
372
cliechti8c2ea842011-03-18 01:51:46 +0000373 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100374 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000375 self.alive = True
376 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000377 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200378 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
379 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000380 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200381 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000382
cliechti6385f2c2005-09-21 19:51:19 +0000383 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100384 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000385 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000386
cliechtibf6bb7d2006-03-30 00:28:18 +0000387 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100388 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000389 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000390 if not transmit_only:
Chris Liechti933a5172016-05-04 16:12:15 +0200391 if hasattr(self.serial, 'cancel_read'):
392 self.serial.cancel_read()
cliechtibf6bb7d2006-03-30 00:28:18 +0000393 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000394
Chris Liechti933a5172016-05-04 16:12:15 +0200395 def close(self):
396 self.serial.close()
397
Chris Liechtib3df13e2015-08-25 02:20:09 +0200398 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100399 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100400 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
401 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200402 self.tx_transformations = [t() for t in transformations]
403 self.rx_transformations = list(reversed(self.tx_transformations))
404
Chris Liechtid698af72015-08-24 20:24:55 +0200405 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100406 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200407 self.input_encoding = encoding
408 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
409
410 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100411 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200412 self.output_encoding = encoding
413 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
414
cliechti6c8eb2f2009-07-08 02:10:46 +0000415 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100416 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200417 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100418 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200419 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100420 ('active' if self.serial.rts else 'inactive'),
421 ('active' if self.serial.dtr else 'inactive'),
422 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000423 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200424 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100425 ('active' if self.serial.cts else 'inactive'),
426 ('active' if self.serial.dsr else 'inactive'),
427 ('active' if self.serial.ri else 'inactive'),
428 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000429 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200430 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000431 # yet received. ignore this error.
432 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200433 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
434 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200435 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
436 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200437 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
438 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000439
cliechti6385f2c2005-09-21 19:51:19 +0000440 def reader(self):
441 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000442 try:
cliechti8c2ea842011-03-18 01:51:46 +0000443 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200444 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200445 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200446 if data:
447 if self.raw:
448 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000449 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200450 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200451 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200452 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200453 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200454 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000455 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200456 self.console.cancel()
457 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000458
cliechti6385f2c2005-09-21 19:51:19 +0000459 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000460 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200461 Loop and copy console->serial until self.exit_character character is
462 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000463 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000464 """
465 menu_active = False
466 try:
467 while self.alive:
468 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200469 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000470 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200471 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200472 if not self.alive:
473 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000474 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200475 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000476 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200477 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200478 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200479 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200480 self.stop() # exit app
481 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000482 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200483 #~ if self.raw:
484 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200485 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200486 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200487 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000488 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200489 echo_text = c
490 for transformation in self.tx_transformations:
491 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200492 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000493 except:
494 self.alive = False
495 raise
cliechti6385f2c2005-09-21 19:51:19 +0000496
Chris Liechti7af7c752015-08-12 15:45:19 +0200497 def handle_menu_key(self, c):
498 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200499 if c == self.menu_character or c == self.exit_character:
500 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200501 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200502 if self.echo:
503 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200504 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti45c6f222017-07-17 23:56:24 +0200505 self.upload_file()
Chris Liechti7af7c752015-08-12 15:45:19 +0200506 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200507 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200508 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200509 self.serial.rts = not self.serial.rts
510 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200511 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200512 self.serial.dtr = not self.serial.dtr
513 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200514 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200515 self.serial.break_condition = not self.serial.break_condition
516 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200517 elif c == '\x05': # CTRL+E -> toggle local echo
518 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200519 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200520 elif c == '\x06': # CTRL+F -> edit filters
Chris Liechti45c6f222017-07-17 23:56:24 +0200521 self.change_filter()
Chris Liechtib3df13e2015-08-25 02:20:09 +0200522 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti033f17c2015-08-30 21:28:04 +0200523 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200524 eol = modes.index(self.eol) + 1
525 if eol >= len(modes):
526 eol = 0
527 self.eol = modes[eol]
528 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
529 self.update_transformations()
530 elif c == '\x01': # CTRL+A -> set encoding
Chris Liechti45c6f222017-07-17 23:56:24 +0200531 self.change_encoding()
Chris Liechti7af7c752015-08-12 15:45:19 +0200532 elif c == '\x09': # CTRL+I -> info
533 self.dump_port_settings()
534 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
535 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
536 elif c in 'pP': # P -> change port
Chris Liechti45c6f222017-07-17 23:56:24 +0200537 self.change_port()
Chris Liechtia73b96b2017-07-13 23:32:24 +0200538 elif c in 'sS': # S -> suspend / open port temporarily
Chris Liechti45c6f222017-07-17 23:56:24 +0200539 self.suspend_port()
Chris Liechti7af7c752015-08-12 15:45:19 +0200540 elif c in 'bB': # B -> change baudrate
Chris Liechti45c6f222017-07-17 23:56:24 +0200541 self.change_baudrate()
Chris Liechti7af7c752015-08-12 15:45:19 +0200542 elif c == '8': # 8 -> change to 8 bits
543 self.serial.bytesize = serial.EIGHTBITS
544 self.dump_port_settings()
545 elif c == '7': # 7 -> change to 8 bits
546 self.serial.bytesize = serial.SEVENBITS
547 self.dump_port_settings()
548 elif c in 'eE': # E -> change to even parity
549 self.serial.parity = serial.PARITY_EVEN
550 self.dump_port_settings()
551 elif c in 'oO': # O -> change to odd parity
552 self.serial.parity = serial.PARITY_ODD
553 self.dump_port_settings()
554 elif c in 'mM': # M -> change to mark parity
555 self.serial.parity = serial.PARITY_MARK
556 self.dump_port_settings()
557 elif c in 'sS': # S -> change to space parity
558 self.serial.parity = serial.PARITY_SPACE
559 self.dump_port_settings()
560 elif c in 'nN': # N -> change to no parity
561 self.serial.parity = serial.PARITY_NONE
562 self.dump_port_settings()
563 elif c == '1': # 1 -> change to 1 stop bits
564 self.serial.stopbits = serial.STOPBITS_ONE
565 self.dump_port_settings()
566 elif c == '2': # 2 -> change to 2 stop bits
567 self.serial.stopbits = serial.STOPBITS_TWO
568 self.dump_port_settings()
569 elif c == '3': # 3 -> change to 1.5 stop bits
570 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
571 self.dump_port_settings()
572 elif c in 'xX': # X -> change software flow control
573 self.serial.xonxoff = (c == 'X')
574 self.dump_port_settings()
575 elif c in 'rR': # R -> change hardware flow control
576 self.serial.rtscts = (c == 'R')
577 self.dump_port_settings()
578 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200579 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
580
Chris Liechti45c6f222017-07-17 23:56:24 +0200581 def upload_file(self):
582 """Ask user for filenname and send its contents"""
583 sys.stderr.write('\n--- File to upload: ')
584 sys.stderr.flush()
585 with self.console:
586 filename = sys.stdin.readline().rstrip('\r\n')
587 if filename:
588 try:
589 with open(filename, 'rb') as f:
590 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
591 while True:
592 block = f.read(1024)
593 if not block:
594 break
595 self.serial.write(block)
596 # Wait for output buffer to drain.
597 self.serial.flush()
598 sys.stderr.write('.') # Progress indicator.
599 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
600 except IOError as e:
601 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
602
603 def change_filter(self):
604 """change the i/o transformations"""
605 sys.stderr.write('\n--- Available Filters:\n')
606 sys.stderr.write('\n'.join(
607 '--- {:<10} = {.__doc__}'.format(k, v)
608 for k, v in sorted(TRANSFORMATIONS.items())))
609 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
610 with self.console:
611 new_filters = sys.stdin.readline().lower().split()
612 if new_filters:
613 for f in new_filters:
614 if f not in TRANSFORMATIONS:
Chris Liechtifac1c132017-08-27 23:35:55 +0200615 sys.stderr.write('--- unknown filter: {!r}\n'.format(f))
Chris Liechti45c6f222017-07-17 23:56:24 +0200616 break
617 else:
618 self.filters = new_filters
619 self.update_transformations()
620 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
621
622 def change_encoding(self):
623 """change encoding on the serial port"""
624 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
625 with self.console:
626 new_encoding = sys.stdin.readline().strip()
627 if new_encoding:
628 try:
629 codecs.lookup(new_encoding)
630 except LookupError:
631 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
632 else:
633 self.set_rx_encoding(new_encoding)
634 self.set_tx_encoding(new_encoding)
635 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
636 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
637
638 def change_baudrate(self):
639 """change the baudrate"""
640 sys.stderr.write('\n--- Baudrate: ')
641 sys.stderr.flush()
642 with self.console:
643 backup = self.serial.baudrate
644 try:
645 self.serial.baudrate = int(sys.stdin.readline().strip())
646 except ValueError as e:
647 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
648 self.serial.baudrate = backup
649 else:
650 self.dump_port_settings()
651
652 def change_port(self):
653 """Have a conversation with the user to change the serial port"""
654 with self.console:
655 try:
656 port = ask_for_port()
657 except KeyboardInterrupt:
658 port = None
659 if port and port != self.serial.port:
660 # reader thread needs to be shut down
661 self._stop_reader()
662 # save settings
663 settings = self.serial.getSettingsDict()
664 try:
665 new_serial = serial.serial_for_url(port, do_not_open=True)
666 # restore settings and open
667 new_serial.applySettingsDict(settings)
668 new_serial.rts = self.serial.rts
669 new_serial.dtr = self.serial.dtr
670 new_serial.open()
671 new_serial.break_condition = self.serial.break_condition
672 except Exception as e:
673 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
674 new_serial.close()
675 else:
676 self.serial.close()
677 self.serial = new_serial
678 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
679 # and restart the reader thread
680 self._start_reader()
681
682 def suspend_port(self):
683 """\
684 open port temporarily, allow reconnect, exit and port change to get
685 out of the loop
686 """
687 # reader thread needs to be shut down
688 self._stop_reader()
689 self.serial.close()
690 sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
691 do_change_port = False
692 while not self.serial.is_open:
693 sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
694 exit=key_description(self.exit_character)))
695 k = self.console.getkey()
696 if k == self.exit_character:
697 self.stop() # exit app
698 break
699 elif k in 'pP':
700 do_change_port = True
701 break
702 try:
703 self.serial.open()
704 except Exception as e:
705 sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
706 if do_change_port:
707 self.change_port()
708 else:
709 # and restart the reader thread
710 self._start_reader()
711 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
712
Chris Liechti442bf512015-08-15 01:42:24 +0200713 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100714 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200715 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200716 return """
717--- pySerial ({version}) - miniterm - help
718---
719--- {exit:8} Exit program
720--- {menu:8} Menu escape key, followed by:
721--- Menu keys:
722--- {menu:7} Send the menu character itself to remote
723--- {exit:7} Send the exit character itself to remote
724--- {info:7} Show info
725--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200726--- {repr:7} encoding
727--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200728--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200729--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
730--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200731---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200732--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200733--- p change port
734--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200735--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200736--- 1 2 3 set stop bits (1, 2, 1.5)
737--- b change baud rate
738--- x X disable/enable software flow control
739--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100740""".format(version=getattr(serial, 'VERSION', 'unknown version'),
741 exit=key_description(self.exit_character),
742 menu=key_description(self.menu_character),
743 rts=key_description('\x12'),
744 dtr=key_description('\x04'),
745 brk=key_description('\x02'),
746 echo=key_description('\x05'),
747 info=key_description('\x09'),
748 upload=key_description('\x15'),
749 repr=key_description('\x01'),
750 filter=key_description('\x06'),
751 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200752
753
Chris Liechtib3df13e2015-08-25 02:20:09 +0200754# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200755# default args can be used to override when calling main() from an other script
756# e.g to create a miniterm-my-device.py
757def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100758 """Command line tool, entry point"""
759
Chris Liechtib7550bd2015-08-15 04:09:10 +0200760 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000761
Chris Liechtib7550bd2015-08-15 04:09:10 +0200762 parser = argparse.ArgumentParser(
Chris Liechti397cf412016-02-11 00:11:48 +0100763 description="Miniterm - A simple terminal program for the serial port.")
cliechti6385f2c2005-09-21 19:51:19 +0000764
Chris Liechti033f17c2015-08-30 21:28:04 +0200765 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100766 "port",
767 nargs='?',
768 help="serial port name ('-' to show port list)",
769 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000770
Chris Liechti033f17c2015-08-30 21:28:04 +0200771 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100772 "baudrate",
773 nargs='?',
774 type=int,
775 help="set baud rate, default: %(default)s",
776 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000777
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200778 group = parser.add_argument_group("port settings")
cliechti53edb472009-02-06 21:18:46 +0000779
Chris Liechti033f17c2015-08-30 21:28:04 +0200780 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100781 "--parity",
782 choices=['N', 'E', 'O', 'S', 'M'],
783 type=lambda c: c.upper(),
784 help="set parity, one of {N E O S M}, default: N",
785 default='N')
cliechti53edb472009-02-06 21:18:46 +0000786
Chris Liechti033f17c2015-08-30 21:28:04 +0200787 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100788 "--rtscts",
789 action="store_true",
790 help="enable RTS/CTS flow control (default off)",
791 default=False)
cliechti53edb472009-02-06 21:18:46 +0000792
Chris Liechti033f17c2015-08-30 21:28:04 +0200793 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100794 "--xonxoff",
795 action="store_true",
796 help="enable software flow control (default off)",
797 default=False)
cliechti53edb472009-02-06 21:18:46 +0000798
Chris Liechti033f17c2015-08-30 21:28:04 +0200799 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100800 "--rts",
801 type=int,
802 help="set initial RTS line state (possible values: 0, 1)",
803 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000804
Chris Liechti033f17c2015-08-30 21:28:04 +0200805 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100806 "--dtr",
807 type=int,
808 help="set initial DTR line state (possible values: 0, 1)",
809 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000810
Chris Liechti00f84282015-12-24 23:40:34 +0100811 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100812 "--ask",
813 action="store_true",
814 help="ask again for port when open fails",
815 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100816
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200817 group = parser.add_argument_group("data handling")
cliechti5370cee2013-10-13 03:08:19 +0000818
Chris Liechti033f17c2015-08-30 21:28:04 +0200819 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100820 "-e", "--echo",
821 action="store_true",
822 help="enable local echo (default off)",
823 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000824
Chris Liechti033f17c2015-08-30 21:28:04 +0200825 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100826 "--encoding",
827 dest="serial_port_encoding",
828 metavar="CODEC",
829 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
830 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000831
Chris Liechti033f17c2015-08-30 21:28:04 +0200832 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100833 "-f", "--filter",
834 action="append",
835 metavar="NAME",
836 help="add text transformation",
837 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200838
Chris Liechti033f17c2015-08-30 21:28:04 +0200839 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100840 "--eol",
841 choices=['CR', 'LF', 'CRLF'],
842 type=lambda c: c.upper(),
843 help="end of line mode",
844 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000845
Chris Liechti033f17c2015-08-30 21:28:04 +0200846 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100847 "--raw",
848 action="store_true",
849 help="Do no apply any encodings/transformations",
850 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000851
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200852 group = parser.add_argument_group("hotkeys")
cliechtib7d746d2006-03-28 22:44:30 +0000853
Chris Liechti033f17c2015-08-30 21:28:04 +0200854 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100855 "--exit-char",
856 type=int,
857 metavar='NUM',
858 help="Unicode of special character that is used to exit the application, default: %(default)s",
859 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000860
Chris Liechti033f17c2015-08-30 21:28:04 +0200861 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100862 "--menu-char",
863 type=int,
864 metavar='NUM',
865 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
866 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000867
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200868 group = parser.add_argument_group("diagnostics")
cliechti6385f2c2005-09-21 19:51:19 +0000869
Chris Liechti033f17c2015-08-30 21:28:04 +0200870 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100871 "-q", "--quiet",
872 action="store_true",
873 help="suppress non-error messages",
874 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000875
Chris Liechti033f17c2015-08-30 21:28:04 +0200876 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100877 "--develop",
878 action="store_true",
879 help="show Python traceback on error",
880 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000881
Chris Liechtib7550bd2015-08-15 04:09:10 +0200882 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000883
Chris Liechtib7550bd2015-08-15 04:09:10 +0200884 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000885 parser.error('--exit-char can not be the same as --menu-char')
886
Chris Liechtib3df13e2015-08-25 02:20:09 +0200887 if args.filter:
888 if 'help' in args.filter:
889 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200890 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100891 '{:<10} = {.__doc__}'.format(k, v)
892 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200893 sys.stderr.write('\n')
894 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200895 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200896 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200897 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200898
Chris Liechti00f84282015-12-24 23:40:34 +0100899 while True:
900 # no port given on command line -> ask user now
901 if args.port is None or args.port == '-':
902 try:
903 args.port = ask_for_port()
904 except KeyboardInterrupt:
905 sys.stderr.write('\n')
906 parser.error('user aborted and port is not given')
907 else:
908 if not args.port:
909 parser.error('port is not given')
910 try:
911 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100912 args.port,
913 args.baudrate,
914 parity=args.parity,
915 rtscts=args.rtscts,
916 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100917 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200918
Chris Liechtif542fca2016-05-13 00:20:14 +0200919 if not hasattr(serial_instance, 'cancel_read'):
920 # enable timeout for alive flag polling if cancel_read is not available
921 serial_instance.timeout = 1
922
Chris Liechti00f84282015-12-24 23:40:34 +0100923 if args.dtr is not None:
924 if not args.quiet:
925 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
926 serial_instance.dtr = args.dtr
927 if args.rts is not None:
928 if not args.quiet:
929 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
930 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200931
Chris Liechti00f84282015-12-24 23:40:34 +0100932 serial_instance.open()
933 except serial.SerialException as e:
Chris Liechtifac1c132017-08-27 23:35:55 +0200934 sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e))
Chris Liechti00f84282015-12-24 23:40:34 +0100935 if args.develop:
936 raise
937 if not args.ask:
938 sys.exit(1)
939 else:
940 args.port = '-'
941 else:
942 break
cliechti6385f2c2005-09-21 19:51:19 +0000943
Chris Liechti3b454802015-08-26 23:39:59 +0200944 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100945 serial_instance,
946 echo=args.echo,
947 eol=args.eol.lower(),
948 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200949 miniterm.exit_character = unichr(args.exit_char)
950 miniterm.menu_character = unichr(args.menu_char)
951 miniterm.raw = args.raw
952 miniterm.set_rx_encoding(args.serial_port_encoding)
953 miniterm.set_tx_encoding(args.serial_port_encoding)
954
Chris Liechtib7550bd2015-08-15 04:09:10 +0200955 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200956 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100957 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200958 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100959 key_description(miniterm.exit_character),
960 key_description(miniterm.menu_character),
961 key_description(miniterm.menu_character),
962 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +0000963
cliechti6385f2c2005-09-21 19:51:19 +0000964 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +0000965 try:
966 miniterm.join(True)
967 except KeyboardInterrupt:
968 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +0200969 if not args.quiet:
cliechtibf6bb7d2006-03-30 00:28:18 +0000970 sys.stderr.write("\n--- exit ---\n")
cliechti6385f2c2005-09-21 19:51:19 +0000971 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +0200972 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +0000973
cliechti5370cee2013-10-13 03:08:19 +0000974# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +0000975if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +0000976 main()