blob: 2ec155e347c29e54ce3eceddabb1e7839d617fa3 [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 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 = {
107 ';': '\1bOP', # F1
108 '<': '\1bOQ', # F2
109 '=': '\1bOR', # F3
110 '>': '\1bOS', # F4
111 '?': '\1b[15~', # F5
112 '@': '\1b[17~', # F6
113 'A': '\1b[18~', # F7
114 'B': '\1b[19~', # F8
115 'C': '\1b[20~', # F9
116 'D': '\1b[21~', # F10
117 }
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 Liechtia73b96b2017-07-13 23:32:24 +0200592 elif c in 'sS': # 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()
632 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200633 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
634
Chris Liechti45c6f222017-07-17 23:56:24 +0200635 def upload_file(self):
636 """Ask user for filenname and send its contents"""
637 sys.stderr.write('\n--- File to upload: ')
638 sys.stderr.flush()
639 with self.console:
640 filename = sys.stdin.readline().rstrip('\r\n')
641 if filename:
642 try:
643 with open(filename, 'rb') as f:
644 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
645 while True:
646 block = f.read(1024)
647 if not block:
648 break
649 self.serial.write(block)
650 # Wait for output buffer to drain.
651 self.serial.flush()
652 sys.stderr.write('.') # Progress indicator.
653 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
654 except IOError as e:
655 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
656
657 def change_filter(self):
658 """change the i/o transformations"""
659 sys.stderr.write('\n--- Available Filters:\n')
660 sys.stderr.write('\n'.join(
661 '--- {:<10} = {.__doc__}'.format(k, v)
662 for k, v in sorted(TRANSFORMATIONS.items())))
663 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
664 with self.console:
665 new_filters = sys.stdin.readline().lower().split()
666 if new_filters:
667 for f in new_filters:
668 if f not in TRANSFORMATIONS:
Chris Liechtifac1c132017-08-27 23:35:55 +0200669 sys.stderr.write('--- unknown filter: {!r}\n'.format(f))
Chris Liechti45c6f222017-07-17 23:56:24 +0200670 break
671 else:
672 self.filters = new_filters
673 self.update_transformations()
674 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
675
676 def change_encoding(self):
677 """change encoding on the serial port"""
678 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
679 with self.console:
680 new_encoding = sys.stdin.readline().strip()
681 if new_encoding:
682 try:
683 codecs.lookup(new_encoding)
684 except LookupError:
685 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
686 else:
687 self.set_rx_encoding(new_encoding)
688 self.set_tx_encoding(new_encoding)
689 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
690 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
691
692 def change_baudrate(self):
693 """change the baudrate"""
694 sys.stderr.write('\n--- Baudrate: ')
695 sys.stderr.flush()
696 with self.console:
697 backup = self.serial.baudrate
698 try:
699 self.serial.baudrate = int(sys.stdin.readline().strip())
700 except ValueError as e:
701 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
702 self.serial.baudrate = backup
703 else:
704 self.dump_port_settings()
705
706 def change_port(self):
707 """Have a conversation with the user to change the serial port"""
708 with self.console:
709 try:
710 port = ask_for_port()
711 except KeyboardInterrupt:
712 port = None
713 if port and port != self.serial.port:
714 # reader thread needs to be shut down
715 self._stop_reader()
716 # save settings
717 settings = self.serial.getSettingsDict()
718 try:
719 new_serial = serial.serial_for_url(port, do_not_open=True)
720 # restore settings and open
721 new_serial.applySettingsDict(settings)
722 new_serial.rts = self.serial.rts
723 new_serial.dtr = self.serial.dtr
724 new_serial.open()
725 new_serial.break_condition = self.serial.break_condition
726 except Exception as e:
727 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
728 new_serial.close()
729 else:
730 self.serial.close()
731 self.serial = new_serial
732 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
733 # and restart the reader thread
734 self._start_reader()
735
736 def suspend_port(self):
737 """\
738 open port temporarily, allow reconnect, exit and port change to get
739 out of the loop
740 """
741 # reader thread needs to be shut down
742 self._stop_reader()
743 self.serial.close()
744 sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
745 do_change_port = False
746 while not self.serial.is_open:
747 sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
748 exit=key_description(self.exit_character)))
749 k = self.console.getkey()
750 if k == self.exit_character:
751 self.stop() # exit app
752 break
753 elif k in 'pP':
754 do_change_port = True
755 break
756 try:
757 self.serial.open()
758 except Exception as e:
759 sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
760 if do_change_port:
761 self.change_port()
762 else:
763 # and restart the reader thread
764 self._start_reader()
765 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
766
Chris Liechti442bf512015-08-15 01:42:24 +0200767 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100768 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200769 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200770 return """
771--- pySerial ({version}) - miniterm - help
772---
773--- {exit:8} Exit program
774--- {menu:8} Menu escape key, followed by:
775--- Menu keys:
776--- {menu:7} Send the menu character itself to remote
777--- {exit:7} Send the exit character itself to remote
778--- {info:7} Show info
779--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200780--- {repr:7} encoding
781--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200782--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200783--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
784--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200785---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200786--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200787--- p change port
788--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200789--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200790--- 1 2 3 set stop bits (1, 2, 1.5)
791--- b change baud rate
792--- x X disable/enable software flow control
793--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100794""".format(version=getattr(serial, 'VERSION', 'unknown version'),
795 exit=key_description(self.exit_character),
796 menu=key_description(self.menu_character),
797 rts=key_description('\x12'),
798 dtr=key_description('\x04'),
799 brk=key_description('\x02'),
800 echo=key_description('\x05'),
801 info=key_description('\x09'),
802 upload=key_description('\x15'),
803 repr=key_description('\x01'),
804 filter=key_description('\x06'),
805 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200806
807
Chris Liechtib3df13e2015-08-25 02:20:09 +0200808# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200809# default args can be used to override when calling main() from an other script
810# e.g to create a miniterm-my-device.py
811def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100812 """Command line tool, entry point"""
813
Chris Liechtib7550bd2015-08-15 04:09:10 +0200814 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000815
Chris Liechtib7550bd2015-08-15 04:09:10 +0200816 parser = argparse.ArgumentParser(
Chris Liechti49f19932017-08-30 17:55:39 +0200817 description='Miniterm - A simple terminal program for the serial port.')
cliechti6385f2c2005-09-21 19:51:19 +0000818
Chris Liechti033f17c2015-08-30 21:28:04 +0200819 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200820 'port',
Chris Liechti397cf412016-02-11 00:11:48 +0100821 nargs='?',
Chris Liechti49f19932017-08-30 17:55:39 +0200822 help='serial port name ("-" to show port list)',
Chris Liechti397cf412016-02-11 00:11:48 +0100823 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000824
Chris Liechti033f17c2015-08-30 21:28:04 +0200825 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200826 'baudrate',
Chris Liechti397cf412016-02-11 00:11:48 +0100827 nargs='?',
828 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200829 help='set baud rate, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100830 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000831
Chris Liechti49f19932017-08-30 17:55:39 +0200832 group = parser.add_argument_group('port settings')
cliechti53edb472009-02-06 21:18:46 +0000833
Chris Liechti033f17c2015-08-30 21:28:04 +0200834 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200835 '--parity',
Chris Liechti397cf412016-02-11 00:11:48 +0100836 choices=['N', 'E', 'O', 'S', 'M'],
837 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200838 help='set parity, one of {N E O S M}, default: N',
Chris Liechti397cf412016-02-11 00:11:48 +0100839 default='N')
cliechti53edb472009-02-06 21:18:46 +0000840
Chris Liechti033f17c2015-08-30 21:28:04 +0200841 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200842 '--rtscts',
843 action='store_true',
844 help='enable RTS/CTS flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100845 default=False)
cliechti53edb472009-02-06 21:18:46 +0000846
Chris Liechti033f17c2015-08-30 21:28:04 +0200847 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200848 '--xonxoff',
849 action='store_true',
850 help='enable software flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100851 default=False)
cliechti53edb472009-02-06 21:18:46 +0000852
Chris Liechti033f17c2015-08-30 21:28:04 +0200853 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200854 '--rts',
Chris Liechti397cf412016-02-11 00:11:48 +0100855 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200856 help='set initial RTS line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100857 default=default_rts)
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 '--dtr',
Chris Liechti397cf412016-02-11 00:11:48 +0100861 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200862 help='set initial DTR line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100863 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000864
Chris Liechti00f84282015-12-24 23:40:34 +0100865 group.add_argument(
zsquarepluscb178d122018-05-07 20:12:59 +0200866 '--non-exclusive',
867 dest='exclusive',
868 action='store_false',
869 help='disable locking for native ports',
Sascha Silbe9c055352018-03-19 20:10:26 +0100870 default=True)
871
872 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200873 '--ask',
874 action='store_true',
875 help='ask again for port when open fails',
Chris Liechti397cf412016-02-11 00:11:48 +0100876 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100877
Chris Liechti49f19932017-08-30 17:55:39 +0200878 group = parser.add_argument_group('data handling')
cliechti5370cee2013-10-13 03:08:19 +0000879
Chris Liechti033f17c2015-08-30 21:28:04 +0200880 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200881 '-e', '--echo',
882 action='store_true',
883 help='enable local echo (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100884 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000885
Chris Liechti033f17c2015-08-30 21:28:04 +0200886 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200887 '--encoding',
888 dest='serial_port_encoding',
889 metavar='CODEC',
890 help='set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100891 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000892
Chris Liechti033f17c2015-08-30 21:28:04 +0200893 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200894 '-f', '--filter',
895 action='append',
896 metavar='NAME',
897 help='add text transformation',
Chris Liechti397cf412016-02-11 00:11:48 +0100898 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200899
Chris Liechti033f17c2015-08-30 21:28:04 +0200900 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200901 '--eol',
Chris Liechti397cf412016-02-11 00:11:48 +0100902 choices=['CR', 'LF', 'CRLF'],
903 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200904 help='end of line mode',
Chris Liechti397cf412016-02-11 00:11:48 +0100905 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000906
Chris Liechti033f17c2015-08-30 21:28:04 +0200907 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200908 '--raw',
909 action='store_true',
910 help='Do no apply any encodings/transformations',
Chris Liechti397cf412016-02-11 00:11:48 +0100911 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000912
Chris Liechti49f19932017-08-30 17:55:39 +0200913 group = parser.add_argument_group('hotkeys')
cliechtib7d746d2006-03-28 22:44:30 +0000914
Chris Liechti033f17c2015-08-30 21:28:04 +0200915 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200916 '--exit-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100917 type=int,
918 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200919 help='Unicode of special character that is used to exit the application, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100920 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000921
Chris Liechti033f17c2015-08-30 21:28:04 +0200922 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200923 '--menu-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100924 type=int,
925 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200926 help='Unicode code of special character that is used to control miniterm (menu), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100927 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000928
Chris Liechti49f19932017-08-30 17:55:39 +0200929 group = parser.add_argument_group('diagnostics')
cliechti6385f2c2005-09-21 19:51:19 +0000930
Chris Liechti033f17c2015-08-30 21:28:04 +0200931 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200932 '-q', '--quiet',
933 action='store_true',
934 help='suppress non-error messages',
Chris Liechti397cf412016-02-11 00:11:48 +0100935 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000936
Chris Liechti033f17c2015-08-30 21:28:04 +0200937 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200938 '--develop',
939 action='store_true',
940 help='show Python traceback on error',
Chris Liechti397cf412016-02-11 00:11:48 +0100941 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000942
Chris Liechtib7550bd2015-08-15 04:09:10 +0200943 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000944
Chris Liechtib7550bd2015-08-15 04:09:10 +0200945 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000946 parser.error('--exit-char can not be the same as --menu-char')
947
Chris Liechtib3df13e2015-08-25 02:20:09 +0200948 if args.filter:
949 if 'help' in args.filter:
950 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200951 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100952 '{:<10} = {.__doc__}'.format(k, v)
953 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200954 sys.stderr.write('\n')
955 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200956 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200957 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200958 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200959
Chris Liechti00f84282015-12-24 23:40:34 +0100960 while True:
961 # no port given on command line -> ask user now
962 if args.port is None or args.port == '-':
963 try:
964 args.port = ask_for_port()
965 except KeyboardInterrupt:
966 sys.stderr.write('\n')
967 parser.error('user aborted and port is not given')
968 else:
969 if not args.port:
970 parser.error('port is not given')
971 try:
972 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100973 args.port,
974 args.baudrate,
975 parity=args.parity,
976 rtscts=args.rtscts,
977 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100978 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200979
Chris Liechtif542fca2016-05-13 00:20:14 +0200980 if not hasattr(serial_instance, 'cancel_read'):
981 # enable timeout for alive flag polling if cancel_read is not available
982 serial_instance.timeout = 1
983
Chris Liechti00f84282015-12-24 23:40:34 +0100984 if args.dtr is not None:
985 if not args.quiet:
986 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
987 serial_instance.dtr = args.dtr
988 if args.rts is not None:
989 if not args.quiet:
990 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
991 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200992
Sascha Silbe9c055352018-03-19 20:10:26 +0100993 if isinstance(serial_instance, serial.Serial):
994 serial_instance.exclusive = args.exclusive
995
Chris Liechti00f84282015-12-24 23:40:34 +0100996 serial_instance.open()
997 except serial.SerialException as e:
Chris Liechtifac1c132017-08-27 23:35:55 +0200998 sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e))
Chris Liechti00f84282015-12-24 23:40:34 +0100999 if args.develop:
1000 raise
1001 if not args.ask:
1002 sys.exit(1)
1003 else:
1004 args.port = '-'
1005 else:
1006 break
cliechti6385f2c2005-09-21 19:51:19 +00001007
Chris Liechti3b454802015-08-26 23:39:59 +02001008 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +01001009 serial_instance,
1010 echo=args.echo,
1011 eol=args.eol.lower(),
1012 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +02001013 miniterm.exit_character = unichr(args.exit_char)
1014 miniterm.menu_character = unichr(args.menu_char)
1015 miniterm.raw = args.raw
1016 miniterm.set_rx_encoding(args.serial_port_encoding)
1017 miniterm.set_tx_encoding(args.serial_port_encoding)
1018
Chris Liechtib7550bd2015-08-15 04:09:10 +02001019 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +02001020 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +01001021 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +02001022 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +01001023 key_description(miniterm.exit_character),
1024 key_description(miniterm.menu_character),
1025 key_description(miniterm.menu_character),
1026 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +00001027
cliechti6385f2c2005-09-21 19:51:19 +00001028 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +00001029 try:
1030 miniterm.join(True)
1031 except KeyboardInterrupt:
1032 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +02001033 if not args.quiet:
Chris Liechti49f19932017-08-30 17:55:39 +02001034 sys.stderr.write('\n--- exit ---\n')
cliechti6385f2c2005-09-21 19:51:19 +00001035 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +02001036 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +00001037
cliechti5370cee2013-10-13 03:08:19 +00001038# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +00001039if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +00001040 main()