blob: 83d8dbda70b5b5ad4bb70917fb99ca44927c5a9c [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 Liechti49f19932017-08-30 17:55:39 +02006# (C)2002-2017 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
Cefn Hoiled64fb602018-06-08 09:46:28 +010091 import ctypes.wintypes as wintypes
92 import platform
Chris Liechti9cc696b2015-08-28 00:54:22 +020093
94 class Out(object):
Chris Liechtia887c932016-02-13 23:10:14 +010095 """file-like wrapper that uses os.write"""
96
Chris Liechti9cc696b2015-08-28 00:54:22 +020097 def __init__(self, fd):
98 self.fd = fd
99
100 def flush(self):
101 pass
102
103 def write(self, s):
104 os.write(self.fd, s)
105
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200106 class Console(ConsoleBase):
Cefn Hoiled64fb602018-06-08 09:46:28 +0100107 nav = {
108 'H': '\x1b[A', # UP
109 'P': '\x1b[B', # DOWN
110 'K': '\x1b[D', # LEFT
111 'M': '\x1b[C', # RIGHT
112 'G': '\x1b[H', # HOME
113 'O': '\x1b[F', # END
114 }
115
Chris Liechticbb00b22015-08-13 22:58:49 +0200116 def __init__(self):
117 super(Console, self).__init__()
Cefn Hoiled64fb602018-06-08 09:46:28 +0100118 if not hasattr(wintypes, 'LPDWORD'): # PY2
119 wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
120 self._saved_cm = mode = wintypes.DWORD()
121 ctypes.windll.kernel32.GetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), ctypes.byref(self._saved_cm))
Chris Liechti1df28272015-08-27 23:37:38 +0200122 self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
123 self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
Chris Liechticbb00b22015-08-13 22:58:49 +0200124 ctypes.windll.kernel32.SetConsoleOutputCP(65001)
125 ctypes.windll.kernel32.SetConsoleCP(65001)
Cefn Hoiled64fb602018-06-08 09:46:28 +0100126 # ANSI handling available through SetConsoleMode since v1511
127 if platform.release() == '10' and int(platform.version().split('.')[2]) > 10586:
128 ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), 0x7)
Chris Liechti9cc696b2015-08-28 00:54:22 +0200129 self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
130 # the change of the code page is not propagated to Python, manually fix it
131 sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
132 sys.stdout = self.output
Chris Liechti168704f2015-09-30 16:50:29 +0200133 self.output.encoding = 'UTF-8' # needed for input
Chris Liechticbb00b22015-08-13 22:58:49 +0200134
Chris Liechti1df28272015-08-27 23:37:38 +0200135 def __del__(self):
136 ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
137 ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
Cefn Hoiled64fb602018-06-08 09:46:28 +0100138 ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), self._saved_cm)
Chris Liechti1df28272015-08-27 23:37:38 +0200139
cliechti3a8bf092008-09-17 11:26:53 +0000140 def getkey(self):
cliechti91165532011-03-18 02:02:52 +0000141 while True:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200142 z = msvcrt.getwch()
Chris Liechti9f398812015-09-13 18:50:44 +0200143 if z == unichr(13):
144 return unichr(10)
Cefn Hoiled64fb602018-06-08 09:46:28 +0100145 elif z in unichr(0): # functions keys, ignore
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200146 msvcrt.getwch()
Cefn Hoiled64fb602018-06-08 09:46:28 +0100147 elif z in unichr(0xe0): # map special keys
148 code = msvcrt.getwch()
149 try:
150 return self.nav[code]
151 except KeyError:
152 pass
cliechti9c592b32008-06-16 22:00:14 +0000153 else:
cliechti9c592b32008-06-16 22:00:14 +0000154 return z
cliechti53edb472009-02-06 21:18:46 +0000155
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200156 def cancel(self):
Chris Liechtic20c3732016-05-14 02:25:13 +0200157 # CancelIo, CancelSynchronousIo do not seem to work when using
158 # getwch, so instead, send a key to the window with the console
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200159 hwnd = ctypes.windll.kernel32.GetConsoleWindow()
160 ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
161
cliechti576de252002-02-28 23:54:44 +0000162elif os.name == 'posix':
Chris Liechtia1d5c6d2015-08-07 14:41:24 +0200163 import atexit
164 import termios
Chris Liechticab3dab2016-12-07 01:27:41 +0100165 import fcntl
Chris Liechti9cc696b2015-08-28 00:54:22 +0200166
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200167 class Console(ConsoleBase):
cliechti9c592b32008-06-16 22:00:14 +0000168 def __init__(self):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200169 super(Console, self).__init__()
cliechti9c592b32008-06-16 22:00:14 +0000170 self.fd = sys.stdin.fileno()
Chris Liechti4d989c22015-08-24 00:24:49 +0200171 self.old = termios.tcgetattr(self.fd)
Chris Liechti89eb2472015-08-08 17:06:25 +0200172 atexit.register(self.cleanup)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200173 if sys.version_info < (3, 0):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200174 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
175 else:
176 self.enc_stdin = sys.stdin
cliechti9c592b32008-06-16 22:00:14 +0000177
178 def setup(self):
cliechti9c592b32008-06-16 22:00:14 +0000179 new = termios.tcgetattr(self.fd)
180 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
181 new[6][termios.VMIN] = 1
182 new[6][termios.VTIME] = 0
183 termios.tcsetattr(self.fd, termios.TCSANOW, new)
cliechti53edb472009-02-06 21:18:46 +0000184
cliechti9c592b32008-06-16 22:00:14 +0000185 def getkey(self):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200186 c = self.enc_stdin.read(1)
Chris Liechti9f398812015-09-13 18:50:44 +0200187 if c == unichr(0x7f):
188 c = unichr(8) # map the BS key (which yields DEL) to backspace
Chris Liechti9a720852015-08-25 00:20:38 +0200189 return c
cliechti53edb472009-02-06 21:18:46 +0000190
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200191 def cancel(self):
Chris Liechticab3dab2016-12-07 01:27:41 +0100192 fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200193
cliechti9c592b32008-06-16 22:00:14 +0000194 def cleanup(self):
Chris Liechti4d989c22015-08-24 00:24:49 +0200195 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
cliechti9c592b32008-06-16 22:00:14 +0000196
cliechti576de252002-02-28 23:54:44 +0000197else:
Chris Liechti397cf412016-02-11 00:11:48 +0100198 raise NotImplementedError(
199 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
cliechti576de252002-02-28 23:54:44 +0000200
cliechti6fa76fb2009-07-08 23:53:39 +0000201
Chris Liechti9a720852015-08-25 00:20:38 +0200202# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200203
204class Transform(object):
Chris Liechticbb00b22015-08-13 22:58:49 +0200205 """do-nothing: forward all data unchanged"""
Chris Liechtid698af72015-08-24 20:24:55 +0200206 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200207 """text received from serial port"""
208 return text
209
Chris Liechtid698af72015-08-24 20:24:55 +0200210 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200211 """text to be sent to serial port"""
212 return text
213
214 def echo(self, text):
215 """text to be sent but displayed on console"""
216 return text
217
Chris Liechti442bf512015-08-15 01:42:24 +0200218
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200219class CRLF(Transform):
220 """ENTER sends CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200221
Chris Liechtid698af72015-08-24 20:24:55 +0200222 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200223 return text.replace('\n', '\r\n')
224
Chris Liechti442bf512015-08-15 01:42:24 +0200225
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200226class CR(Transform):
227 """ENTER sends CR"""
Chris Liechtid698af72015-08-24 20:24:55 +0200228
229 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200230 return text.replace('\r', '\n')
231
Chris Liechtid698af72015-08-24 20:24:55 +0200232 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200233 return text.replace('\n', '\r')
234
Chris Liechti442bf512015-08-15 01:42:24 +0200235
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200236class LF(Transform):
237 """ENTER sends LF"""
238
239
240class NoTerminal(Transform):
241 """remove typical terminal control codes from input"""
Chris Liechti9a720852015-08-25 00:20:38 +0200242
243 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 +0100244 REPLACEMENT_MAP.update(
245 {
Chris Liechti033f17c2015-08-30 21:28:04 +0200246 0x7F: 0x2421, # DEL
247 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100248 })
Chris Liechti9a720852015-08-25 00:20:38 +0200249
Chris Liechtid698af72015-08-24 20:24:55 +0200250 def rx(self, text):
Chris Liechti9a720852015-08-25 00:20:38 +0200251 return text.translate(self.REPLACEMENT_MAP)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200252
Chris Liechtid698af72015-08-24 20:24:55 +0200253 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200254
255
Chris Liechti9a720852015-08-25 00:20:38 +0200256class NoControls(NoTerminal):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200257 """Remove all control codes, incl. CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200258
Chris Liechti9a720852015-08-25 00:20:38 +0200259 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
Chris Liechtiba45c522016-02-06 23:53:23 +0100260 REPLACEMENT_MAP.update(
261 {
Chris Liechtia887c932016-02-13 23:10:14 +0100262 0x20: 0x2423, # visual space
Chris Liechti033f17c2015-08-30 21:28:04 +0200263 0x7F: 0x2421, # DEL
264 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100265 })
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200266
267
268class Printable(Transform):
Chris Liechtid698af72015-08-24 20:24:55 +0200269 """Show decimal code for all non-ASCII characters and replace most control codes"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200270
Chris Liechtid698af72015-08-24 20:24:55 +0200271 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200272 r = []
Chris Liechtia887c932016-02-13 23:10:14 +0100273 for c in text:
274 if ' ' <= c < '\x7f' or c in '\r\n\b\t':
275 r.append(c)
276 elif c < ' ':
277 r.append(unichr(0x2400 + ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200278 else:
Chris Liechtia887c932016-02-13 23:10:14 +0100279 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200280 r.append(' ')
281 return ''.join(r)
282
Chris Liechtid698af72015-08-24 20:24:55 +0200283 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200284
285
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200286class Colorize(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200287 """Apply different colors for received and echo"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200288
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200289 def __init__(self):
290 # XXX make it configurable, use colorama?
291 self.input_color = '\x1b[37m'
292 self.echo_color = '\x1b[31m'
293
Chris Liechtid698af72015-08-24 20:24:55 +0200294 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200295 return self.input_color + text
296
297 def echo(self, text):
298 return self.echo_color + text
299
Chris Liechti442bf512015-08-15 01:42:24 +0200300
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200301class DebugIO(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200302 """Print what is sent and received"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200303
Chris Liechtid698af72015-08-24 20:24:55 +0200304 def rx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200305 sys.stderr.write(' [RX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200306 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200307 return text
308
Chris Liechtid698af72015-08-24 20:24:55 +0200309 def tx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200310 sys.stderr.write(' [TX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200311 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200312 return text
313
Chris Liechti442bf512015-08-15 01:42:24 +0200314
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200315# other ideas:
316# - add date/time for each newline
317# - insert newline after: a) timeout b) packet end character
318
Chris Liechtib3df13e2015-08-25 02:20:09 +0200319EOL_TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100320 'crlf': CRLF,
321 'cr': CR,
322 'lf': LF,
323}
Chris Liechtib3df13e2015-08-25 02:20:09 +0200324
325TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100326 'direct': Transform, # no transformation
327 'default': NoTerminal,
328 'nocontrol': NoControls,
329 'printable': Printable,
330 'colorize': Colorize,
331 'debug': DebugIO,
332}
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200333
334
Chris Liechti033f17c2015-08-30 21:28:04 +0200335# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti89313c92015-09-01 02:33:13 +0200336def ask_for_port():
337 """\
338 Show a list of ports and ask the user for a choice. To make selection
339 easier on systems with long device names, also allow the input of an
340 index.
341 """
342 sys.stderr.write('\n--- Available ports:\n')
343 ports = []
344 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
Chris Liechti8b0eaf22017-07-19 22:59:57 +0200345 sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
Chris Liechti89313c92015-09-01 02:33:13 +0200346 ports.append(port)
347 while True:
348 port = raw_input('--- Enter port index or full name: ')
349 try:
350 index = int(port) - 1
351 if not 0 <= index < len(ports):
352 sys.stderr.write('--- Invalid index!\n')
353 continue
354 except ValueError:
355 pass
356 else:
357 port = ports[index]
358 return port
cliechti1351dde2012-04-12 16:47:47 +0000359
360
cliechti8c2ea842011-03-18 01:51:46 +0000361class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200362 """\
363 Terminal application. Copy data from serial port to console and vice versa.
364 Handle special keys from the console to show menu etc.
365 """
366
Chris Liechti3b454802015-08-26 23:39:59 +0200367 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200368 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200369 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000370 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200371 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200372 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200373 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200374 self.eol = eol
375 self.filters = filters
376 self.update_transformations()
Carlos52bfe352018-03-16 13:27:19 +0000377 self.exit_character = unichr(0x1d) # GS/CTRL+]
378 self.menu_character = unichr(0x14) # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100379 self.alive = None
380 self._reader_alive = None
381 self.receiver_thread = None
382 self.rx_decoder = None
383 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000384
cliechti8c2ea842011-03-18 01:51:46 +0000385 def _start_reader(self):
386 """Start reader thread"""
387 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000388 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200389 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
390 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000391 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000392
393 def _stop_reader(self):
394 """Stop reader thread only, wait for clean exit of thread"""
395 self._reader_alive = False
Chris Liechti933a5172016-05-04 16:12:15 +0200396 if hasattr(self.serial, 'cancel_read'):
397 self.serial.cancel_read()
cliechti8c2ea842011-03-18 01:51:46 +0000398 self.receiver_thread.join()
399
cliechti8c2ea842011-03-18 01:51:46 +0000400 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100401 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000402 self.alive = True
403 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000404 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200405 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
406 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000407 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200408 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000409
cliechti6385f2c2005-09-21 19:51:19 +0000410 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100411 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000412 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000413
cliechtibf6bb7d2006-03-30 00:28:18 +0000414 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100415 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000416 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000417 if not transmit_only:
Chris Liechti933a5172016-05-04 16:12:15 +0200418 if hasattr(self.serial, 'cancel_read'):
419 self.serial.cancel_read()
cliechtibf6bb7d2006-03-30 00:28:18 +0000420 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000421
Chris Liechti933a5172016-05-04 16:12:15 +0200422 def close(self):
423 self.serial.close()
424
Chris Liechtib3df13e2015-08-25 02:20:09 +0200425 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100426 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100427 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
428 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200429 self.tx_transformations = [t() for t in transformations]
430 self.rx_transformations = list(reversed(self.tx_transformations))
431
Chris Liechtid698af72015-08-24 20:24:55 +0200432 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100433 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200434 self.input_encoding = encoding
435 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
436
437 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100438 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200439 self.output_encoding = encoding
440 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
441
cliechti6c8eb2f2009-07-08 02:10:46 +0000442 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100443 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200444 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100445 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200446 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100447 ('active' if self.serial.rts else 'inactive'),
448 ('active' if self.serial.dtr else 'inactive'),
449 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000450 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200451 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100452 ('active' if self.serial.cts else 'inactive'),
453 ('active' if self.serial.dsr else 'inactive'),
454 ('active' if self.serial.ri else 'inactive'),
455 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000456 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200457 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000458 # yet received. ignore this error.
459 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200460 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
461 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200462 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
463 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200464 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
465 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000466
cliechti6385f2c2005-09-21 19:51:19 +0000467 def reader(self):
468 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000469 try:
cliechti8c2ea842011-03-18 01:51:46 +0000470 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200471 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200472 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200473 if data:
474 if self.raw:
475 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000476 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200477 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200478 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200479 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200480 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200481 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000482 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200483 self.console.cancel()
484 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000485
cliechti6385f2c2005-09-21 19:51:19 +0000486 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000487 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200488 Loop and copy console->serial until self.exit_character character is
489 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000490 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000491 """
492 menu_active = False
493 try:
494 while self.alive:
495 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200496 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000497 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200498 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200499 if not self.alive:
500 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000501 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200502 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000503 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200504 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200505 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200506 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200507 self.stop() # exit app
508 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000509 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200510 #~ if self.raw:
511 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200512 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200513 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200514 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000515 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200516 echo_text = c
517 for transformation in self.tx_transformations:
518 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200519 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000520 except:
521 self.alive = False
522 raise
cliechti6385f2c2005-09-21 19:51:19 +0000523
Chris Liechti7af7c752015-08-12 15:45:19 +0200524 def handle_menu_key(self, c):
525 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200526 if c == self.menu_character or c == self.exit_character:
527 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200528 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200529 if self.echo:
530 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200531 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti45c6f222017-07-17 23:56:24 +0200532 self.upload_file()
Chris Liechti7af7c752015-08-12 15:45:19 +0200533 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200534 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200535 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200536 self.serial.rts = not self.serial.rts
537 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200538 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200539 self.serial.dtr = not self.serial.dtr
540 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200541 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200542 self.serial.break_condition = not self.serial.break_condition
543 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200544 elif c == '\x05': # CTRL+E -> toggle local echo
545 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200546 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200547 elif c == '\x06': # CTRL+F -> edit filters
Chris Liechti45c6f222017-07-17 23:56:24 +0200548 self.change_filter()
Chris Liechtib3df13e2015-08-25 02:20:09 +0200549 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti49f19932017-08-30 17:55:39 +0200550 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200551 eol = modes.index(self.eol) + 1
552 if eol >= len(modes):
553 eol = 0
554 self.eol = modes[eol]
555 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
556 self.update_transformations()
557 elif c == '\x01': # CTRL+A -> set encoding
Chris Liechti45c6f222017-07-17 23:56:24 +0200558 self.change_encoding()
Chris Liechti7af7c752015-08-12 15:45:19 +0200559 elif c == '\x09': # CTRL+I -> info
560 self.dump_port_settings()
561 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
562 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
563 elif c in 'pP': # P -> change port
Chris Liechti45c6f222017-07-17 23:56:24 +0200564 self.change_port()
Chris Liechtia73b96b2017-07-13 23:32:24 +0200565 elif c in 'sS': # S -> suspend / open port temporarily
Chris Liechti45c6f222017-07-17 23:56:24 +0200566 self.suspend_port()
Chris Liechti7af7c752015-08-12 15:45:19 +0200567 elif c in 'bB': # B -> change baudrate
Chris Liechti45c6f222017-07-17 23:56:24 +0200568 self.change_baudrate()
Chris Liechti7af7c752015-08-12 15:45:19 +0200569 elif c == '8': # 8 -> change to 8 bits
570 self.serial.bytesize = serial.EIGHTBITS
571 self.dump_port_settings()
572 elif c == '7': # 7 -> change to 8 bits
573 self.serial.bytesize = serial.SEVENBITS
574 self.dump_port_settings()
575 elif c in 'eE': # E -> change to even parity
576 self.serial.parity = serial.PARITY_EVEN
577 self.dump_port_settings()
578 elif c in 'oO': # O -> change to odd parity
579 self.serial.parity = serial.PARITY_ODD
580 self.dump_port_settings()
581 elif c in 'mM': # M -> change to mark parity
582 self.serial.parity = serial.PARITY_MARK
583 self.dump_port_settings()
584 elif c in 'sS': # S -> change to space parity
585 self.serial.parity = serial.PARITY_SPACE
586 self.dump_port_settings()
587 elif c in 'nN': # N -> change to no parity
588 self.serial.parity = serial.PARITY_NONE
589 self.dump_port_settings()
590 elif c == '1': # 1 -> change to 1 stop bits
591 self.serial.stopbits = serial.STOPBITS_ONE
592 self.dump_port_settings()
593 elif c == '2': # 2 -> change to 2 stop bits
594 self.serial.stopbits = serial.STOPBITS_TWO
595 self.dump_port_settings()
596 elif c == '3': # 3 -> change to 1.5 stop bits
597 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
598 self.dump_port_settings()
599 elif c in 'xX': # X -> change software flow control
600 self.serial.xonxoff = (c == 'X')
601 self.dump_port_settings()
602 elif c in 'rR': # R -> change hardware flow control
603 self.serial.rtscts = (c == 'R')
604 self.dump_port_settings()
605 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200606 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
607
Chris Liechti45c6f222017-07-17 23:56:24 +0200608 def upload_file(self):
609 """Ask user for filenname and send its contents"""
610 sys.stderr.write('\n--- File to upload: ')
611 sys.stderr.flush()
612 with self.console:
613 filename = sys.stdin.readline().rstrip('\r\n')
614 if filename:
615 try:
616 with open(filename, 'rb') as f:
617 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
618 while True:
619 block = f.read(1024)
620 if not block:
621 break
622 self.serial.write(block)
623 # Wait for output buffer to drain.
624 self.serial.flush()
625 sys.stderr.write('.') # Progress indicator.
626 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
627 except IOError as e:
628 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
629
630 def change_filter(self):
631 """change the i/o transformations"""
632 sys.stderr.write('\n--- Available Filters:\n')
633 sys.stderr.write('\n'.join(
634 '--- {:<10} = {.__doc__}'.format(k, v)
635 for k, v in sorted(TRANSFORMATIONS.items())))
636 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
637 with self.console:
638 new_filters = sys.stdin.readline().lower().split()
639 if new_filters:
640 for f in new_filters:
641 if f not in TRANSFORMATIONS:
Chris Liechtifac1c132017-08-27 23:35:55 +0200642 sys.stderr.write('--- unknown filter: {!r}\n'.format(f))
Chris Liechti45c6f222017-07-17 23:56:24 +0200643 break
644 else:
645 self.filters = new_filters
646 self.update_transformations()
647 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
648
649 def change_encoding(self):
650 """change encoding on the serial port"""
651 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
652 with self.console:
653 new_encoding = sys.stdin.readline().strip()
654 if new_encoding:
655 try:
656 codecs.lookup(new_encoding)
657 except LookupError:
658 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
659 else:
660 self.set_rx_encoding(new_encoding)
661 self.set_tx_encoding(new_encoding)
662 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
663 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
664
665 def change_baudrate(self):
666 """change the baudrate"""
667 sys.stderr.write('\n--- Baudrate: ')
668 sys.stderr.flush()
669 with self.console:
670 backup = self.serial.baudrate
671 try:
672 self.serial.baudrate = int(sys.stdin.readline().strip())
673 except ValueError as e:
674 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
675 self.serial.baudrate = backup
676 else:
677 self.dump_port_settings()
678
679 def change_port(self):
680 """Have a conversation with the user to change the serial port"""
681 with self.console:
682 try:
683 port = ask_for_port()
684 except KeyboardInterrupt:
685 port = None
686 if port and port != self.serial.port:
687 # reader thread needs to be shut down
688 self._stop_reader()
689 # save settings
690 settings = self.serial.getSettingsDict()
691 try:
692 new_serial = serial.serial_for_url(port, do_not_open=True)
693 # restore settings and open
694 new_serial.applySettingsDict(settings)
695 new_serial.rts = self.serial.rts
696 new_serial.dtr = self.serial.dtr
697 new_serial.open()
698 new_serial.break_condition = self.serial.break_condition
699 except Exception as e:
700 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
701 new_serial.close()
702 else:
703 self.serial.close()
704 self.serial = new_serial
705 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
706 # and restart the reader thread
707 self._start_reader()
708
709 def suspend_port(self):
710 """\
711 open port temporarily, allow reconnect, exit and port change to get
712 out of the loop
713 """
714 # reader thread needs to be shut down
715 self._stop_reader()
716 self.serial.close()
717 sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
718 do_change_port = False
719 while not self.serial.is_open:
720 sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
721 exit=key_description(self.exit_character)))
722 k = self.console.getkey()
723 if k == self.exit_character:
724 self.stop() # exit app
725 break
726 elif k in 'pP':
727 do_change_port = True
728 break
729 try:
730 self.serial.open()
731 except Exception as e:
732 sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
733 if do_change_port:
734 self.change_port()
735 else:
736 # and restart the reader thread
737 self._start_reader()
738 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
739
Chris Liechti442bf512015-08-15 01:42:24 +0200740 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100741 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200742 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200743 return """
744--- pySerial ({version}) - miniterm - help
745---
746--- {exit:8} Exit program
747--- {menu:8} Menu escape key, followed by:
748--- Menu keys:
749--- {menu:7} Send the menu character itself to remote
750--- {exit:7} Send the exit character itself to remote
751--- {info:7} Show info
752--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200753--- {repr:7} encoding
754--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200755--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200756--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
757--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200758---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200759--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200760--- p change port
761--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200762--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200763--- 1 2 3 set stop bits (1, 2, 1.5)
764--- b change baud rate
765--- x X disable/enable software flow control
766--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100767""".format(version=getattr(serial, 'VERSION', 'unknown version'),
768 exit=key_description(self.exit_character),
769 menu=key_description(self.menu_character),
770 rts=key_description('\x12'),
771 dtr=key_description('\x04'),
772 brk=key_description('\x02'),
773 echo=key_description('\x05'),
774 info=key_description('\x09'),
775 upload=key_description('\x15'),
776 repr=key_description('\x01'),
777 filter=key_description('\x06'),
778 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200779
780
Chris Liechtib3df13e2015-08-25 02:20:09 +0200781# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200782# default args can be used to override when calling main() from an other script
783# e.g to create a miniterm-my-device.py
784def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100785 """Command line tool, entry point"""
786
Chris Liechtib7550bd2015-08-15 04:09:10 +0200787 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000788
Chris Liechtib7550bd2015-08-15 04:09:10 +0200789 parser = argparse.ArgumentParser(
Chris Liechti49f19932017-08-30 17:55:39 +0200790 description='Miniterm - A simple terminal program for the serial port.')
cliechti6385f2c2005-09-21 19:51:19 +0000791
Chris Liechti033f17c2015-08-30 21:28:04 +0200792 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200793 'port',
Chris Liechti397cf412016-02-11 00:11:48 +0100794 nargs='?',
Chris Liechti49f19932017-08-30 17:55:39 +0200795 help='serial port name ("-" to show port list)',
Chris Liechti397cf412016-02-11 00:11:48 +0100796 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000797
Chris Liechti033f17c2015-08-30 21:28:04 +0200798 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200799 'baudrate',
Chris Liechti397cf412016-02-11 00:11:48 +0100800 nargs='?',
801 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200802 help='set baud rate, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100803 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000804
Chris Liechti49f19932017-08-30 17:55:39 +0200805 group = parser.add_argument_group('port settings')
cliechti53edb472009-02-06 21:18:46 +0000806
Chris Liechti033f17c2015-08-30 21:28:04 +0200807 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200808 '--parity',
Chris Liechti397cf412016-02-11 00:11:48 +0100809 choices=['N', 'E', 'O', 'S', 'M'],
810 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200811 help='set parity, one of {N E O S M}, default: N',
Chris Liechti397cf412016-02-11 00:11:48 +0100812 default='N')
cliechti53edb472009-02-06 21:18:46 +0000813
Chris Liechti033f17c2015-08-30 21:28:04 +0200814 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200815 '--rtscts',
816 action='store_true',
817 help='enable RTS/CTS flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100818 default=False)
cliechti53edb472009-02-06 21:18:46 +0000819
Chris Liechti033f17c2015-08-30 21:28:04 +0200820 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200821 '--xonxoff',
822 action='store_true',
823 help='enable software flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100824 default=False)
cliechti53edb472009-02-06 21:18:46 +0000825
Chris Liechti033f17c2015-08-30 21:28:04 +0200826 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200827 '--rts',
Chris Liechti397cf412016-02-11 00:11:48 +0100828 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200829 help='set initial RTS line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100830 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000831
Chris Liechti033f17c2015-08-30 21:28:04 +0200832 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200833 '--dtr',
Chris Liechti397cf412016-02-11 00:11:48 +0100834 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200835 help='set initial DTR line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100836 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000837
Chris Liechti00f84282015-12-24 23:40:34 +0100838 group.add_argument(
zsquarepluscb178d122018-05-07 20:12:59 +0200839 '--non-exclusive',
840 dest='exclusive',
841 action='store_false',
842 help='disable locking for native ports',
Sascha Silbe9c055352018-03-19 20:10:26 +0100843 default=True)
844
845 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200846 '--ask',
847 action='store_true',
848 help='ask again for port when open fails',
Chris Liechti397cf412016-02-11 00:11:48 +0100849 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100850
Chris Liechti49f19932017-08-30 17:55:39 +0200851 group = parser.add_argument_group('data handling')
cliechti5370cee2013-10-13 03:08:19 +0000852
Chris Liechti033f17c2015-08-30 21:28:04 +0200853 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200854 '-e', '--echo',
855 action='store_true',
856 help='enable local echo (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100857 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000858
Chris Liechti033f17c2015-08-30 21:28:04 +0200859 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200860 '--encoding',
861 dest='serial_port_encoding',
862 metavar='CODEC',
863 help='set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100864 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000865
Chris Liechti033f17c2015-08-30 21:28:04 +0200866 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200867 '-f', '--filter',
868 action='append',
869 metavar='NAME',
870 help='add text transformation',
Chris Liechti397cf412016-02-11 00:11:48 +0100871 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200872
Chris Liechti033f17c2015-08-30 21:28:04 +0200873 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200874 '--eol',
Chris Liechti397cf412016-02-11 00:11:48 +0100875 choices=['CR', 'LF', 'CRLF'],
876 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200877 help='end of line mode',
Chris Liechti397cf412016-02-11 00:11:48 +0100878 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000879
Chris Liechti033f17c2015-08-30 21:28:04 +0200880 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200881 '--raw',
882 action='store_true',
883 help='Do no apply any encodings/transformations',
Chris Liechti397cf412016-02-11 00:11:48 +0100884 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000885
Chris Liechti49f19932017-08-30 17:55:39 +0200886 group = parser.add_argument_group('hotkeys')
cliechtib7d746d2006-03-28 22:44:30 +0000887
Chris Liechti033f17c2015-08-30 21:28:04 +0200888 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200889 '--exit-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100890 type=int,
891 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200892 help='Unicode of special character that is used to exit the application, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100893 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000894
Chris Liechti033f17c2015-08-30 21:28:04 +0200895 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200896 '--menu-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100897 type=int,
898 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200899 help='Unicode code of special character that is used to control miniterm (menu), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100900 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000901
Chris Liechti49f19932017-08-30 17:55:39 +0200902 group = parser.add_argument_group('diagnostics')
cliechti6385f2c2005-09-21 19:51:19 +0000903
Chris Liechti033f17c2015-08-30 21:28:04 +0200904 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200905 '-q', '--quiet',
906 action='store_true',
907 help='suppress non-error messages',
Chris Liechti397cf412016-02-11 00:11:48 +0100908 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000909
Chris Liechti033f17c2015-08-30 21:28:04 +0200910 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200911 '--develop',
912 action='store_true',
913 help='show Python traceback on error',
Chris Liechti397cf412016-02-11 00:11:48 +0100914 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000915
Chris Liechtib7550bd2015-08-15 04:09:10 +0200916 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000917
Chris Liechtib7550bd2015-08-15 04:09:10 +0200918 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000919 parser.error('--exit-char can not be the same as --menu-char')
920
Chris Liechtib3df13e2015-08-25 02:20:09 +0200921 if args.filter:
922 if 'help' in args.filter:
923 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200924 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100925 '{:<10} = {.__doc__}'.format(k, v)
926 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200927 sys.stderr.write('\n')
928 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200929 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200930 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200931 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200932
Chris Liechti00f84282015-12-24 23:40:34 +0100933 while True:
934 # no port given on command line -> ask user now
935 if args.port is None or args.port == '-':
936 try:
937 args.port = ask_for_port()
938 except KeyboardInterrupt:
939 sys.stderr.write('\n')
940 parser.error('user aborted and port is not given')
941 else:
942 if not args.port:
943 parser.error('port is not given')
944 try:
945 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100946 args.port,
947 args.baudrate,
948 parity=args.parity,
949 rtscts=args.rtscts,
950 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100951 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200952
Chris Liechtif542fca2016-05-13 00:20:14 +0200953 if not hasattr(serial_instance, 'cancel_read'):
954 # enable timeout for alive flag polling if cancel_read is not available
955 serial_instance.timeout = 1
956
Chris Liechti00f84282015-12-24 23:40:34 +0100957 if args.dtr is not None:
958 if not args.quiet:
959 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
960 serial_instance.dtr = args.dtr
961 if args.rts is not None:
962 if not args.quiet:
963 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
964 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200965
Sascha Silbe9c055352018-03-19 20:10:26 +0100966 if isinstance(serial_instance, serial.Serial):
967 serial_instance.exclusive = args.exclusive
968
Chris Liechti00f84282015-12-24 23:40:34 +0100969 serial_instance.open()
970 except serial.SerialException as e:
Chris Liechtifac1c132017-08-27 23:35:55 +0200971 sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e))
Chris Liechti00f84282015-12-24 23:40:34 +0100972 if args.develop:
973 raise
974 if not args.ask:
975 sys.exit(1)
976 else:
977 args.port = '-'
978 else:
979 break
cliechti6385f2c2005-09-21 19:51:19 +0000980
Chris Liechti3b454802015-08-26 23:39:59 +0200981 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100982 serial_instance,
983 echo=args.echo,
984 eol=args.eol.lower(),
985 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200986 miniterm.exit_character = unichr(args.exit_char)
987 miniterm.menu_character = unichr(args.menu_char)
988 miniterm.raw = args.raw
989 miniterm.set_rx_encoding(args.serial_port_encoding)
990 miniterm.set_tx_encoding(args.serial_port_encoding)
991
Chris Liechtib7550bd2015-08-15 04:09:10 +0200992 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200993 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100994 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200995 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100996 key_description(miniterm.exit_character),
997 key_description(miniterm.menu_character),
998 key_description(miniterm.menu_character),
999 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +00001000
cliechti6385f2c2005-09-21 19:51:19 +00001001 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +00001002 try:
1003 miniterm.join(True)
1004 except KeyboardInterrupt:
1005 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +02001006 if not args.quiet:
Chris Liechti49f19932017-08-30 17:55:39 +02001007 sys.stderr.write('\n--- exit ---\n')
cliechti6385f2c2005-09-21 19:51:19 +00001008 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +02001009 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +00001010
cliechti5370cee2013-10-13 03:08:19 +00001011# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +00001012if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +00001013 main()
Cefn Hoiled64fb602018-06-08 09:46:28 +01001014