blob: 634264ea9dfc10c55e08f249d548691c0d86a5a5 [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 Liechtie1384382015-08-15 17:06:05 +0200278 sys.stderr.write(' [RX:{}] '.format(repr(text)))
279 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 Liechtie1384382015-08-15 17:06:05 +0200283 sys.stderr.write(' [TX:{}] '.format(repr(text)))
284 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 Liechti89313c92015-09-01 02:33:13 +0200318 sys.stderr.write('--- {:2}: {:20} {}\n'.format(n, port, desc))
319 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()
Chris Liechti442bf512015-08-15 01:42:24 +0200350 self.exit_character = 0x1d # GS/CTRL+]
351 self.menu_character = 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 Liechti7af7c752015-08-12 15:45:19 +0200505 sys.stderr.write('\n--- File to upload: ')
506 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200507 with self.console:
508 filename = sys.stdin.readline().rstrip('\r\n')
509 if filename:
510 try:
511 with open(filename, 'rb') as f:
512 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
513 while True:
514 block = f.read(1024)
515 if not block:
516 break
517 self.serial.write(block)
518 # Wait for output buffer to drain.
519 self.serial.flush()
520 sys.stderr.write('.') # Progress indicator.
521 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
522 except IOError as e:
523 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200524 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200525 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200526 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200527 self.serial.rts = not self.serial.rts
528 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200529 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200530 self.serial.dtr = not self.serial.dtr
531 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200532 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200533 self.serial.break_condition = not self.serial.break_condition
534 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200535 elif c == '\x05': # CTRL+E -> toggle local echo
536 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200537 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200538 elif c == '\x06': # CTRL+F -> edit filters
539 sys.stderr.write('\n--- Available Filters:\n')
540 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100541 '--- {:<10} = {.__doc__}'.format(k, v)
542 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200543 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
544 with self.console:
545 new_filters = sys.stdin.readline().lower().split()
546 if new_filters:
547 for f in new_filters:
548 if f not in TRANSFORMATIONS:
Chris Liechtibfaf6c82016-10-01 22:54:27 +0200549 sys.stderr.write('--- unknown filter: {}\n'.format(repr(f)))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200550 break
551 else:
552 self.filters = new_filters
553 self.update_transformations()
554 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
555 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti033f17c2015-08-30 21:28:04 +0200556 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200557 eol = modes.index(self.eol) + 1
558 if eol >= len(modes):
559 eol = 0
560 self.eol = modes[eol]
561 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
562 self.update_transformations()
563 elif c == '\x01': # CTRL+A -> set encoding
564 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
565 with self.console:
566 new_encoding = sys.stdin.readline().strip()
567 if new_encoding:
568 try:
569 codecs.lookup(new_encoding)
570 except LookupError:
571 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
572 else:
573 self.set_rx_encoding(new_encoding)
574 self.set_tx_encoding(new_encoding)
575 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
576 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechti7af7c752015-08-12 15:45:19 +0200577 elif c == '\x09': # CTRL+I -> info
578 self.dump_port_settings()
579 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
580 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
581 elif c in 'pP': # P -> change port
Chris Liechti269f77b2015-08-24 01:31:42 +0200582 with self.console:
583 try:
Chris Liechti89313c92015-09-01 02:33:13 +0200584 port = ask_for_port()
Chris Liechti269f77b2015-08-24 01:31:42 +0200585 except KeyboardInterrupt:
586 port = None
Chris Liechti7af7c752015-08-12 15:45:19 +0200587 if port and port != self.serial.port:
588 # reader thread needs to be shut down
589 self._stop_reader()
590 # save settings
591 settings = self.serial.getSettingsDict()
592 try:
593 new_serial = serial.serial_for_url(port, do_not_open=True)
594 # restore settings and open
595 new_serial.applySettingsDict(settings)
Chris Liechti89313c92015-09-01 02:33:13 +0200596 new_serial.rts = self.serial.rts
597 new_serial.dtr = self.serial.dtr
Chris Liechti7af7c752015-08-12 15:45:19 +0200598 new_serial.open()
Chris Liechti89313c92015-09-01 02:33:13 +0200599 new_serial.break_condition = self.serial.break_condition
Chris Liechti7af7c752015-08-12 15:45:19 +0200600 except Exception as e:
Chris Liechti442bf512015-08-15 01:42:24 +0200601 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200602 new_serial.close()
603 else:
604 self.serial.close()
605 self.serial = new_serial
Chris Liechti442bf512015-08-15 01:42:24 +0200606 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
Chris Liechti7af7c752015-08-12 15:45:19 +0200607 # and restart the reader thread
608 self._start_reader()
Chris Liechtia73b96b2017-07-13 23:32:24 +0200609 elif c in 'sS': # S -> suspend / open port temporarily
610 # reader thread needs to be shut down
611 self._stop_reader()
612 self.serial.close()
Chris Liechtiae59fd02017-07-14 23:51:45 +0200613 sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
614 while not self.serial.is_open:
615 sys.stderr.write('--- press {exit} to exit or any other key to reconnect ---\n'.format(
616 exit=key_description(self.exit_character)))
617 k = self.console.getkey()
618 if k == self.exit_character:
619 self.stop() # exit app
620 break
621 try:
622 self.serial.open()
623 except Exception as e:
624 sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
Chris Liechtia73b96b2017-07-13 23:32:24 +0200625 # and restart the reader thread
626 self._start_reader()
627 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
Chris Liechti7af7c752015-08-12 15:45:19 +0200628 elif c in 'bB': # B -> change baudrate
629 sys.stderr.write('\n--- Baudrate: ')
630 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200631 with self.console:
632 backup = self.serial.baudrate
633 try:
634 self.serial.baudrate = int(sys.stdin.readline().strip())
635 except ValueError as e:
Chris Liechti4d541e22016-02-13 23:13:59 +0100636 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
Chris Liechti269f77b2015-08-24 01:31:42 +0200637 self.serial.baudrate = backup
638 else:
639 self.dump_port_settings()
Chris Liechti7af7c752015-08-12 15:45:19 +0200640 elif c == '8': # 8 -> change to 8 bits
641 self.serial.bytesize = serial.EIGHTBITS
642 self.dump_port_settings()
643 elif c == '7': # 7 -> change to 8 bits
644 self.serial.bytesize = serial.SEVENBITS
645 self.dump_port_settings()
646 elif c in 'eE': # E -> change to even parity
647 self.serial.parity = serial.PARITY_EVEN
648 self.dump_port_settings()
649 elif c in 'oO': # O -> change to odd parity
650 self.serial.parity = serial.PARITY_ODD
651 self.dump_port_settings()
652 elif c in 'mM': # M -> change to mark parity
653 self.serial.parity = serial.PARITY_MARK
654 self.dump_port_settings()
655 elif c in 'sS': # S -> change to space parity
656 self.serial.parity = serial.PARITY_SPACE
657 self.dump_port_settings()
658 elif c in 'nN': # N -> change to no parity
659 self.serial.parity = serial.PARITY_NONE
660 self.dump_port_settings()
661 elif c == '1': # 1 -> change to 1 stop bits
662 self.serial.stopbits = serial.STOPBITS_ONE
663 self.dump_port_settings()
664 elif c == '2': # 2 -> change to 2 stop bits
665 self.serial.stopbits = serial.STOPBITS_TWO
666 self.dump_port_settings()
667 elif c == '3': # 3 -> change to 1.5 stop bits
668 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
669 self.dump_port_settings()
670 elif c in 'xX': # X -> change software flow control
671 self.serial.xonxoff = (c == 'X')
672 self.dump_port_settings()
673 elif c in 'rR': # R -> change hardware flow control
674 self.serial.rtscts = (c == 'R')
675 self.dump_port_settings()
676 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200677 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
678
679 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100680 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200681 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200682 return """
683--- pySerial ({version}) - miniterm - help
684---
685--- {exit:8} Exit program
686--- {menu:8} Menu escape key, followed by:
687--- Menu keys:
688--- {menu:7} Send the menu character itself to remote
689--- {exit:7} Send the exit character itself to remote
690--- {info:7} Show info
691--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200692--- {repr:7} encoding
693--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200694--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200695--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
696--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200697---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200698--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200699--- p change port
700--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200701--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200702--- 1 2 3 set stop bits (1, 2, 1.5)
703--- b change baud rate
704--- x X disable/enable software flow control
705--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100706""".format(version=getattr(serial, 'VERSION', 'unknown version'),
707 exit=key_description(self.exit_character),
708 menu=key_description(self.menu_character),
709 rts=key_description('\x12'),
710 dtr=key_description('\x04'),
711 brk=key_description('\x02'),
712 echo=key_description('\x05'),
713 info=key_description('\x09'),
714 upload=key_description('\x15'),
715 repr=key_description('\x01'),
716 filter=key_description('\x06'),
717 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200718
719
Chris Liechtib3df13e2015-08-25 02:20:09 +0200720# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200721# default args can be used to override when calling main() from an other script
722# e.g to create a miniterm-my-device.py
723def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100724 """Command line tool, entry point"""
725
Chris Liechtib7550bd2015-08-15 04:09:10 +0200726 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000727
Chris Liechtib7550bd2015-08-15 04:09:10 +0200728 parser = argparse.ArgumentParser(
Chris Liechti397cf412016-02-11 00:11:48 +0100729 description="Miniterm - A simple terminal program for the serial port.")
cliechti6385f2c2005-09-21 19:51:19 +0000730
Chris Liechti033f17c2015-08-30 21:28:04 +0200731 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100732 "port",
733 nargs='?',
734 help="serial port name ('-' to show port list)",
735 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000736
Chris Liechti033f17c2015-08-30 21:28:04 +0200737 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100738 "baudrate",
739 nargs='?',
740 type=int,
741 help="set baud rate, default: %(default)s",
742 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000743
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200744 group = parser.add_argument_group("port settings")
cliechti53edb472009-02-06 21:18:46 +0000745
Chris Liechti033f17c2015-08-30 21:28:04 +0200746 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100747 "--parity",
748 choices=['N', 'E', 'O', 'S', 'M'],
749 type=lambda c: c.upper(),
750 help="set parity, one of {N E O S M}, default: N",
751 default='N')
cliechti53edb472009-02-06 21:18:46 +0000752
Chris Liechti033f17c2015-08-30 21:28:04 +0200753 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100754 "--rtscts",
755 action="store_true",
756 help="enable RTS/CTS flow control (default off)",
757 default=False)
cliechti53edb472009-02-06 21:18:46 +0000758
Chris Liechti033f17c2015-08-30 21:28:04 +0200759 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100760 "--xonxoff",
761 action="store_true",
762 help="enable software flow control (default off)",
763 default=False)
cliechti53edb472009-02-06 21:18:46 +0000764
Chris Liechti033f17c2015-08-30 21:28:04 +0200765 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100766 "--rts",
767 type=int,
768 help="set initial RTS line state (possible values: 0, 1)",
769 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000770
Chris Liechti033f17c2015-08-30 21:28:04 +0200771 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100772 "--dtr",
773 type=int,
774 help="set initial DTR line state (possible values: 0, 1)",
775 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000776
Chris Liechti00f84282015-12-24 23:40:34 +0100777 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100778 "--ask",
779 action="store_true",
780 help="ask again for port when open fails",
781 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100782
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200783 group = parser.add_argument_group("data handling")
cliechti5370cee2013-10-13 03:08:19 +0000784
Chris Liechti033f17c2015-08-30 21:28:04 +0200785 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100786 "-e", "--echo",
787 action="store_true",
788 help="enable local echo (default off)",
789 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000790
Chris Liechti033f17c2015-08-30 21:28:04 +0200791 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100792 "--encoding",
793 dest="serial_port_encoding",
794 metavar="CODEC",
795 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
796 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000797
Chris Liechti033f17c2015-08-30 21:28:04 +0200798 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100799 "-f", "--filter",
800 action="append",
801 metavar="NAME",
802 help="add text transformation",
803 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200804
Chris Liechti033f17c2015-08-30 21:28:04 +0200805 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100806 "--eol",
807 choices=['CR', 'LF', 'CRLF'],
808 type=lambda c: c.upper(),
809 help="end of line mode",
810 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000811
Chris Liechti033f17c2015-08-30 21:28:04 +0200812 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100813 "--raw",
814 action="store_true",
815 help="Do no apply any encodings/transformations",
816 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000817
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200818 group = parser.add_argument_group("hotkeys")
cliechtib7d746d2006-03-28 22:44:30 +0000819
Chris Liechti033f17c2015-08-30 21:28:04 +0200820 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100821 "--exit-char",
822 type=int,
823 metavar='NUM',
824 help="Unicode of special character that is used to exit the application, default: %(default)s",
825 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000826
Chris Liechti033f17c2015-08-30 21:28:04 +0200827 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100828 "--menu-char",
829 type=int,
830 metavar='NUM',
831 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
832 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000833
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200834 group = parser.add_argument_group("diagnostics")
cliechti6385f2c2005-09-21 19:51:19 +0000835
Chris Liechti033f17c2015-08-30 21:28:04 +0200836 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100837 "-q", "--quiet",
838 action="store_true",
839 help="suppress non-error messages",
840 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000841
Chris Liechti033f17c2015-08-30 21:28:04 +0200842 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100843 "--develop",
844 action="store_true",
845 help="show Python traceback on error",
846 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000847
Chris Liechtib7550bd2015-08-15 04:09:10 +0200848 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000849
Chris Liechtib7550bd2015-08-15 04:09:10 +0200850 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000851 parser.error('--exit-char can not be the same as --menu-char')
852
Chris Liechtib3df13e2015-08-25 02:20:09 +0200853 if args.filter:
854 if 'help' in args.filter:
855 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200856 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100857 '{:<10} = {.__doc__}'.format(k, v)
858 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200859 sys.stderr.write('\n')
860 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200861 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200862 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200863 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200864
Chris Liechti00f84282015-12-24 23:40:34 +0100865 while True:
866 # no port given on command line -> ask user now
867 if args.port is None or args.port == '-':
868 try:
869 args.port = ask_for_port()
870 except KeyboardInterrupt:
871 sys.stderr.write('\n')
872 parser.error('user aborted and port is not given')
873 else:
874 if not args.port:
875 parser.error('port is not given')
876 try:
877 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100878 args.port,
879 args.baudrate,
880 parity=args.parity,
881 rtscts=args.rtscts,
882 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100883 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200884
Chris Liechtif542fca2016-05-13 00:20:14 +0200885 if not hasattr(serial_instance, 'cancel_read'):
886 # enable timeout for alive flag polling if cancel_read is not available
887 serial_instance.timeout = 1
888
Chris Liechti00f84282015-12-24 23:40:34 +0100889 if args.dtr is not None:
890 if not args.quiet:
891 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
892 serial_instance.dtr = args.dtr
893 if args.rts is not None:
894 if not args.quiet:
895 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
896 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200897
Chris Liechti00f84282015-12-24 23:40:34 +0100898 serial_instance.open()
899 except serial.SerialException as e:
900 sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
901 if args.develop:
902 raise
903 if not args.ask:
904 sys.exit(1)
905 else:
906 args.port = '-'
907 else:
908 break
cliechti6385f2c2005-09-21 19:51:19 +0000909
Chris Liechti3b454802015-08-26 23:39:59 +0200910 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100911 serial_instance,
912 echo=args.echo,
913 eol=args.eol.lower(),
914 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200915 miniterm.exit_character = unichr(args.exit_char)
916 miniterm.menu_character = unichr(args.menu_char)
917 miniterm.raw = args.raw
918 miniterm.set_rx_encoding(args.serial_port_encoding)
919 miniterm.set_tx_encoding(args.serial_port_encoding)
920
Chris Liechtib7550bd2015-08-15 04:09:10 +0200921 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200922 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100923 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200924 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100925 key_description(miniterm.exit_character),
926 key_description(miniterm.menu_character),
927 key_description(miniterm.menu_character),
928 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +0000929
cliechti6385f2c2005-09-21 19:51:19 +0000930 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +0000931 try:
932 miniterm.join(True)
933 except KeyboardInterrupt:
934 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +0200935 if not args.quiet:
cliechtibf6bb7d2006-03-30 00:28:18 +0000936 sys.stderr.write("\n--- exit ---\n")
cliechti6385f2c2005-09-21 19:51:19 +0000937 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +0200938 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +0000939
cliechti5370cee2013-10-13 03:08:19 +0000940# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +0000941if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +0000942 main()