blob: b0804ba9645b81a7ec77f6a2840c33dbfa0de82b [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
Kurt McKee057387c2018-02-07 22:10:38 -060010from __future__ import absolute_import
11
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020012import codecs
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020013import os
14import sys
15import threading
cliechti576de252002-02-28 23:54:44 +000016
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020017import serial
Chris Liechti55ba7d92015-08-15 16:33:51 +020018from serial.tools.list_ports import comports
Chris Liechti168704f2015-09-30 16:50:29 +020019from serial.tools import hexlify_codec
20
Chris Liechtia887c932016-02-13 23:10:14 +010021# pylint: disable=wrong-import-order,wrong-import-position
22
Chris Liechti168704f2015-09-30 16:50:29 +020023codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None)
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020024
Chris Liechti68340d72015-08-03 14:15:48 +020025try:
26 raw_input
27except NameError:
Chris Liechtia887c932016-02-13 23:10:14 +010028 # pylint: disable=redefined-builtin,invalid-name
Chris Liechti68340d72015-08-03 14:15:48 +020029 raw_input = input # in python3 it's "raw"
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020030 unichr = chr
Chris Liechti68340d72015-08-03 14:15:48 +020031
cliechti6c8eb2f2009-07-08 02:10:46 +000032
33def key_description(character):
34 """generate a readable description for a key"""
35 ascii_code = ord(character)
36 if ascii_code < 32:
Chris Liechtic8f3f822016-06-08 03:35:28 +020037 return 'Ctrl+{:c}'.format(ord('@') + ascii_code)
cliechti6c8eb2f2009-07-08 02:10:46 +000038 else:
39 return repr(character)
40
cliechti91165532011-03-18 02:02:52 +000041
Chris Liechti9a720852015-08-25 00:20:38 +020042# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020043class ConsoleBase(object):
Chris Liechti397cf412016-02-11 00:11:48 +010044 """OS abstraction for console (input/output codec, no echo)"""
45
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020046 def __init__(self):
47 if sys.version_info >= (3, 0):
48 self.byte_output = sys.stdout.buffer
49 else:
50 self.byte_output = sys.stdout
51 self.output = sys.stdout
cliechtif467aa82013-10-13 21:36:49 +000052
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020053 def setup(self):
Chris Liechti397cf412016-02-11 00:11:48 +010054 """Set console to read single characters, no echo"""
cliechtif467aa82013-10-13 21:36:49 +000055
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020056 def cleanup(self):
Chris Liechti397cf412016-02-11 00:11:48 +010057 """Restore default console settings"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020058
59 def getkey(self):
Chris Liechti397cf412016-02-11 00:11:48 +010060 """Read a single key from the console"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020061 return None
62
Chris Liechtia887c932016-02-13 23:10:14 +010063 def write_bytes(self, byte_string):
Chris Liechti397cf412016-02-11 00:11:48 +010064 """Write bytes (already encoded)"""
Chris Liechtia887c932016-02-13 23:10:14 +010065 self.byte_output.write(byte_string)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020066 self.byte_output.flush()
67
Chris Liechtia887c932016-02-13 23:10:14 +010068 def write(self, text):
Chris Liechti397cf412016-02-11 00:11:48 +010069 """Write string"""
Chris Liechtia887c932016-02-13 23:10:14 +010070 self.output.write(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020071 self.output.flush()
72
Chris Liechti1eb3f6b2016-04-27 02:12:50 +020073 def cancel(self):
74 """Cancel getkey operation"""
75
Chris Liechti269f77b2015-08-24 01:31:42 +020076 # - - - - - - - - - - - - - - - - - - - - - - - -
77 # context manager:
78 # switch terminal temporary to normal mode (e.g. to get user input)
79
80 def __enter__(self):
81 self.cleanup()
82 return self
83
84 def __exit__(self, *args, **kwargs):
85 self.setup()
86
cliechti9c592b32008-06-16 22:00:14 +000087
Chris Liechtiba45c522016-02-06 23:53:23 +010088if os.name == 'nt': # noqa
cliechti576de252002-02-28 23:54:44 +000089 import msvcrt
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020090 import ctypes
Chris Liechti9cc696b2015-08-28 00:54:22 +020091
92 class Out(object):
Chris Liechtia887c932016-02-13 23:10:14 +010093 """file-like wrapper that uses os.write"""
94
Chris Liechti9cc696b2015-08-28 00:54:22 +020095 def __init__(self, fd):
96 self.fd = fd
97
98 def flush(self):
99 pass
100
101 def write(self, s):
102 os.write(self.fd, s)
103
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200104 class Console(ConsoleBase):
Chris Liechticbb00b22015-08-13 22:58:49 +0200105 def __init__(self):
106 super(Console, self).__init__()
Chris Liechti1df28272015-08-27 23:37:38 +0200107 self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
108 self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
Chris Liechticbb00b22015-08-13 22:58:49 +0200109 ctypes.windll.kernel32.SetConsoleOutputCP(65001)
110 ctypes.windll.kernel32.SetConsoleCP(65001)
Chris Liechti9cc696b2015-08-28 00:54:22 +0200111 self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
112 # the change of the code page is not propagated to Python, manually fix it
113 sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
114 sys.stdout = self.output
Chris Liechti168704f2015-09-30 16:50:29 +0200115 self.output.encoding = 'UTF-8' # needed for input
Chris Liechticbb00b22015-08-13 22:58:49 +0200116
Chris Liechti1df28272015-08-27 23:37:38 +0200117 def __del__(self):
118 ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
119 ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
120
cliechti3a8bf092008-09-17 11:26:53 +0000121 def getkey(self):
cliechti91165532011-03-18 02:02:52 +0000122 while True:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200123 z = msvcrt.getwch()
Chris Liechti9f398812015-09-13 18:50:44 +0200124 if z == unichr(13):
125 return unichr(10)
126 elif z in (unichr(0), unichr(0x0e)): # functions keys, ignore
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200127 msvcrt.getwch()
cliechti9c592b32008-06-16 22:00:14 +0000128 else:
cliechti9c592b32008-06-16 22:00:14 +0000129 return z
cliechti53edb472009-02-06 21:18:46 +0000130
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200131 def cancel(self):
Chris Liechtic20c3732016-05-14 02:25:13 +0200132 # CancelIo, CancelSynchronousIo do not seem to work when using
133 # getwch, so instead, send a key to the window with the console
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200134 hwnd = ctypes.windll.kernel32.GetConsoleWindow()
135 ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
136
cliechti576de252002-02-28 23:54:44 +0000137elif os.name == 'posix':
Chris Liechtia1d5c6d2015-08-07 14:41:24 +0200138 import atexit
139 import termios
Chris Liechticab3dab2016-12-07 01:27:41 +0100140 import fcntl
Chris Liechti9cc696b2015-08-28 00:54:22 +0200141
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200142 class Console(ConsoleBase):
cliechti9c592b32008-06-16 22:00:14 +0000143 def __init__(self):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200144 super(Console, self).__init__()
cliechti9c592b32008-06-16 22:00:14 +0000145 self.fd = sys.stdin.fileno()
Chris Liechti4d989c22015-08-24 00:24:49 +0200146 self.old = termios.tcgetattr(self.fd)
Chris Liechti89eb2472015-08-08 17:06:25 +0200147 atexit.register(self.cleanup)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200148 if sys.version_info < (3, 0):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200149 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
150 else:
151 self.enc_stdin = sys.stdin
cliechti9c592b32008-06-16 22:00:14 +0000152
153 def setup(self):
cliechti9c592b32008-06-16 22:00:14 +0000154 new = termios.tcgetattr(self.fd)
155 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
156 new[6][termios.VMIN] = 1
157 new[6][termios.VTIME] = 0
158 termios.tcsetattr(self.fd, termios.TCSANOW, new)
cliechti53edb472009-02-06 21:18:46 +0000159
cliechti9c592b32008-06-16 22:00:14 +0000160 def getkey(self):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200161 c = self.enc_stdin.read(1)
Chris Liechti9f398812015-09-13 18:50:44 +0200162 if c == unichr(0x7f):
163 c = unichr(8) # map the BS key (which yields DEL) to backspace
Chris Liechti9a720852015-08-25 00:20:38 +0200164 return c
cliechti53edb472009-02-06 21:18:46 +0000165
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200166 def cancel(self):
Chris Liechticab3dab2016-12-07 01:27:41 +0100167 fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200168
cliechti9c592b32008-06-16 22:00:14 +0000169 def cleanup(self):
Chris Liechti4d989c22015-08-24 00:24:49 +0200170 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
cliechti9c592b32008-06-16 22:00:14 +0000171
cliechti576de252002-02-28 23:54:44 +0000172else:
Chris Liechti397cf412016-02-11 00:11:48 +0100173 raise NotImplementedError(
174 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
cliechti576de252002-02-28 23:54:44 +0000175
cliechti6fa76fb2009-07-08 23:53:39 +0000176
Chris Liechti9a720852015-08-25 00:20:38 +0200177# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200178
179class Transform(object):
Chris Liechticbb00b22015-08-13 22:58:49 +0200180 """do-nothing: forward all data unchanged"""
Chris Liechtid698af72015-08-24 20:24:55 +0200181 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200182 """text received from serial port"""
183 return text
184
Chris Liechtid698af72015-08-24 20:24:55 +0200185 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200186 """text to be sent to serial port"""
187 return text
188
189 def echo(self, text):
190 """text to be sent but displayed on console"""
191 return text
192
Chris Liechti442bf512015-08-15 01:42:24 +0200193
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200194class CRLF(Transform):
195 """ENTER sends CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200196
Chris Liechtid698af72015-08-24 20:24:55 +0200197 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200198 return text.replace('\n', '\r\n')
199
Chris Liechti442bf512015-08-15 01:42:24 +0200200
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200201class CR(Transform):
202 """ENTER sends CR"""
Chris Liechtid698af72015-08-24 20:24:55 +0200203
204 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200205 return text.replace('\r', '\n')
206
Chris Liechtid698af72015-08-24 20:24:55 +0200207 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200208 return text.replace('\n', '\r')
209
Chris Liechti442bf512015-08-15 01:42:24 +0200210
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200211class LF(Transform):
212 """ENTER sends LF"""
213
214
215class NoTerminal(Transform):
216 """remove typical terminal control codes from input"""
Chris Liechti9a720852015-08-25 00:20:38 +0200217
218 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 +0100219 REPLACEMENT_MAP.update(
220 {
Chris Liechti033f17c2015-08-30 21:28:04 +0200221 0x7F: 0x2421, # DEL
222 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100223 })
Chris Liechti9a720852015-08-25 00:20:38 +0200224
Chris Liechtid698af72015-08-24 20:24:55 +0200225 def rx(self, text):
Chris Liechti9a720852015-08-25 00:20:38 +0200226 return text.translate(self.REPLACEMENT_MAP)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200227
Chris Liechtid698af72015-08-24 20:24:55 +0200228 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200229
230
Chris Liechti9a720852015-08-25 00:20:38 +0200231class NoControls(NoTerminal):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200232 """Remove all control codes, incl. CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200233
Chris Liechti9a720852015-08-25 00:20:38 +0200234 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
Chris Liechtiba45c522016-02-06 23:53:23 +0100235 REPLACEMENT_MAP.update(
236 {
Chris Liechtia887c932016-02-13 23:10:14 +0100237 0x20: 0x2423, # visual space
Chris Liechti033f17c2015-08-30 21:28:04 +0200238 0x7F: 0x2421, # DEL
239 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100240 })
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200241
242
243class Printable(Transform):
Chris Liechtid698af72015-08-24 20:24:55 +0200244 """Show decimal code for all non-ASCII characters and replace most control codes"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200245
Chris Liechtid698af72015-08-24 20:24:55 +0200246 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200247 r = []
Chris Liechtia887c932016-02-13 23:10:14 +0100248 for c in text:
249 if ' ' <= c < '\x7f' or c in '\r\n\b\t':
250 r.append(c)
251 elif c < ' ':
252 r.append(unichr(0x2400 + ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200253 else:
Chris Liechtia887c932016-02-13 23:10:14 +0100254 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200255 r.append(' ')
256 return ''.join(r)
257
Chris Liechtid698af72015-08-24 20:24:55 +0200258 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200259
260
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200261class Colorize(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200262 """Apply different colors for received and echo"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200263
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200264 def __init__(self):
265 # XXX make it configurable, use colorama?
266 self.input_color = '\x1b[37m'
267 self.echo_color = '\x1b[31m'
268
Chris Liechtid698af72015-08-24 20:24:55 +0200269 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200270 return self.input_color + text
271
272 def echo(self, text):
273 return self.echo_color + text
274
Chris Liechti442bf512015-08-15 01:42:24 +0200275
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200276class DebugIO(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200277 """Print what is sent and received"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200278
Chris Liechtid698af72015-08-24 20:24:55 +0200279 def rx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200280 sys.stderr.write(' [RX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200281 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200282 return text
283
Chris Liechtid698af72015-08-24 20:24:55 +0200284 def tx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200285 sys.stderr.write(' [TX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200286 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200287 return text
288
Chris Liechti442bf512015-08-15 01:42:24 +0200289
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200290# other ideas:
291# - add date/time for each newline
292# - insert newline after: a) timeout b) packet end character
293
Chris Liechtib3df13e2015-08-25 02:20:09 +0200294EOL_TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100295 'crlf': CRLF,
296 'cr': CR,
297 'lf': LF,
298}
Chris Liechtib3df13e2015-08-25 02:20:09 +0200299
300TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100301 'direct': Transform, # no transformation
302 'default': NoTerminal,
303 'nocontrol': NoControls,
304 'printable': Printable,
305 'colorize': Colorize,
306 'debug': DebugIO,
307}
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200308
309
Chris Liechti033f17c2015-08-30 21:28:04 +0200310# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti89313c92015-09-01 02:33:13 +0200311def ask_for_port():
312 """\
313 Show a list of ports and ask the user for a choice. To make selection
314 easier on systems with long device names, also allow the input of an
315 index.
316 """
317 sys.stderr.write('\n--- Available ports:\n')
318 ports = []
319 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
Chris Liechti8b0eaf22017-07-19 22:59:57 +0200320 sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
Chris Liechti89313c92015-09-01 02:33:13 +0200321 ports.append(port)
322 while True:
323 port = raw_input('--- Enter port index or full name: ')
324 try:
325 index = int(port) - 1
326 if not 0 <= index < len(ports):
327 sys.stderr.write('--- Invalid index!\n')
328 continue
329 except ValueError:
330 pass
331 else:
332 port = ports[index]
333 return port
cliechti1351dde2012-04-12 16:47:47 +0000334
335
cliechti8c2ea842011-03-18 01:51:46 +0000336class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200337 """\
338 Terminal application. Copy data from serial port to console and vice versa.
339 Handle special keys from the console to show menu etc.
340 """
341
Chris Liechti3b454802015-08-26 23:39:59 +0200342 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200343 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200344 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000345 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200346 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200347 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200348 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200349 self.eol = eol
350 self.filters = filters
351 self.update_transformations()
Chris Liechti442bf512015-08-15 01:42:24 +0200352 self.exit_character = 0x1d # GS/CTRL+]
353 self.menu_character = 0x14 # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100354 self.alive = None
355 self._reader_alive = None
356 self.receiver_thread = None
357 self.rx_decoder = None
358 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000359
cliechti8c2ea842011-03-18 01:51:46 +0000360 def _start_reader(self):
361 """Start reader thread"""
362 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000363 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200364 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
365 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000366 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000367
368 def _stop_reader(self):
369 """Stop reader thread only, wait for clean exit of thread"""
370 self._reader_alive = False
Chris Liechti933a5172016-05-04 16:12:15 +0200371 if hasattr(self.serial, 'cancel_read'):
372 self.serial.cancel_read()
cliechti8c2ea842011-03-18 01:51:46 +0000373 self.receiver_thread.join()
374
cliechti8c2ea842011-03-18 01:51:46 +0000375 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100376 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000377 self.alive = True
378 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000379 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200380 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
381 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000382 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200383 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000384
cliechti6385f2c2005-09-21 19:51:19 +0000385 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100386 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000387 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000388
cliechtibf6bb7d2006-03-30 00:28:18 +0000389 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100390 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000391 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000392 if not transmit_only:
Chris Liechti933a5172016-05-04 16:12:15 +0200393 if hasattr(self.serial, 'cancel_read'):
394 self.serial.cancel_read()
cliechtibf6bb7d2006-03-30 00:28:18 +0000395 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000396
Chris Liechti933a5172016-05-04 16:12:15 +0200397 def close(self):
398 self.serial.close()
399
Chris Liechtib3df13e2015-08-25 02:20:09 +0200400 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100401 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100402 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
403 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200404 self.tx_transformations = [t() for t in transformations]
405 self.rx_transformations = list(reversed(self.tx_transformations))
406
Chris Liechtid698af72015-08-24 20:24:55 +0200407 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100408 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200409 self.input_encoding = encoding
410 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
411
412 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100413 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200414 self.output_encoding = encoding
415 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
416
cliechti6c8eb2f2009-07-08 02:10:46 +0000417 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100418 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200419 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100420 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200421 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100422 ('active' if self.serial.rts else 'inactive'),
423 ('active' if self.serial.dtr else 'inactive'),
424 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000425 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200426 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100427 ('active' if self.serial.cts else 'inactive'),
428 ('active' if self.serial.dsr else 'inactive'),
429 ('active' if self.serial.ri else 'inactive'),
430 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000431 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200432 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000433 # yet received. ignore this error.
434 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200435 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
436 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200437 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
438 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200439 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
440 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000441
cliechti6385f2c2005-09-21 19:51:19 +0000442 def reader(self):
443 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000444 try:
cliechti8c2ea842011-03-18 01:51:46 +0000445 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200446 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200447 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200448 if data:
449 if self.raw:
450 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000451 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200452 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200453 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200454 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200455 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200456 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000457 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200458 self.console.cancel()
459 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000460
cliechti6385f2c2005-09-21 19:51:19 +0000461 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000462 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200463 Loop and copy console->serial until self.exit_character character is
464 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000465 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000466 """
467 menu_active = False
468 try:
469 while self.alive:
470 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200471 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000472 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200473 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200474 if not self.alive:
475 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000476 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200477 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000478 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200479 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200480 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200481 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200482 self.stop() # exit app
483 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000484 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200485 #~ if self.raw:
486 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200487 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200488 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200489 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000490 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200491 echo_text = c
492 for transformation in self.tx_transformations:
493 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200494 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000495 except:
496 self.alive = False
497 raise
cliechti6385f2c2005-09-21 19:51:19 +0000498
Chris Liechti7af7c752015-08-12 15:45:19 +0200499 def handle_menu_key(self, c):
500 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200501 if c == self.menu_character or c == self.exit_character:
502 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200503 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200504 if self.echo:
505 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200506 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti45c6f222017-07-17 23:56:24 +0200507 self.upload_file()
Chris Liechti7af7c752015-08-12 15:45:19 +0200508 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200509 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200510 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200511 self.serial.rts = not self.serial.rts
512 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200513 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200514 self.serial.dtr = not self.serial.dtr
515 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200516 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200517 self.serial.break_condition = not self.serial.break_condition
518 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200519 elif c == '\x05': # CTRL+E -> toggle local echo
520 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200521 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200522 elif c == '\x06': # CTRL+F -> edit filters
Chris Liechti45c6f222017-07-17 23:56:24 +0200523 self.change_filter()
Chris Liechtib3df13e2015-08-25 02:20:09 +0200524 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti033f17c2015-08-30 21:28:04 +0200525 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200526 eol = modes.index(self.eol) + 1
527 if eol >= len(modes):
528 eol = 0
529 self.eol = modes[eol]
530 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
531 self.update_transformations()
532 elif c == '\x01': # CTRL+A -> set encoding
Chris Liechti45c6f222017-07-17 23:56:24 +0200533 self.change_encoding()
Chris Liechti7af7c752015-08-12 15:45:19 +0200534 elif c == '\x09': # CTRL+I -> info
535 self.dump_port_settings()
536 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
537 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
538 elif c in 'pP': # P -> change port
Chris Liechti45c6f222017-07-17 23:56:24 +0200539 self.change_port()
Chris Liechtia73b96b2017-07-13 23:32:24 +0200540 elif c in 'sS': # S -> suspend / open port temporarily
Chris Liechti45c6f222017-07-17 23:56:24 +0200541 self.suspend_port()
Chris Liechti7af7c752015-08-12 15:45:19 +0200542 elif c in 'bB': # B -> change baudrate
Chris Liechti45c6f222017-07-17 23:56:24 +0200543 self.change_baudrate()
Chris Liechti7af7c752015-08-12 15:45:19 +0200544 elif c == '8': # 8 -> change to 8 bits
545 self.serial.bytesize = serial.EIGHTBITS
546 self.dump_port_settings()
547 elif c == '7': # 7 -> change to 8 bits
548 self.serial.bytesize = serial.SEVENBITS
549 self.dump_port_settings()
550 elif c in 'eE': # E -> change to even parity
551 self.serial.parity = serial.PARITY_EVEN
552 self.dump_port_settings()
553 elif c in 'oO': # O -> change to odd parity
554 self.serial.parity = serial.PARITY_ODD
555 self.dump_port_settings()
556 elif c in 'mM': # M -> change to mark parity
557 self.serial.parity = serial.PARITY_MARK
558 self.dump_port_settings()
559 elif c in 'sS': # S -> change to space parity
560 self.serial.parity = serial.PARITY_SPACE
561 self.dump_port_settings()
562 elif c in 'nN': # N -> change to no parity
563 self.serial.parity = serial.PARITY_NONE
564 self.dump_port_settings()
565 elif c == '1': # 1 -> change to 1 stop bits
566 self.serial.stopbits = serial.STOPBITS_ONE
567 self.dump_port_settings()
568 elif c == '2': # 2 -> change to 2 stop bits
569 self.serial.stopbits = serial.STOPBITS_TWO
570 self.dump_port_settings()
571 elif c == '3': # 3 -> change to 1.5 stop bits
572 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
573 self.dump_port_settings()
574 elif c in 'xX': # X -> change software flow control
575 self.serial.xonxoff = (c == 'X')
576 self.dump_port_settings()
577 elif c in 'rR': # R -> change hardware flow control
578 self.serial.rtscts = (c == 'R')
579 self.dump_port_settings()
580 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200581 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
582
Chris Liechti45c6f222017-07-17 23:56:24 +0200583 def upload_file(self):
584 """Ask user for filenname and send its contents"""
585 sys.stderr.write('\n--- File to upload: ')
586 sys.stderr.flush()
587 with self.console:
588 filename = sys.stdin.readline().rstrip('\r\n')
589 if filename:
590 try:
591 with open(filename, 'rb') as f:
592 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
593 while True:
594 block = f.read(1024)
595 if not block:
596 break
597 self.serial.write(block)
598 # Wait for output buffer to drain.
599 self.serial.flush()
600 sys.stderr.write('.') # Progress indicator.
601 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
602 except IOError as e:
603 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
604
605 def change_filter(self):
606 """change the i/o transformations"""
607 sys.stderr.write('\n--- Available Filters:\n')
608 sys.stderr.write('\n'.join(
609 '--- {:<10} = {.__doc__}'.format(k, v)
610 for k, v in sorted(TRANSFORMATIONS.items())))
611 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
612 with self.console:
613 new_filters = sys.stdin.readline().lower().split()
614 if new_filters:
615 for f in new_filters:
616 if f not in TRANSFORMATIONS:
Chris Liechtifac1c132017-08-27 23:35:55 +0200617 sys.stderr.write('--- unknown filter: {!r}\n'.format(f))
Chris Liechti45c6f222017-07-17 23:56:24 +0200618 break
619 else:
620 self.filters = new_filters
621 self.update_transformations()
622 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
623
624 def change_encoding(self):
625 """change encoding on the serial port"""
626 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
627 with self.console:
628 new_encoding = sys.stdin.readline().strip()
629 if new_encoding:
630 try:
631 codecs.lookup(new_encoding)
632 except LookupError:
633 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
634 else:
635 self.set_rx_encoding(new_encoding)
636 self.set_tx_encoding(new_encoding)
637 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
638 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
639
640 def change_baudrate(self):
641 """change the baudrate"""
642 sys.stderr.write('\n--- Baudrate: ')
643 sys.stderr.flush()
644 with self.console:
645 backup = self.serial.baudrate
646 try:
647 self.serial.baudrate = int(sys.stdin.readline().strip())
648 except ValueError as e:
649 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
650 self.serial.baudrate = backup
651 else:
652 self.dump_port_settings()
653
654 def change_port(self):
655 """Have a conversation with the user to change the serial port"""
656 with self.console:
657 try:
658 port = ask_for_port()
659 except KeyboardInterrupt:
660 port = None
661 if port and port != self.serial.port:
662 # reader thread needs to be shut down
663 self._stop_reader()
664 # save settings
665 settings = self.serial.getSettingsDict()
666 try:
667 new_serial = serial.serial_for_url(port, do_not_open=True)
668 # restore settings and open
669 new_serial.applySettingsDict(settings)
670 new_serial.rts = self.serial.rts
671 new_serial.dtr = self.serial.dtr
672 new_serial.open()
673 new_serial.break_condition = self.serial.break_condition
674 except Exception as e:
675 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
676 new_serial.close()
677 else:
678 self.serial.close()
679 self.serial = new_serial
680 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
681 # and restart the reader thread
682 self._start_reader()
683
684 def suspend_port(self):
685 """\
686 open port temporarily, allow reconnect, exit and port change to get
687 out of the loop
688 """
689 # reader thread needs to be shut down
690 self._stop_reader()
691 self.serial.close()
692 sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
693 do_change_port = False
694 while not self.serial.is_open:
695 sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
696 exit=key_description(self.exit_character)))
697 k = self.console.getkey()
698 if k == self.exit_character:
699 self.stop() # exit app
700 break
701 elif k in 'pP':
702 do_change_port = True
703 break
704 try:
705 self.serial.open()
706 except Exception as e:
707 sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
708 if do_change_port:
709 self.change_port()
710 else:
711 # and restart the reader thread
712 self._start_reader()
713 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
714
Chris Liechti442bf512015-08-15 01:42:24 +0200715 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100716 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200717 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200718 return """
719--- pySerial ({version}) - miniterm - help
720---
721--- {exit:8} Exit program
722--- {menu:8} Menu escape key, followed by:
723--- Menu keys:
724--- {menu:7} Send the menu character itself to remote
725--- {exit:7} Send the exit character itself to remote
726--- {info:7} Show info
727--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200728--- {repr:7} encoding
729--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200730--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200731--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
732--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200733---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200734--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200735--- p change port
736--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200737--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200738--- 1 2 3 set stop bits (1, 2, 1.5)
739--- b change baud rate
740--- x X disable/enable software flow control
741--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100742""".format(version=getattr(serial, 'VERSION', 'unknown version'),
743 exit=key_description(self.exit_character),
744 menu=key_description(self.menu_character),
745 rts=key_description('\x12'),
746 dtr=key_description('\x04'),
747 brk=key_description('\x02'),
748 echo=key_description('\x05'),
749 info=key_description('\x09'),
750 upload=key_description('\x15'),
751 repr=key_description('\x01'),
752 filter=key_description('\x06'),
753 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200754
755
Chris Liechtib3df13e2015-08-25 02:20:09 +0200756# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200757# default args can be used to override when calling main() from an other script
758# e.g to create a miniterm-my-device.py
759def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100760 """Command line tool, entry point"""
761
Chris Liechtib7550bd2015-08-15 04:09:10 +0200762 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000763
Chris Liechtib7550bd2015-08-15 04:09:10 +0200764 parser = argparse.ArgumentParser(
Chris Liechti397cf412016-02-11 00:11:48 +0100765 description="Miniterm - A simple terminal program for the serial port.")
cliechti6385f2c2005-09-21 19:51:19 +0000766
Chris Liechti033f17c2015-08-30 21:28:04 +0200767 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100768 "port",
769 nargs='?',
770 help="serial port name ('-' to show port list)",
771 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000772
Chris Liechti033f17c2015-08-30 21:28:04 +0200773 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100774 "baudrate",
775 nargs='?',
776 type=int,
777 help="set baud rate, default: %(default)s",
778 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000779
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200780 group = parser.add_argument_group("port settings")
cliechti53edb472009-02-06 21:18:46 +0000781
Chris Liechti033f17c2015-08-30 21:28:04 +0200782 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100783 "--parity",
784 choices=['N', 'E', 'O', 'S', 'M'],
785 type=lambda c: c.upper(),
786 help="set parity, one of {N E O S M}, default: N",
787 default='N')
cliechti53edb472009-02-06 21:18:46 +0000788
Chris Liechti033f17c2015-08-30 21:28:04 +0200789 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100790 "--rtscts",
791 action="store_true",
792 help="enable RTS/CTS flow control (default off)",
793 default=False)
cliechti53edb472009-02-06 21:18:46 +0000794
Chris Liechti033f17c2015-08-30 21:28:04 +0200795 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100796 "--xonxoff",
797 action="store_true",
798 help="enable software flow control (default off)",
799 default=False)
cliechti53edb472009-02-06 21:18:46 +0000800
Chris Liechti033f17c2015-08-30 21:28:04 +0200801 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100802 "--rts",
803 type=int,
804 help="set initial RTS line state (possible values: 0, 1)",
805 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000806
Chris Liechti033f17c2015-08-30 21:28:04 +0200807 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100808 "--dtr",
809 type=int,
810 help="set initial DTR line state (possible values: 0, 1)",
811 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000812
Chris Liechti00f84282015-12-24 23:40:34 +0100813 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100814 "--ask",
815 action="store_true",
816 help="ask again for port when open fails",
817 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100818
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200819 group = parser.add_argument_group("data handling")
cliechti5370cee2013-10-13 03:08:19 +0000820
Chris Liechti033f17c2015-08-30 21:28:04 +0200821 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100822 "-e", "--echo",
823 action="store_true",
824 help="enable local echo (default off)",
825 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000826
Chris Liechti033f17c2015-08-30 21:28:04 +0200827 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100828 "--encoding",
829 dest="serial_port_encoding",
830 metavar="CODEC",
831 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
832 default='UTF-8')
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 "-f", "--filter",
836 action="append",
837 metavar="NAME",
838 help="add text transformation",
839 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200840
Chris Liechti033f17c2015-08-30 21:28:04 +0200841 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100842 "--eol",
843 choices=['CR', 'LF', 'CRLF'],
844 type=lambda c: c.upper(),
845 help="end of line mode",
846 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000847
Chris Liechti033f17c2015-08-30 21:28:04 +0200848 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100849 "--raw",
850 action="store_true",
851 help="Do no apply any encodings/transformations",
852 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000853
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200854 group = parser.add_argument_group("hotkeys")
cliechtib7d746d2006-03-28 22:44:30 +0000855
Chris Liechti033f17c2015-08-30 21:28:04 +0200856 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100857 "--exit-char",
858 type=int,
859 metavar='NUM',
860 help="Unicode of special character that is used to exit the application, default: %(default)s",
861 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000862
Chris Liechti033f17c2015-08-30 21:28:04 +0200863 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100864 "--menu-char",
865 type=int,
866 metavar='NUM',
867 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
868 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000869
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200870 group = parser.add_argument_group("diagnostics")
cliechti6385f2c2005-09-21 19:51:19 +0000871
Chris Liechti033f17c2015-08-30 21:28:04 +0200872 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100873 "-q", "--quiet",
874 action="store_true",
875 help="suppress non-error messages",
876 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000877
Chris Liechti033f17c2015-08-30 21:28:04 +0200878 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100879 "--develop",
880 action="store_true",
881 help="show Python traceback on error",
882 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000883
Chris Liechtib7550bd2015-08-15 04:09:10 +0200884 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000885
Chris Liechtib7550bd2015-08-15 04:09:10 +0200886 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000887 parser.error('--exit-char can not be the same as --menu-char')
888
Chris Liechtib3df13e2015-08-25 02:20:09 +0200889 if args.filter:
890 if 'help' in args.filter:
891 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200892 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100893 '{:<10} = {.__doc__}'.format(k, v)
894 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200895 sys.stderr.write('\n')
896 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200897 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200898 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200899 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200900
Chris Liechti00f84282015-12-24 23:40:34 +0100901 while True:
902 # no port given on command line -> ask user now
903 if args.port is None or args.port == '-':
904 try:
905 args.port = ask_for_port()
906 except KeyboardInterrupt:
907 sys.stderr.write('\n')
908 parser.error('user aborted and port is not given')
909 else:
910 if not args.port:
911 parser.error('port is not given')
912 try:
913 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100914 args.port,
915 args.baudrate,
916 parity=args.parity,
917 rtscts=args.rtscts,
918 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100919 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200920
Chris Liechtif542fca2016-05-13 00:20:14 +0200921 if not hasattr(serial_instance, 'cancel_read'):
922 # enable timeout for alive flag polling if cancel_read is not available
923 serial_instance.timeout = 1
924
Chris Liechti00f84282015-12-24 23:40:34 +0100925 if args.dtr is not None:
926 if not args.quiet:
927 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
928 serial_instance.dtr = args.dtr
929 if args.rts is not None:
930 if not args.quiet:
931 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
932 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200933
Chris Liechti00f84282015-12-24 23:40:34 +0100934 serial_instance.open()
935 except serial.SerialException as e:
Chris Liechtifac1c132017-08-27 23:35:55 +0200936 sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e))
Chris Liechti00f84282015-12-24 23:40:34 +0100937 if args.develop:
938 raise
939 if not args.ask:
940 sys.exit(1)
941 else:
942 args.port = '-'
943 else:
944 break
cliechti6385f2c2005-09-21 19:51:19 +0000945
Chris Liechti3b454802015-08-26 23:39:59 +0200946 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100947 serial_instance,
948 echo=args.echo,
949 eol=args.eol.lower(),
950 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200951 miniterm.exit_character = unichr(args.exit_char)
952 miniterm.menu_character = unichr(args.menu_char)
953 miniterm.raw = args.raw
954 miniterm.set_rx_encoding(args.serial_port_encoding)
955 miniterm.set_tx_encoding(args.serial_port_encoding)
956
Chris Liechtib7550bd2015-08-15 04:09:10 +0200957 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200958 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100959 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200960 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100961 key_description(miniterm.exit_character),
962 key_description(miniterm.menu_character),
963 key_description(miniterm.menu_character),
964 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +0000965
cliechti6385f2c2005-09-21 19:51:19 +0000966 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +0000967 try:
968 miniterm.join(True)
969 except KeyboardInterrupt:
970 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +0200971 if not args.quiet:
cliechtibf6bb7d2006-03-30 00:28:18 +0000972 sys.stderr.write("\n--- exit ---\n")
cliechti6385f2c2005-09-21 19:51:19 +0000973 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +0200974 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +0000975
cliechti5370cee2013-10-13 03:08:19 +0000976# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +0000977if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +0000978 main()