blob: 8de7c717a96d673f5783d97dfb3f5493887ab1ec [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 Liechtiaebbb382020-09-16 14:02:11 +02006# (C)2002-2020 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 platform
Chris Liechti9cc696b2015-08-28 00:54:22 +020092
93 class Out(object):
Chris Liechtia887c932016-02-13 23:10:14 +010094 """file-like wrapper that uses os.write"""
95
Chris Liechti9cc696b2015-08-28 00:54:22 +020096 def __init__(self, fd):
97 self.fd = fd
98
99 def flush(self):
100 pass
101
102 def write(self, s):
103 os.write(self.fd, s)
104
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200105 class Console(ConsoleBase):
Cefn Hoile2d36b212018-06-08 10:29:50 +0100106 fncodes = {
Chris Liechti8813fb52021-05-14 00:38:24 +0200107 ';': '\x1bOP', # F1
108 '<': '\x1bOQ', # F2
109 '=': '\x1bOR', # F3
110 '>': '\x1bOS', # F4
111 '?': '\x1b[15~', # F5
112 '@': '\x1b[17~', # F6
113 'A': '\x1b[18~', # F7
114 'B': '\x1b[19~', # F8
115 'C': '\x1b[20~', # F9
116 'D': '\x1b[21~', # F10
Cefn Hoile2d36b212018-06-08 10:29:50 +0100117 }
118 navcodes = {
Cefn Hoiled64fb602018-06-08 09:46:28 +0100119 'H': '\x1b[A', # UP
120 'P': '\x1b[B', # DOWN
121 'K': '\x1b[D', # LEFT
122 'M': '\x1b[C', # RIGHT
123 'G': '\x1b[H', # HOME
124 'O': '\x1b[F', # END
Cefn Hoile2d36b212018-06-08 10:29:50 +0100125 'R': '\x1b[2~', # INSERT
126 'S': '\x1b[3~', # DELETE
127 'I': '\x1b[5~', # PGUP
128 'Q': '\x1b[6~', # PGDN
Cefn Hoiled64fb602018-06-08 09:46:28 +0100129 }
130
Chris Liechticbb00b22015-08-13 22:58:49 +0200131 def __init__(self):
132 super(Console, self).__init__()
Chris Liechti1df28272015-08-27 23:37:38 +0200133 self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
134 self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
Chris Liechticbb00b22015-08-13 22:58:49 +0200135 ctypes.windll.kernel32.SetConsoleOutputCP(65001)
136 ctypes.windll.kernel32.SetConsoleCP(65001)
Cefn Hoile035053d2018-06-08 10:45:18 +0100137 # ANSI handling available through SetConsoleMode since Windows 10 v1511
138 # https://en.wikipedia.org/wiki/ANSI_escape_code#cite_note-win10th2-1
Cefn Hoiled64fb602018-06-08 09:46:28 +0100139 if platform.release() == '10' and int(platform.version().split('.')[2]) > 10586:
Cefn Hoile2d36b212018-06-08 10:29:50 +0100140 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
141 import ctypes.wintypes as wintypes
142 if not hasattr(wintypes, 'LPDWORD'): # PY2
143 wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
144 SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode
145 GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode
146 GetStdHandle = ctypes.windll.kernel32.GetStdHandle
147 mode = wintypes.DWORD()
148 GetConsoleMode(GetStdHandle(-11), ctypes.byref(mode))
149 if (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0:
150 SetConsoleMode(GetStdHandle(-11), mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
151 self._saved_cm = mode
Chris Liechti9cc696b2015-08-28 00:54:22 +0200152 self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
153 # the change of the code page is not propagated to Python, manually fix it
154 sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
155 sys.stdout = self.output
Chris Liechti168704f2015-09-30 16:50:29 +0200156 self.output.encoding = 'UTF-8' # needed for input
Chris Liechticbb00b22015-08-13 22:58:49 +0200157
Chris Liechti1df28272015-08-27 23:37:38 +0200158 def __del__(self):
159 ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
160 ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
Cefn Hoile2d36b212018-06-08 10:29:50 +0100161 try:
162 ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), self._saved_cm)
163 except AttributeError: # in case no _saved_cm
164 pass
Chris Liechti1df28272015-08-27 23:37:38 +0200165
cliechti3a8bf092008-09-17 11:26:53 +0000166 def getkey(self):
cliechti91165532011-03-18 02:02:52 +0000167 while True:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200168 z = msvcrt.getwch()
Chris Liechti9f398812015-09-13 18:50:44 +0200169 if z == unichr(13):
170 return unichr(10)
Cefn Hoile2d36b212018-06-08 10:29:50 +0100171 elif z is unichr(0) or z is unichr(0xe0):
Cefn Hoiled64fb602018-06-08 09:46:28 +0100172 try:
Cefn Hoile2d36b212018-06-08 10:29:50 +0100173 code = msvcrt.getwch()
174 if z is unichr(0):
175 return self.fncodes[code]
176 else:
177 return self.navcodes[code]
Cefn Hoiled64fb602018-06-08 09:46:28 +0100178 except KeyError:
179 pass
cliechti9c592b32008-06-16 22:00:14 +0000180 else:
cliechti9c592b32008-06-16 22:00:14 +0000181 return z
cliechti53edb472009-02-06 21:18:46 +0000182
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200183 def cancel(self):
Chris Liechtic20c3732016-05-14 02:25:13 +0200184 # CancelIo, CancelSynchronousIo do not seem to work when using
185 # getwch, so instead, send a key to the window with the console
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200186 hwnd = ctypes.windll.kernel32.GetConsoleWindow()
187 ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
188
cliechti576de252002-02-28 23:54:44 +0000189elif os.name == 'posix':
Chris Liechtia1d5c6d2015-08-07 14:41:24 +0200190 import atexit
191 import termios
Chris Liechticab3dab2016-12-07 01:27:41 +0100192 import fcntl
Chris Liechti9cc696b2015-08-28 00:54:22 +0200193
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200194 class Console(ConsoleBase):
cliechti9c592b32008-06-16 22:00:14 +0000195 def __init__(self):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200196 super(Console, self).__init__()
cliechti9c592b32008-06-16 22:00:14 +0000197 self.fd = sys.stdin.fileno()
Chris Liechti4d989c22015-08-24 00:24:49 +0200198 self.old = termios.tcgetattr(self.fd)
Chris Liechti89eb2472015-08-08 17:06:25 +0200199 atexit.register(self.cleanup)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200200 if sys.version_info < (3, 0):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200201 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
202 else:
203 self.enc_stdin = sys.stdin
cliechti9c592b32008-06-16 22:00:14 +0000204
205 def setup(self):
cliechti9c592b32008-06-16 22:00:14 +0000206 new = termios.tcgetattr(self.fd)
207 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
208 new[6][termios.VMIN] = 1
209 new[6][termios.VTIME] = 0
210 termios.tcsetattr(self.fd, termios.TCSANOW, new)
cliechti53edb472009-02-06 21:18:46 +0000211
cliechti9c592b32008-06-16 22:00:14 +0000212 def getkey(self):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200213 c = self.enc_stdin.read(1)
Chris Liechti9f398812015-09-13 18:50:44 +0200214 if c == unichr(0x7f):
215 c = unichr(8) # map the BS key (which yields DEL) to backspace
Chris Liechti9a720852015-08-25 00:20:38 +0200216 return c
cliechti53edb472009-02-06 21:18:46 +0000217
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200218 def cancel(self):
Chris Liechticab3dab2016-12-07 01:27:41 +0100219 fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200220
cliechti9c592b32008-06-16 22:00:14 +0000221 def cleanup(self):
Chris Liechti4d989c22015-08-24 00:24:49 +0200222 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
cliechti9c592b32008-06-16 22:00:14 +0000223
cliechti576de252002-02-28 23:54:44 +0000224else:
Chris Liechti397cf412016-02-11 00:11:48 +0100225 raise NotImplementedError(
226 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
cliechti576de252002-02-28 23:54:44 +0000227
cliechti6fa76fb2009-07-08 23:53:39 +0000228
Chris Liechti9a720852015-08-25 00:20:38 +0200229# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200230
231class Transform(object):
Chris Liechticbb00b22015-08-13 22:58:49 +0200232 """do-nothing: forward all data unchanged"""
Chris Liechtid698af72015-08-24 20:24:55 +0200233 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200234 """text received from serial port"""
235 return text
236
Chris Liechtid698af72015-08-24 20:24:55 +0200237 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200238 """text to be sent to serial port"""
239 return text
240
241 def echo(self, text):
242 """text to be sent but displayed on console"""
243 return text
244
Chris Liechti442bf512015-08-15 01:42:24 +0200245
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200246class CRLF(Transform):
247 """ENTER sends CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200248
Chris Liechtid698af72015-08-24 20:24:55 +0200249 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200250 return text.replace('\n', '\r\n')
251
Chris Liechti442bf512015-08-15 01:42:24 +0200252
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200253class CR(Transform):
254 """ENTER sends CR"""
Chris Liechtid698af72015-08-24 20:24:55 +0200255
256 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200257 return text.replace('\r', '\n')
258
Chris Liechtid698af72015-08-24 20:24:55 +0200259 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200260 return text.replace('\n', '\r')
261
Chris Liechti442bf512015-08-15 01:42:24 +0200262
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200263class LF(Transform):
264 """ENTER sends LF"""
265
266
267class NoTerminal(Transform):
268 """remove typical terminal control codes from input"""
Chris Liechti9a720852015-08-25 00:20:38 +0200269
270 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 +0100271 REPLACEMENT_MAP.update(
272 {
Chris Liechti033f17c2015-08-30 21:28:04 +0200273 0x7F: 0x2421, # DEL
274 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100275 })
Chris Liechti9a720852015-08-25 00:20:38 +0200276
Chris Liechtid698af72015-08-24 20:24:55 +0200277 def rx(self, text):
Chris Liechti9a720852015-08-25 00:20:38 +0200278 return text.translate(self.REPLACEMENT_MAP)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200279
Chris Liechtid698af72015-08-24 20:24:55 +0200280 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200281
282
Chris Liechti9a720852015-08-25 00:20:38 +0200283class NoControls(NoTerminal):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200284 """Remove all control codes, incl. CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200285
Chris Liechti9a720852015-08-25 00:20:38 +0200286 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
Chris Liechtiba45c522016-02-06 23:53:23 +0100287 REPLACEMENT_MAP.update(
288 {
Chris Liechtia887c932016-02-13 23:10:14 +0100289 0x20: 0x2423, # visual space
Chris Liechti033f17c2015-08-30 21:28:04 +0200290 0x7F: 0x2421, # DEL
291 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100292 })
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200293
294
295class Printable(Transform):
Chris Liechtid698af72015-08-24 20:24:55 +0200296 """Show decimal code for all non-ASCII characters and replace most control codes"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200297
Chris Liechtid698af72015-08-24 20:24:55 +0200298 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200299 r = []
Chris Liechtia887c932016-02-13 23:10:14 +0100300 for c in text:
301 if ' ' <= c < '\x7f' or c in '\r\n\b\t':
302 r.append(c)
303 elif c < ' ':
304 r.append(unichr(0x2400 + ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200305 else:
Chris Liechtia887c932016-02-13 23:10:14 +0100306 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200307 r.append(' ')
308 return ''.join(r)
309
Chris Liechtid698af72015-08-24 20:24:55 +0200310 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200311
312
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200313class Colorize(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200314 """Apply different colors for received and echo"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200315
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200316 def __init__(self):
317 # XXX make it configurable, use colorama?
318 self.input_color = '\x1b[37m'
319 self.echo_color = '\x1b[31m'
320
Chris Liechtid698af72015-08-24 20:24:55 +0200321 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200322 return self.input_color + text
323
324 def echo(self, text):
325 return self.echo_color + text
326
Chris Liechti442bf512015-08-15 01:42:24 +0200327
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200328class DebugIO(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200329 """Print what is sent and received"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200330
Chris Liechtid698af72015-08-24 20:24:55 +0200331 def rx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200332 sys.stderr.write(' [RX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200333 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200334 return text
335
Chris Liechtid698af72015-08-24 20:24:55 +0200336 def tx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200337 sys.stderr.write(' [TX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200338 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200339 return text
340
Chris Liechti442bf512015-08-15 01:42:24 +0200341
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200342# other ideas:
343# - add date/time for each newline
344# - insert newline after: a) timeout b) packet end character
345
Chris Liechtib3df13e2015-08-25 02:20:09 +0200346EOL_TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100347 'crlf': CRLF,
348 'cr': CR,
349 'lf': LF,
350}
Chris Liechtib3df13e2015-08-25 02:20:09 +0200351
352TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100353 'direct': Transform, # no transformation
354 'default': NoTerminal,
355 'nocontrol': NoControls,
356 'printable': Printable,
357 'colorize': Colorize,
358 'debug': DebugIO,
359}
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200360
361
Chris Liechti033f17c2015-08-30 21:28:04 +0200362# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti89313c92015-09-01 02:33:13 +0200363def ask_for_port():
364 """\
365 Show a list of ports and ask the user for a choice. To make selection
366 easier on systems with long device names, also allow the input of an
367 index.
368 """
369 sys.stderr.write('\n--- Available ports:\n')
370 ports = []
371 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
Chris Liechti8b0eaf22017-07-19 22:59:57 +0200372 sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
Chris Liechti89313c92015-09-01 02:33:13 +0200373 ports.append(port)
374 while True:
375 port = raw_input('--- Enter port index or full name: ')
376 try:
377 index = int(port) - 1
378 if not 0 <= index < len(ports):
379 sys.stderr.write('--- Invalid index!\n')
380 continue
381 except ValueError:
382 pass
383 else:
384 port = ports[index]
385 return port
cliechti1351dde2012-04-12 16:47:47 +0000386
387
cliechti8c2ea842011-03-18 01:51:46 +0000388class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200389 """\
390 Terminal application. Copy data from serial port to console and vice versa.
391 Handle special keys from the console to show menu etc.
392 """
393
Chris Liechti3b454802015-08-26 23:39:59 +0200394 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200395 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200396 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000397 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200398 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200399 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200400 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200401 self.eol = eol
402 self.filters = filters
403 self.update_transformations()
Carlos52bfe352018-03-16 13:27:19 +0000404 self.exit_character = unichr(0x1d) # GS/CTRL+]
405 self.menu_character = unichr(0x14) # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100406 self.alive = None
407 self._reader_alive = None
408 self.receiver_thread = None
409 self.rx_decoder = None
410 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000411
cliechti8c2ea842011-03-18 01:51:46 +0000412 def _start_reader(self):
413 """Start reader thread"""
414 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000415 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200416 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
417 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000418 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000419
420 def _stop_reader(self):
421 """Stop reader thread only, wait for clean exit of thread"""
422 self._reader_alive = False
Chris Liechti933a5172016-05-04 16:12:15 +0200423 if hasattr(self.serial, 'cancel_read'):
424 self.serial.cancel_read()
cliechti8c2ea842011-03-18 01:51:46 +0000425 self.receiver_thread.join()
426
cliechti8c2ea842011-03-18 01:51:46 +0000427 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100428 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000429 self.alive = True
430 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000431 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200432 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
433 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000434 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200435 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000436
cliechti6385f2c2005-09-21 19:51:19 +0000437 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100438 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000439 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000440
cliechtibf6bb7d2006-03-30 00:28:18 +0000441 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100442 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000443 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000444 if not transmit_only:
Chris Liechti933a5172016-05-04 16:12:15 +0200445 if hasattr(self.serial, 'cancel_read'):
446 self.serial.cancel_read()
cliechtibf6bb7d2006-03-30 00:28:18 +0000447 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000448
Chris Liechti933a5172016-05-04 16:12:15 +0200449 def close(self):
450 self.serial.close()
451
Chris Liechtib3df13e2015-08-25 02:20:09 +0200452 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100453 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100454 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
455 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200456 self.tx_transformations = [t() for t in transformations]
457 self.rx_transformations = list(reversed(self.tx_transformations))
458
Chris Liechtid698af72015-08-24 20:24:55 +0200459 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100460 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200461 self.input_encoding = encoding
462 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
463
464 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100465 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200466 self.output_encoding = encoding
467 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
468
cliechti6c8eb2f2009-07-08 02:10:46 +0000469 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100470 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200471 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100472 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200473 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100474 ('active' if self.serial.rts else 'inactive'),
475 ('active' if self.serial.dtr else 'inactive'),
476 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000477 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200478 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100479 ('active' if self.serial.cts else 'inactive'),
480 ('active' if self.serial.dsr else 'inactive'),
481 ('active' if self.serial.ri else 'inactive'),
482 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000483 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200484 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000485 # yet received. ignore this error.
486 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200487 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
488 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200489 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
490 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200491 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
492 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000493
cliechti6385f2c2005-09-21 19:51:19 +0000494 def reader(self):
495 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000496 try:
cliechti8c2ea842011-03-18 01:51:46 +0000497 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200498 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200499 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200500 if data:
501 if self.raw:
502 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000503 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200504 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200505 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200506 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200507 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200508 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000509 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200510 self.console.cancel()
511 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000512
cliechti6385f2c2005-09-21 19:51:19 +0000513 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000514 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200515 Loop and copy console->serial until self.exit_character character is
516 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000517 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000518 """
519 menu_active = False
520 try:
521 while self.alive:
522 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200523 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000524 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200525 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200526 if not self.alive:
527 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000528 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200529 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000530 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200531 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200532 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200533 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200534 self.stop() # exit app
535 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000536 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200537 #~ if self.raw:
538 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200539 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200540 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200541 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000542 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200543 echo_text = c
544 for transformation in self.tx_transformations:
545 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200546 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000547 except:
548 self.alive = False
549 raise
cliechti6385f2c2005-09-21 19:51:19 +0000550
Chris Liechti7af7c752015-08-12 15:45:19 +0200551 def handle_menu_key(self, c):
552 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200553 if c == self.menu_character or c == self.exit_character:
554 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200555 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200556 if self.echo:
557 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200558 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti45c6f222017-07-17 23:56:24 +0200559 self.upload_file()
Chris Liechti7af7c752015-08-12 15:45:19 +0200560 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200561 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200562 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200563 self.serial.rts = not self.serial.rts
564 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200565 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200566 self.serial.dtr = not self.serial.dtr
567 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200568 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200569 self.serial.break_condition = not self.serial.break_condition
570 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200571 elif c == '\x05': # CTRL+E -> toggle local echo
572 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200573 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200574 elif c == '\x06': # CTRL+F -> edit filters
Chris Liechti45c6f222017-07-17 23:56:24 +0200575 self.change_filter()
Chris Liechtib3df13e2015-08-25 02:20:09 +0200576 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti49f19932017-08-30 17:55:39 +0200577 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200578 eol = modes.index(self.eol) + 1
579 if eol >= len(modes):
580 eol = 0
581 self.eol = modes[eol]
582 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
583 self.update_transformations()
584 elif c == '\x01': # CTRL+A -> set encoding
Chris Liechti45c6f222017-07-17 23:56:24 +0200585 self.change_encoding()
Chris Liechti7af7c752015-08-12 15:45:19 +0200586 elif c == '\x09': # CTRL+I -> info
587 self.dump_port_settings()
588 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
589 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
590 elif c in 'pP': # P -> change port
Chris Liechti45c6f222017-07-17 23:56:24 +0200591 self.change_port()
Chris Liechti0dbc2e52020-09-14 06:51:11 +0200592 elif c in 'zZ': # S -> suspend / open port temporarily
Chris Liechti45c6f222017-07-17 23:56:24 +0200593 self.suspend_port()
Chris Liechti7af7c752015-08-12 15:45:19 +0200594 elif c in 'bB': # B -> change baudrate
Chris Liechti45c6f222017-07-17 23:56:24 +0200595 self.change_baudrate()
Chris Liechti7af7c752015-08-12 15:45:19 +0200596 elif c == '8': # 8 -> change to 8 bits
597 self.serial.bytesize = serial.EIGHTBITS
598 self.dump_port_settings()
599 elif c == '7': # 7 -> change to 8 bits
600 self.serial.bytesize = serial.SEVENBITS
601 self.dump_port_settings()
602 elif c in 'eE': # E -> change to even parity
603 self.serial.parity = serial.PARITY_EVEN
604 self.dump_port_settings()
605 elif c in 'oO': # O -> change to odd parity
606 self.serial.parity = serial.PARITY_ODD
607 self.dump_port_settings()
608 elif c in 'mM': # M -> change to mark parity
609 self.serial.parity = serial.PARITY_MARK
610 self.dump_port_settings()
611 elif c in 'sS': # S -> change to space parity
612 self.serial.parity = serial.PARITY_SPACE
613 self.dump_port_settings()
614 elif c in 'nN': # N -> change to no parity
615 self.serial.parity = serial.PARITY_NONE
616 self.dump_port_settings()
617 elif c == '1': # 1 -> change to 1 stop bits
618 self.serial.stopbits = serial.STOPBITS_ONE
619 self.dump_port_settings()
620 elif c == '2': # 2 -> change to 2 stop bits
621 self.serial.stopbits = serial.STOPBITS_TWO
622 self.dump_port_settings()
623 elif c == '3': # 3 -> change to 1.5 stop bits
624 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
625 self.dump_port_settings()
626 elif c in 'xX': # X -> change software flow control
627 self.serial.xonxoff = (c == 'X')
628 self.dump_port_settings()
629 elif c in 'rR': # R -> change hardware flow control
630 self.serial.rtscts = (c == 'R')
631 self.dump_port_settings()
Chris Liechtiaebbb382020-09-16 14:02:11 +0200632 elif c in 'qQ':
633 self.stop() # Q -> exit app
Chris Liechti7af7c752015-08-12 15:45:19 +0200634 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200635 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
636
Chris Liechti45c6f222017-07-17 23:56:24 +0200637 def upload_file(self):
Chris Liechtibce41932021-05-14 00:44:29 +0200638 """Ask user for filename and send its contents"""
Chris Liechti45c6f222017-07-17 23:56:24 +0200639 sys.stderr.write('\n--- File to upload: ')
640 sys.stderr.flush()
641 with self.console:
642 filename = sys.stdin.readline().rstrip('\r\n')
643 if filename:
644 try:
645 with open(filename, 'rb') as f:
646 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
647 while True:
648 block = f.read(1024)
649 if not block:
650 break
651 self.serial.write(block)
652 # Wait for output buffer to drain.
653 self.serial.flush()
654 sys.stderr.write('.') # Progress indicator.
655 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
656 except IOError as e:
657 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
658
659 def change_filter(self):
660 """change the i/o transformations"""
661 sys.stderr.write('\n--- Available Filters:\n')
662 sys.stderr.write('\n'.join(
663 '--- {:<10} = {.__doc__}'.format(k, v)
664 for k, v in sorted(TRANSFORMATIONS.items())))
665 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
666 with self.console:
667 new_filters = sys.stdin.readline().lower().split()
668 if new_filters:
669 for f in new_filters:
670 if f not in TRANSFORMATIONS:
Chris Liechtifac1c132017-08-27 23:35:55 +0200671 sys.stderr.write('--- unknown filter: {!r}\n'.format(f))
Chris Liechti45c6f222017-07-17 23:56:24 +0200672 break
673 else:
674 self.filters = new_filters
675 self.update_transformations()
676 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
677
678 def change_encoding(self):
679 """change encoding on the serial port"""
680 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
681 with self.console:
682 new_encoding = sys.stdin.readline().strip()
683 if new_encoding:
684 try:
685 codecs.lookup(new_encoding)
686 except LookupError:
687 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
688 else:
689 self.set_rx_encoding(new_encoding)
690 self.set_tx_encoding(new_encoding)
691 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
692 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
693
694 def change_baudrate(self):
695 """change the baudrate"""
696 sys.stderr.write('\n--- Baudrate: ')
697 sys.stderr.flush()
698 with self.console:
699 backup = self.serial.baudrate
700 try:
701 self.serial.baudrate = int(sys.stdin.readline().strip())
702 except ValueError as e:
703 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
704 self.serial.baudrate = backup
705 else:
706 self.dump_port_settings()
707
708 def change_port(self):
709 """Have a conversation with the user to change the serial port"""
710 with self.console:
711 try:
712 port = ask_for_port()
713 except KeyboardInterrupt:
714 port = None
715 if port and port != self.serial.port:
716 # reader thread needs to be shut down
717 self._stop_reader()
718 # save settings
719 settings = self.serial.getSettingsDict()
720 try:
721 new_serial = serial.serial_for_url(port, do_not_open=True)
722 # restore settings and open
723 new_serial.applySettingsDict(settings)
724 new_serial.rts = self.serial.rts
725 new_serial.dtr = self.serial.dtr
726 new_serial.open()
727 new_serial.break_condition = self.serial.break_condition
728 except Exception as e:
729 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
730 new_serial.close()
731 else:
732 self.serial.close()
733 self.serial = new_serial
734 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
735 # and restart the reader thread
736 self._start_reader()
737
738 def suspend_port(self):
739 """\
740 open port temporarily, allow reconnect, exit and port change to get
741 out of the loop
742 """
743 # reader thread needs to be shut down
744 self._stop_reader()
745 self.serial.close()
746 sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
747 do_change_port = False
748 while not self.serial.is_open:
749 sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
750 exit=key_description(self.exit_character)))
751 k = self.console.getkey()
752 if k == self.exit_character:
753 self.stop() # exit app
754 break
755 elif k in 'pP':
756 do_change_port = True
757 break
758 try:
759 self.serial.open()
760 except Exception as e:
761 sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
762 if do_change_port:
763 self.change_port()
764 else:
765 # and restart the reader thread
766 self._start_reader()
767 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
768
Chris Liechti442bf512015-08-15 01:42:24 +0200769 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100770 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200771 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200772 return """
773--- pySerial ({version}) - miniterm - help
774---
Chris Liechtiaebbb382020-09-16 14:02:11 +0200775--- {exit:8} Exit program (alias {menu} Q)
Chris Liechti442bf512015-08-15 01:42:24 +0200776--- {menu:8} Menu escape key, followed by:
777--- Menu keys:
778--- {menu:7} Send the menu character itself to remote
779--- {exit:7} Send the exit character itself to remote
780--- {info:7} Show info
781--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200782--- {repr:7} encoding
783--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200784--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200785--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
786--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200787---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200788--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200789--- p change port
790--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200791--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200792--- 1 2 3 set stop bits (1, 2, 1.5)
793--- b change baud rate
794--- x X disable/enable software flow control
795--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100796""".format(version=getattr(serial, 'VERSION', 'unknown version'),
797 exit=key_description(self.exit_character),
798 menu=key_description(self.menu_character),
799 rts=key_description('\x12'),
800 dtr=key_description('\x04'),
801 brk=key_description('\x02'),
802 echo=key_description('\x05'),
803 info=key_description('\x09'),
804 upload=key_description('\x15'),
805 repr=key_description('\x01'),
806 filter=key_description('\x06'),
807 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200808
809
Chris Liechtib3df13e2015-08-25 02:20:09 +0200810# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200811# default args can be used to override when calling main() from an other script
812# e.g to create a miniterm-my-device.py
Chris Liechtibce41932021-05-14 00:44:29 +0200813def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None, serial_instance=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100814 """Command line tool, entry point"""
815
Chris Liechtib7550bd2015-08-15 04:09:10 +0200816 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000817
Chris Liechtib7550bd2015-08-15 04:09:10 +0200818 parser = argparse.ArgumentParser(
Chris Liechti49f19932017-08-30 17:55:39 +0200819 description='Miniterm - A simple terminal program for the serial port.')
cliechti6385f2c2005-09-21 19:51:19 +0000820
Chris Liechti033f17c2015-08-30 21:28:04 +0200821 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200822 'port',
Chris Liechti397cf412016-02-11 00:11:48 +0100823 nargs='?',
Chris Liechti49f19932017-08-30 17:55:39 +0200824 help='serial port name ("-" to show port list)',
Chris Liechti397cf412016-02-11 00:11:48 +0100825 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000826
Chris Liechti033f17c2015-08-30 21:28:04 +0200827 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200828 'baudrate',
Chris Liechti397cf412016-02-11 00:11:48 +0100829 nargs='?',
830 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200831 help='set baud rate, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100832 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000833
Chris Liechti49f19932017-08-30 17:55:39 +0200834 group = parser.add_argument_group('port settings')
cliechti53edb472009-02-06 21:18:46 +0000835
Chris Liechti033f17c2015-08-30 21:28:04 +0200836 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200837 '--parity',
Chris Liechti397cf412016-02-11 00:11:48 +0100838 choices=['N', 'E', 'O', 'S', 'M'],
839 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200840 help='set parity, one of {N E O S M}, default: N',
Chris Liechti397cf412016-02-11 00:11:48 +0100841 default='N')
cliechti53edb472009-02-06 21:18:46 +0000842
Chris Liechti033f17c2015-08-30 21:28:04 +0200843 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200844 '--rtscts',
845 action='store_true',
846 help='enable RTS/CTS flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100847 default=False)
cliechti53edb472009-02-06 21:18:46 +0000848
Chris Liechti033f17c2015-08-30 21:28:04 +0200849 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200850 '--xonxoff',
851 action='store_true',
852 help='enable software flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100853 default=False)
cliechti53edb472009-02-06 21:18:46 +0000854
Chris Liechti033f17c2015-08-30 21:28:04 +0200855 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200856 '--rts',
Chris Liechti397cf412016-02-11 00:11:48 +0100857 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200858 help='set initial RTS line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100859 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000860
Chris Liechti033f17c2015-08-30 21:28:04 +0200861 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200862 '--dtr',
Chris Liechti397cf412016-02-11 00:11:48 +0100863 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200864 help='set initial DTR line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100865 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000866
Chris Liechti00f84282015-12-24 23:40:34 +0100867 group.add_argument(
zsquarepluscb178d122018-05-07 20:12:59 +0200868 '--non-exclusive',
869 dest='exclusive',
870 action='store_false',
871 help='disable locking for native ports',
Sascha Silbe9c055352018-03-19 20:10:26 +0100872 default=True)
873
874 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200875 '--ask',
876 action='store_true',
877 help='ask again for port when open fails',
Chris Liechti397cf412016-02-11 00:11:48 +0100878 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100879
Chris Liechti49f19932017-08-30 17:55:39 +0200880 group = parser.add_argument_group('data handling')
cliechti5370cee2013-10-13 03:08:19 +0000881
Chris Liechti033f17c2015-08-30 21:28:04 +0200882 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200883 '-e', '--echo',
884 action='store_true',
885 help='enable local echo (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100886 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000887
Chris Liechti033f17c2015-08-30 21:28:04 +0200888 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200889 '--encoding',
890 dest='serial_port_encoding',
891 metavar='CODEC',
892 help='set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100893 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000894
Chris Liechti033f17c2015-08-30 21:28:04 +0200895 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200896 '-f', '--filter',
897 action='append',
898 metavar='NAME',
899 help='add text transformation',
Chris Liechti397cf412016-02-11 00:11:48 +0100900 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200901
Chris Liechti033f17c2015-08-30 21:28:04 +0200902 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200903 '--eol',
Chris Liechti397cf412016-02-11 00:11:48 +0100904 choices=['CR', 'LF', 'CRLF'],
905 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200906 help='end of line mode',
Chris Liechti397cf412016-02-11 00:11:48 +0100907 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000908
Chris Liechti033f17c2015-08-30 21:28:04 +0200909 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200910 '--raw',
911 action='store_true',
912 help='Do no apply any encodings/transformations',
Chris Liechti397cf412016-02-11 00:11:48 +0100913 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000914
Chris Liechti49f19932017-08-30 17:55:39 +0200915 group = parser.add_argument_group('hotkeys')
cliechtib7d746d2006-03-28 22:44:30 +0000916
Chris Liechti033f17c2015-08-30 21:28:04 +0200917 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200918 '--exit-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100919 type=int,
920 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200921 help='Unicode of special character that is used to exit the application, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100922 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000923
Chris Liechti033f17c2015-08-30 21:28:04 +0200924 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200925 '--menu-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100926 type=int,
927 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200928 help='Unicode code of special character that is used to control miniterm (menu), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100929 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000930
Chris Liechti49f19932017-08-30 17:55:39 +0200931 group = parser.add_argument_group('diagnostics')
cliechti6385f2c2005-09-21 19:51:19 +0000932
Chris Liechti033f17c2015-08-30 21:28:04 +0200933 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200934 '-q', '--quiet',
935 action='store_true',
936 help='suppress non-error messages',
Chris Liechti397cf412016-02-11 00:11:48 +0100937 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000938
Chris Liechti033f17c2015-08-30 21:28:04 +0200939 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200940 '--develop',
941 action='store_true',
942 help='show Python traceback on error',
Chris Liechti397cf412016-02-11 00:11:48 +0100943 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000944
Chris Liechtib7550bd2015-08-15 04:09:10 +0200945 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000946
Chris Liechtib7550bd2015-08-15 04:09:10 +0200947 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000948 parser.error('--exit-char can not be the same as --menu-char')
949
Chris Liechtib3df13e2015-08-25 02:20:09 +0200950 if args.filter:
951 if 'help' in args.filter:
952 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200953 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100954 '{:<10} = {.__doc__}'.format(k, v)
955 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200956 sys.stderr.write('\n')
957 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200958 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200959 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200960 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200961
Chris Liechtibce41932021-05-14 00:44:29 +0200962 while serial_instance is None:
Chris Liechti00f84282015-12-24 23:40:34 +0100963 # no port given on command line -> ask user now
964 if args.port is None or args.port == '-':
965 try:
966 args.port = ask_for_port()
967 except KeyboardInterrupt:
968 sys.stderr.write('\n')
969 parser.error('user aborted and port is not given')
970 else:
971 if not args.port:
972 parser.error('port is not given')
973 try:
974 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100975 args.port,
976 args.baudrate,
977 parity=args.parity,
978 rtscts=args.rtscts,
979 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100980 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200981
Chris Liechtif542fca2016-05-13 00:20:14 +0200982 if not hasattr(serial_instance, 'cancel_read'):
983 # enable timeout for alive flag polling if cancel_read is not available
984 serial_instance.timeout = 1
985
Chris Liechti00f84282015-12-24 23:40:34 +0100986 if args.dtr is not None:
987 if not args.quiet:
988 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
989 serial_instance.dtr = args.dtr
990 if args.rts is not None:
991 if not args.quiet:
992 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
993 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200994
Sascha Silbe9c055352018-03-19 20:10:26 +0100995 if isinstance(serial_instance, serial.Serial):
996 serial_instance.exclusive = args.exclusive
997
Chris Liechti00f84282015-12-24 23:40:34 +0100998 serial_instance.open()
999 except serial.SerialException as e:
Chris Liechtifac1c132017-08-27 23:35:55 +02001000 sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e))
Chris Liechti00f84282015-12-24 23:40:34 +01001001 if args.develop:
1002 raise
1003 if not args.ask:
1004 sys.exit(1)
1005 else:
1006 args.port = '-'
1007 else:
1008 break
cliechti6385f2c2005-09-21 19:51:19 +00001009
Chris Liechti3b454802015-08-26 23:39:59 +02001010 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +01001011 serial_instance,
1012 echo=args.echo,
1013 eol=args.eol.lower(),
1014 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +02001015 miniterm.exit_character = unichr(args.exit_char)
1016 miniterm.menu_character = unichr(args.menu_char)
1017 miniterm.raw = args.raw
1018 miniterm.set_rx_encoding(args.serial_port_encoding)
1019 miniterm.set_tx_encoding(args.serial_port_encoding)
1020
Chris Liechtib7550bd2015-08-15 04:09:10 +02001021 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +02001022 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +01001023 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +02001024 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +01001025 key_description(miniterm.exit_character),
1026 key_description(miniterm.menu_character),
1027 key_description(miniterm.menu_character),
1028 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +00001029
cliechti6385f2c2005-09-21 19:51:19 +00001030 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +00001031 try:
1032 miniterm.join(True)
1033 except KeyboardInterrupt:
1034 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +02001035 if not args.quiet:
Chris Liechti49f19932017-08-30 17:55:39 +02001036 sys.stderr.write('\n--- exit ---\n')
cliechti6385f2c2005-09-21 19:51:19 +00001037 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +02001038 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +00001039
cliechti5370cee2013-10-13 03:08:19 +00001040# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +00001041if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +00001042 main()