blob: f1e9beddb3b7b05b77c6a6c463d317d90b21deb6 [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()
613 sys.stderr.write('--- Port closed: {} ---\n'.format(self.serial.port))
614 sys.stderr.write('--- press any key to reconnect ---\n')
615 self.console.getkey()
616 self.serial.open()
617 # and restart the reader thread
618 self._start_reader()
619 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
Chris Liechti7af7c752015-08-12 15:45:19 +0200620 elif c in 'bB': # B -> change baudrate
621 sys.stderr.write('\n--- Baudrate: ')
622 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200623 with self.console:
624 backup = self.serial.baudrate
625 try:
626 self.serial.baudrate = int(sys.stdin.readline().strip())
627 except ValueError as e:
Chris Liechti4d541e22016-02-13 23:13:59 +0100628 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
Chris Liechti269f77b2015-08-24 01:31:42 +0200629 self.serial.baudrate = backup
630 else:
631 self.dump_port_settings()
Chris Liechti7af7c752015-08-12 15:45:19 +0200632 elif c == '8': # 8 -> change to 8 bits
633 self.serial.bytesize = serial.EIGHTBITS
634 self.dump_port_settings()
635 elif c == '7': # 7 -> change to 8 bits
636 self.serial.bytesize = serial.SEVENBITS
637 self.dump_port_settings()
638 elif c in 'eE': # E -> change to even parity
639 self.serial.parity = serial.PARITY_EVEN
640 self.dump_port_settings()
641 elif c in 'oO': # O -> change to odd parity
642 self.serial.parity = serial.PARITY_ODD
643 self.dump_port_settings()
644 elif c in 'mM': # M -> change to mark parity
645 self.serial.parity = serial.PARITY_MARK
646 self.dump_port_settings()
647 elif c in 'sS': # S -> change to space parity
648 self.serial.parity = serial.PARITY_SPACE
649 self.dump_port_settings()
650 elif c in 'nN': # N -> change to no parity
651 self.serial.parity = serial.PARITY_NONE
652 self.dump_port_settings()
653 elif c == '1': # 1 -> change to 1 stop bits
654 self.serial.stopbits = serial.STOPBITS_ONE
655 self.dump_port_settings()
656 elif c == '2': # 2 -> change to 2 stop bits
657 self.serial.stopbits = serial.STOPBITS_TWO
658 self.dump_port_settings()
659 elif c == '3': # 3 -> change to 1.5 stop bits
660 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
661 self.dump_port_settings()
662 elif c in 'xX': # X -> change software flow control
663 self.serial.xonxoff = (c == 'X')
664 self.dump_port_settings()
665 elif c in 'rR': # R -> change hardware flow control
666 self.serial.rtscts = (c == 'R')
667 self.dump_port_settings()
668 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200669 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
670
671 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100672 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200673 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200674 return """
675--- pySerial ({version}) - miniterm - help
676---
677--- {exit:8} Exit program
678--- {menu:8} Menu escape key, followed by:
679--- Menu keys:
680--- {menu:7} Send the menu character itself to remote
681--- {exit:7} Send the exit character itself to remote
682--- {info:7} Show info
683--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200684--- {repr:7} encoding
685--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200686--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200687--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
688--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200689---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200690--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200691--- p change port
692--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200693--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200694--- 1 2 3 set stop bits (1, 2, 1.5)
695--- b change baud rate
696--- x X disable/enable software flow control
697--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100698""".format(version=getattr(serial, 'VERSION', 'unknown version'),
699 exit=key_description(self.exit_character),
700 menu=key_description(self.menu_character),
701 rts=key_description('\x12'),
702 dtr=key_description('\x04'),
703 brk=key_description('\x02'),
704 echo=key_description('\x05'),
705 info=key_description('\x09'),
706 upload=key_description('\x15'),
707 repr=key_description('\x01'),
708 filter=key_description('\x06'),
709 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200710
711
Chris Liechtib3df13e2015-08-25 02:20:09 +0200712# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200713# default args can be used to override when calling main() from an other script
714# e.g to create a miniterm-my-device.py
715def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100716 """Command line tool, entry point"""
717
Chris Liechtib7550bd2015-08-15 04:09:10 +0200718 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000719
Chris Liechtib7550bd2015-08-15 04:09:10 +0200720 parser = argparse.ArgumentParser(
Chris Liechti397cf412016-02-11 00:11:48 +0100721 description="Miniterm - A simple terminal program for the serial port.")
cliechti6385f2c2005-09-21 19:51:19 +0000722
Chris Liechti033f17c2015-08-30 21:28:04 +0200723 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100724 "port",
725 nargs='?',
726 help="serial port name ('-' to show port list)",
727 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000728
Chris Liechti033f17c2015-08-30 21:28:04 +0200729 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100730 "baudrate",
731 nargs='?',
732 type=int,
733 help="set baud rate, default: %(default)s",
734 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000735
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200736 group = parser.add_argument_group("port settings")
cliechti53edb472009-02-06 21:18:46 +0000737
Chris Liechti033f17c2015-08-30 21:28:04 +0200738 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100739 "--parity",
740 choices=['N', 'E', 'O', 'S', 'M'],
741 type=lambda c: c.upper(),
742 help="set parity, one of {N E O S M}, default: N",
743 default='N')
cliechti53edb472009-02-06 21:18:46 +0000744
Chris Liechti033f17c2015-08-30 21:28:04 +0200745 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100746 "--rtscts",
747 action="store_true",
748 help="enable RTS/CTS flow control (default off)",
749 default=False)
cliechti53edb472009-02-06 21:18:46 +0000750
Chris Liechti033f17c2015-08-30 21:28:04 +0200751 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100752 "--xonxoff",
753 action="store_true",
754 help="enable software flow control (default off)",
755 default=False)
cliechti53edb472009-02-06 21:18:46 +0000756
Chris Liechti033f17c2015-08-30 21:28:04 +0200757 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100758 "--rts",
759 type=int,
760 help="set initial RTS line state (possible values: 0, 1)",
761 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000762
Chris Liechti033f17c2015-08-30 21:28:04 +0200763 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100764 "--dtr",
765 type=int,
766 help="set initial DTR line state (possible values: 0, 1)",
767 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000768
Chris Liechti00f84282015-12-24 23:40:34 +0100769 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100770 "--ask",
771 action="store_true",
772 help="ask again for port when open fails",
773 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100774
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200775 group = parser.add_argument_group("data handling")
cliechti5370cee2013-10-13 03:08:19 +0000776
Chris Liechti033f17c2015-08-30 21:28:04 +0200777 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100778 "-e", "--echo",
779 action="store_true",
780 help="enable local echo (default off)",
781 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000782
Chris Liechti033f17c2015-08-30 21:28:04 +0200783 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100784 "--encoding",
785 dest="serial_port_encoding",
786 metavar="CODEC",
787 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
788 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000789
Chris Liechti033f17c2015-08-30 21:28:04 +0200790 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100791 "-f", "--filter",
792 action="append",
793 metavar="NAME",
794 help="add text transformation",
795 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200796
Chris Liechti033f17c2015-08-30 21:28:04 +0200797 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100798 "--eol",
799 choices=['CR', 'LF', 'CRLF'],
800 type=lambda c: c.upper(),
801 help="end of line mode",
802 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000803
Chris Liechti033f17c2015-08-30 21:28:04 +0200804 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100805 "--raw",
806 action="store_true",
807 help="Do no apply any encodings/transformations",
808 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000809
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200810 group = parser.add_argument_group("hotkeys")
cliechtib7d746d2006-03-28 22:44:30 +0000811
Chris Liechti033f17c2015-08-30 21:28:04 +0200812 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100813 "--exit-char",
814 type=int,
815 metavar='NUM',
816 help="Unicode of special character that is used to exit the application, default: %(default)s",
817 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000818
Chris Liechti033f17c2015-08-30 21:28:04 +0200819 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100820 "--menu-char",
821 type=int,
822 metavar='NUM',
823 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
824 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000825
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200826 group = parser.add_argument_group("diagnostics")
cliechti6385f2c2005-09-21 19:51:19 +0000827
Chris Liechti033f17c2015-08-30 21:28:04 +0200828 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100829 "-q", "--quiet",
830 action="store_true",
831 help="suppress non-error messages",
832 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000833
Chris Liechti033f17c2015-08-30 21:28:04 +0200834 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100835 "--develop",
836 action="store_true",
837 help="show Python traceback on error",
838 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000839
Chris Liechtib7550bd2015-08-15 04:09:10 +0200840 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000841
Chris Liechtib7550bd2015-08-15 04:09:10 +0200842 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000843 parser.error('--exit-char can not be the same as --menu-char')
844
Chris Liechtib3df13e2015-08-25 02:20:09 +0200845 if args.filter:
846 if 'help' in args.filter:
847 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200848 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100849 '{:<10} = {.__doc__}'.format(k, v)
850 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200851 sys.stderr.write('\n')
852 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200853 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200854 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200855 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200856
Chris Liechti00f84282015-12-24 23:40:34 +0100857 while True:
858 # no port given on command line -> ask user now
859 if args.port is None or args.port == '-':
860 try:
861 args.port = ask_for_port()
862 except KeyboardInterrupt:
863 sys.stderr.write('\n')
864 parser.error('user aborted and port is not given')
865 else:
866 if not args.port:
867 parser.error('port is not given')
868 try:
869 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100870 args.port,
871 args.baudrate,
872 parity=args.parity,
873 rtscts=args.rtscts,
874 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100875 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200876
Chris Liechtif542fca2016-05-13 00:20:14 +0200877 if not hasattr(serial_instance, 'cancel_read'):
878 # enable timeout for alive flag polling if cancel_read is not available
879 serial_instance.timeout = 1
880
Chris Liechti00f84282015-12-24 23:40:34 +0100881 if args.dtr is not None:
882 if not args.quiet:
883 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
884 serial_instance.dtr = args.dtr
885 if args.rts is not None:
886 if not args.quiet:
887 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
888 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200889
Chris Liechti00f84282015-12-24 23:40:34 +0100890 serial_instance.open()
891 except serial.SerialException as e:
892 sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
893 if args.develop:
894 raise
895 if not args.ask:
896 sys.exit(1)
897 else:
898 args.port = '-'
899 else:
900 break
cliechti6385f2c2005-09-21 19:51:19 +0000901
Chris Liechti3b454802015-08-26 23:39:59 +0200902 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100903 serial_instance,
904 echo=args.echo,
905 eol=args.eol.lower(),
906 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200907 miniterm.exit_character = unichr(args.exit_char)
908 miniterm.menu_character = unichr(args.menu_char)
909 miniterm.raw = args.raw
910 miniterm.set_rx_encoding(args.serial_port_encoding)
911 miniterm.set_tx_encoding(args.serial_port_encoding)
912
Chris Liechtib7550bd2015-08-15 04:09:10 +0200913 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200914 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100915 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200916 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100917 key_description(miniterm.exit_character),
918 key_description(miniterm.menu_character),
919 key_description(miniterm.menu_character),
920 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +0000921
cliechti6385f2c2005-09-21 19:51:19 +0000922 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +0000923 try:
924 miniterm.join(True)
925 except KeyboardInterrupt:
926 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +0200927 if not args.quiet:
cliechtibf6bb7d2006-03-30 00:28:18 +0000928 sys.stderr.write("\n--- exit ---\n")
cliechti6385f2c2005-09-21 19:51:19 +0000929 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +0200930 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +0000931
cliechti5370cee2013-10-13 03:08:19 +0000932# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +0000933if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +0000934 main()