blob: a113db308e3fd3b37511dd7d896f4bd2ddbde207 [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 Hoile2d36b212018-06-08 10:29:50 +0100137 # ANSI handling available through SetConsoleMode since v1511 https://en.wikipedia.org/wiki/ANSI_escape_code#cite_note-win10th2-1
Cefn Hoiled64fb602018-06-08 09:46:28 +0100138 if platform.release() == '10' and int(platform.version().split('.')[2]) > 10586:
Cefn Hoile2d36b212018-06-08 10:29:50 +0100139 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
140 import ctypes.wintypes as wintypes
141 if not hasattr(wintypes, 'LPDWORD'): # PY2
142 wintypes.LPDWORD = ctypes.POINTER(wintypes.DWORD)
143 SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode
144 GetConsoleMode = ctypes.windll.kernel32.GetConsoleMode
145 GetStdHandle = ctypes.windll.kernel32.GetStdHandle
146 mode = wintypes.DWORD()
147 GetConsoleMode(GetStdHandle(-11), ctypes.byref(mode))
148 if (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0:
149 SetConsoleMode(GetStdHandle(-11), mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
150 self._saved_cm = mode
Chris Liechti9cc696b2015-08-28 00:54:22 +0200151 self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
152 # the change of the code page is not propagated to Python, manually fix it
153 sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
154 sys.stdout = self.output
Chris Liechti168704f2015-09-30 16:50:29 +0200155 self.output.encoding = 'UTF-8' # needed for input
Chris Liechticbb00b22015-08-13 22:58:49 +0200156
Chris Liechti1df28272015-08-27 23:37:38 +0200157 def __del__(self):
158 ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
159 ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
Cefn Hoile2d36b212018-06-08 10:29:50 +0100160 try:
161 ctypes.windll.kernel32.SetConsoleMode(ctypes.windll.kernel32.GetStdHandle(-11), self._saved_cm)
162 except AttributeError: # in case no _saved_cm
163 pass
Chris Liechti1df28272015-08-27 23:37:38 +0200164
cliechti3a8bf092008-09-17 11:26:53 +0000165 def getkey(self):
cliechti91165532011-03-18 02:02:52 +0000166 while True:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200167 z = msvcrt.getwch()
Chris Liechti9f398812015-09-13 18:50:44 +0200168 if z == unichr(13):
169 return unichr(10)
Cefn Hoile2d36b212018-06-08 10:29:50 +0100170 elif z is unichr(0) or z is unichr(0xe0):
Cefn Hoiled64fb602018-06-08 09:46:28 +0100171 try:
Cefn Hoile2d36b212018-06-08 10:29:50 +0100172 code = msvcrt.getwch()
173 if z is unichr(0):
174 return self.fncodes[code]
175 else:
176 return self.navcodes[code]
Cefn Hoiled64fb602018-06-08 09:46:28 +0100177 except KeyError:
178 pass
cliechti9c592b32008-06-16 22:00:14 +0000179 else:
cliechti9c592b32008-06-16 22:00:14 +0000180 return z
cliechti53edb472009-02-06 21:18:46 +0000181
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200182 def cancel(self):
Chris Liechtic20c3732016-05-14 02:25:13 +0200183 # CancelIo, CancelSynchronousIo do not seem to work when using
184 # getwch, so instead, send a key to the window with the console
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200185 hwnd = ctypes.windll.kernel32.GetConsoleWindow()
186 ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
187
cliechti576de252002-02-28 23:54:44 +0000188elif os.name == 'posix':
Chris Liechtia1d5c6d2015-08-07 14:41:24 +0200189 import atexit
190 import termios
Chris Liechticab3dab2016-12-07 01:27:41 +0100191 import fcntl
Chris Liechti9cc696b2015-08-28 00:54:22 +0200192
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200193 class Console(ConsoleBase):
cliechti9c592b32008-06-16 22:00:14 +0000194 def __init__(self):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200195 super(Console, self).__init__()
cliechti9c592b32008-06-16 22:00:14 +0000196 self.fd = sys.stdin.fileno()
Chris Liechti4d989c22015-08-24 00:24:49 +0200197 self.old = termios.tcgetattr(self.fd)
Chris Liechti89eb2472015-08-08 17:06:25 +0200198 atexit.register(self.cleanup)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200199 if sys.version_info < (3, 0):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200200 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
201 else:
202 self.enc_stdin = sys.stdin
cliechti9c592b32008-06-16 22:00:14 +0000203
204 def setup(self):
cliechti9c592b32008-06-16 22:00:14 +0000205 new = termios.tcgetattr(self.fd)
206 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
207 new[6][termios.VMIN] = 1
208 new[6][termios.VTIME] = 0
209 termios.tcsetattr(self.fd, termios.TCSANOW, new)
cliechti53edb472009-02-06 21:18:46 +0000210
cliechti9c592b32008-06-16 22:00:14 +0000211 def getkey(self):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200212 c = self.enc_stdin.read(1)
Chris Liechti9f398812015-09-13 18:50:44 +0200213 if c == unichr(0x7f):
214 c = unichr(8) # map the BS key (which yields DEL) to backspace
Chris Liechti9a720852015-08-25 00:20:38 +0200215 return c
cliechti53edb472009-02-06 21:18:46 +0000216
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200217 def cancel(self):
Chris Liechticab3dab2016-12-07 01:27:41 +0100218 fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200219
cliechti9c592b32008-06-16 22:00:14 +0000220 def cleanup(self):
Chris Liechti4d989c22015-08-24 00:24:49 +0200221 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
cliechti9c592b32008-06-16 22:00:14 +0000222
cliechti576de252002-02-28 23:54:44 +0000223else:
Chris Liechti397cf412016-02-11 00:11:48 +0100224 raise NotImplementedError(
225 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
cliechti576de252002-02-28 23:54:44 +0000226
cliechti6fa76fb2009-07-08 23:53:39 +0000227
Chris Liechti9a720852015-08-25 00:20:38 +0200228# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200229
230class Transform(object):
Chris Liechticbb00b22015-08-13 22:58:49 +0200231 """do-nothing: forward all data unchanged"""
Chris Liechtid698af72015-08-24 20:24:55 +0200232 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200233 """text received from serial port"""
234 return text
235
Chris Liechtid698af72015-08-24 20:24:55 +0200236 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200237 """text to be sent to serial port"""
238 return text
239
240 def echo(self, text):
241 """text to be sent but displayed on console"""
242 return text
243
Chris Liechti442bf512015-08-15 01:42:24 +0200244
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200245class CRLF(Transform):
246 """ENTER sends CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200247
Chris Liechtid698af72015-08-24 20:24:55 +0200248 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200249 return text.replace('\n', '\r\n')
250
Chris Liechti442bf512015-08-15 01:42:24 +0200251
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200252class CR(Transform):
253 """ENTER sends CR"""
Chris Liechtid698af72015-08-24 20:24:55 +0200254
255 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200256 return text.replace('\r', '\n')
257
Chris Liechtid698af72015-08-24 20:24:55 +0200258 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200259 return text.replace('\n', '\r')
260
Chris Liechti442bf512015-08-15 01:42:24 +0200261
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200262class LF(Transform):
263 """ENTER sends LF"""
264
265
266class NoTerminal(Transform):
267 """remove typical terminal control codes from input"""
Chris Liechti9a720852015-08-25 00:20:38 +0200268
269 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 +0100270 REPLACEMENT_MAP.update(
271 {
Chris Liechti033f17c2015-08-30 21:28:04 +0200272 0x7F: 0x2421, # DEL
273 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100274 })
Chris Liechti9a720852015-08-25 00:20:38 +0200275
Chris Liechtid698af72015-08-24 20:24:55 +0200276 def rx(self, text):
Chris Liechti9a720852015-08-25 00:20:38 +0200277 return text.translate(self.REPLACEMENT_MAP)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200278
Chris Liechtid698af72015-08-24 20:24:55 +0200279 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200280
281
Chris Liechti9a720852015-08-25 00:20:38 +0200282class NoControls(NoTerminal):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200283 """Remove all control codes, incl. CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200284
Chris Liechti9a720852015-08-25 00:20:38 +0200285 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
Chris Liechtiba45c522016-02-06 23:53:23 +0100286 REPLACEMENT_MAP.update(
287 {
Chris Liechtia887c932016-02-13 23:10:14 +0100288 0x20: 0x2423, # visual space
Chris Liechti033f17c2015-08-30 21:28:04 +0200289 0x7F: 0x2421, # DEL
290 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100291 })
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200292
293
294class Printable(Transform):
Chris Liechtid698af72015-08-24 20:24:55 +0200295 """Show decimal code for all non-ASCII characters and replace most control codes"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200296
Chris Liechtid698af72015-08-24 20:24:55 +0200297 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200298 r = []
Chris Liechtia887c932016-02-13 23:10:14 +0100299 for c in text:
300 if ' ' <= c < '\x7f' or c in '\r\n\b\t':
301 r.append(c)
302 elif c < ' ':
303 r.append(unichr(0x2400 + ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200304 else:
Chris Liechtia887c932016-02-13 23:10:14 +0100305 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200306 r.append(' ')
307 return ''.join(r)
308
Chris Liechtid698af72015-08-24 20:24:55 +0200309 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200310
311
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200312class Colorize(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200313 """Apply different colors for received and echo"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200314
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200315 def __init__(self):
316 # XXX make it configurable, use colorama?
317 self.input_color = '\x1b[37m'
318 self.echo_color = '\x1b[31m'
319
Chris Liechtid698af72015-08-24 20:24:55 +0200320 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200321 return self.input_color + text
322
323 def echo(self, text):
324 return self.echo_color + text
325
Chris Liechti442bf512015-08-15 01:42:24 +0200326
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200327class DebugIO(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200328 """Print what is sent and received"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200329
Chris Liechtid698af72015-08-24 20:24:55 +0200330 def rx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200331 sys.stderr.write(' [RX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200332 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200333 return text
334
Chris Liechtid698af72015-08-24 20:24:55 +0200335 def tx(self, text):
Chris Liechtifac1c132017-08-27 23:35:55 +0200336 sys.stderr.write(' [TX:{!r}] '.format(text))
Chris Liechtie1384382015-08-15 17:06:05 +0200337 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200338 return text
339
Chris Liechti442bf512015-08-15 01:42:24 +0200340
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200341# other ideas:
342# - add date/time for each newline
343# - insert newline after: a) timeout b) packet end character
344
Chris Liechtib3df13e2015-08-25 02:20:09 +0200345EOL_TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100346 'crlf': CRLF,
347 'cr': CR,
348 'lf': LF,
349}
Chris Liechtib3df13e2015-08-25 02:20:09 +0200350
351TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100352 'direct': Transform, # no transformation
353 'default': NoTerminal,
354 'nocontrol': NoControls,
355 'printable': Printable,
356 'colorize': Colorize,
357 'debug': DebugIO,
358}
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200359
360
Chris Liechti033f17c2015-08-30 21:28:04 +0200361# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti89313c92015-09-01 02:33:13 +0200362def ask_for_port():
363 """\
364 Show a list of ports and ask the user for a choice. To make selection
365 easier on systems with long device names, also allow the input of an
366 index.
367 """
368 sys.stderr.write('\n--- Available ports:\n')
369 ports = []
370 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
Chris Liechti8b0eaf22017-07-19 22:59:57 +0200371 sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
Chris Liechti89313c92015-09-01 02:33:13 +0200372 ports.append(port)
373 while True:
374 port = raw_input('--- Enter port index or full name: ')
375 try:
376 index = int(port) - 1
377 if not 0 <= index < len(ports):
378 sys.stderr.write('--- Invalid index!\n')
379 continue
380 except ValueError:
381 pass
382 else:
383 port = ports[index]
384 return port
cliechti1351dde2012-04-12 16:47:47 +0000385
386
cliechti8c2ea842011-03-18 01:51:46 +0000387class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200388 """\
389 Terminal application. Copy data from serial port to console and vice versa.
390 Handle special keys from the console to show menu etc.
391 """
392
Chris Liechti3b454802015-08-26 23:39:59 +0200393 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200394 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200395 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000396 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200397 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200398 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200399 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200400 self.eol = eol
401 self.filters = filters
402 self.update_transformations()
Carlos52bfe352018-03-16 13:27:19 +0000403 self.exit_character = unichr(0x1d) # GS/CTRL+]
404 self.menu_character = unichr(0x14) # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100405 self.alive = None
406 self._reader_alive = None
407 self.receiver_thread = None
408 self.rx_decoder = None
409 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000410
cliechti8c2ea842011-03-18 01:51:46 +0000411 def _start_reader(self):
412 """Start reader thread"""
413 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000414 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200415 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
416 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000417 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000418
419 def _stop_reader(self):
420 """Stop reader thread only, wait for clean exit of thread"""
421 self._reader_alive = False
Chris Liechti933a5172016-05-04 16:12:15 +0200422 if hasattr(self.serial, 'cancel_read'):
423 self.serial.cancel_read()
cliechti8c2ea842011-03-18 01:51:46 +0000424 self.receiver_thread.join()
425
cliechti8c2ea842011-03-18 01:51:46 +0000426 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100427 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000428 self.alive = True
429 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000430 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200431 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
432 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000433 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200434 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000435
cliechti6385f2c2005-09-21 19:51:19 +0000436 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100437 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000438 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000439
cliechtibf6bb7d2006-03-30 00:28:18 +0000440 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100441 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000442 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000443 if not transmit_only:
Chris Liechti933a5172016-05-04 16:12:15 +0200444 if hasattr(self.serial, 'cancel_read'):
445 self.serial.cancel_read()
cliechtibf6bb7d2006-03-30 00:28:18 +0000446 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000447
Chris Liechti933a5172016-05-04 16:12:15 +0200448 def close(self):
449 self.serial.close()
450
Chris Liechtib3df13e2015-08-25 02:20:09 +0200451 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100452 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100453 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
454 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200455 self.tx_transformations = [t() for t in transformations]
456 self.rx_transformations = list(reversed(self.tx_transformations))
457
Chris Liechtid698af72015-08-24 20:24:55 +0200458 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100459 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200460 self.input_encoding = encoding
461 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
462
463 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100464 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200465 self.output_encoding = encoding
466 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
467
cliechti6c8eb2f2009-07-08 02:10:46 +0000468 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100469 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200470 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100471 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200472 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100473 ('active' if self.serial.rts else 'inactive'),
474 ('active' if self.serial.dtr else 'inactive'),
475 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000476 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200477 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100478 ('active' if self.serial.cts else 'inactive'),
479 ('active' if self.serial.dsr else 'inactive'),
480 ('active' if self.serial.ri else 'inactive'),
481 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000482 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200483 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000484 # yet received. ignore this error.
485 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200486 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
487 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200488 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
489 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200490 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
491 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000492
cliechti6385f2c2005-09-21 19:51:19 +0000493 def reader(self):
494 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000495 try:
cliechti8c2ea842011-03-18 01:51:46 +0000496 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200497 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200498 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200499 if data:
500 if self.raw:
501 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000502 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200503 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200504 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200505 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200506 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200507 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000508 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200509 self.console.cancel()
510 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000511
cliechti6385f2c2005-09-21 19:51:19 +0000512 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000513 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200514 Loop and copy console->serial until self.exit_character character is
515 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000516 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000517 """
518 menu_active = False
519 try:
520 while self.alive:
521 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200522 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000523 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200524 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200525 if not self.alive:
526 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000527 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200528 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000529 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200530 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200531 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200532 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200533 self.stop() # exit app
534 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000535 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200536 #~ if self.raw:
537 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200538 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200539 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200540 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000541 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200542 echo_text = c
543 for transformation in self.tx_transformations:
544 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200545 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000546 except:
547 self.alive = False
548 raise
cliechti6385f2c2005-09-21 19:51:19 +0000549
Chris Liechti7af7c752015-08-12 15:45:19 +0200550 def handle_menu_key(self, c):
551 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200552 if c == self.menu_character or c == self.exit_character:
553 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200554 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200555 if self.echo:
556 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200557 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti45c6f222017-07-17 23:56:24 +0200558 self.upload_file()
Chris Liechti7af7c752015-08-12 15:45:19 +0200559 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200560 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200561 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200562 self.serial.rts = not self.serial.rts
563 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200564 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200565 self.serial.dtr = not self.serial.dtr
566 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200567 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200568 self.serial.break_condition = not self.serial.break_condition
569 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200570 elif c == '\x05': # CTRL+E -> toggle local echo
571 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200572 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200573 elif c == '\x06': # CTRL+F -> edit filters
Chris Liechti45c6f222017-07-17 23:56:24 +0200574 self.change_filter()
Chris Liechtib3df13e2015-08-25 02:20:09 +0200575 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti49f19932017-08-30 17:55:39 +0200576 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200577 eol = modes.index(self.eol) + 1
578 if eol >= len(modes):
579 eol = 0
580 self.eol = modes[eol]
581 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
582 self.update_transformations()
583 elif c == '\x01': # CTRL+A -> set encoding
Chris Liechti45c6f222017-07-17 23:56:24 +0200584 self.change_encoding()
Chris Liechti7af7c752015-08-12 15:45:19 +0200585 elif c == '\x09': # CTRL+I -> info
586 self.dump_port_settings()
587 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
588 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
589 elif c in 'pP': # P -> change port
Chris Liechti45c6f222017-07-17 23:56:24 +0200590 self.change_port()
Chris Liechtia73b96b2017-07-13 23:32:24 +0200591 elif c in 'sS': # S -> suspend / open port temporarily
Chris Liechti45c6f222017-07-17 23:56:24 +0200592 self.suspend_port()
Chris Liechti7af7c752015-08-12 15:45:19 +0200593 elif c in 'bB': # B -> change baudrate
Chris Liechti45c6f222017-07-17 23:56:24 +0200594 self.change_baudrate()
Chris Liechti7af7c752015-08-12 15:45:19 +0200595 elif c == '8': # 8 -> change to 8 bits
596 self.serial.bytesize = serial.EIGHTBITS
597 self.dump_port_settings()
598 elif c == '7': # 7 -> change to 8 bits
599 self.serial.bytesize = serial.SEVENBITS
600 self.dump_port_settings()
601 elif c in 'eE': # E -> change to even parity
602 self.serial.parity = serial.PARITY_EVEN
603 self.dump_port_settings()
604 elif c in 'oO': # O -> change to odd parity
605 self.serial.parity = serial.PARITY_ODD
606 self.dump_port_settings()
607 elif c in 'mM': # M -> change to mark parity
608 self.serial.parity = serial.PARITY_MARK
609 self.dump_port_settings()
610 elif c in 'sS': # S -> change to space parity
611 self.serial.parity = serial.PARITY_SPACE
612 self.dump_port_settings()
613 elif c in 'nN': # N -> change to no parity
614 self.serial.parity = serial.PARITY_NONE
615 self.dump_port_settings()
616 elif c == '1': # 1 -> change to 1 stop bits
617 self.serial.stopbits = serial.STOPBITS_ONE
618 self.dump_port_settings()
619 elif c == '2': # 2 -> change to 2 stop bits
620 self.serial.stopbits = serial.STOPBITS_TWO
621 self.dump_port_settings()
622 elif c == '3': # 3 -> change to 1.5 stop bits
623 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
624 self.dump_port_settings()
625 elif c in 'xX': # X -> change software flow control
626 self.serial.xonxoff = (c == 'X')
627 self.dump_port_settings()
628 elif c in 'rR': # R -> change hardware flow control
629 self.serial.rtscts = (c == 'R')
630 self.dump_port_settings()
631 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200632 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
633
Chris Liechti45c6f222017-07-17 23:56:24 +0200634 def upload_file(self):
635 """Ask user for filenname and send its contents"""
636 sys.stderr.write('\n--- File to upload: ')
637 sys.stderr.flush()
638 with self.console:
639 filename = sys.stdin.readline().rstrip('\r\n')
640 if filename:
641 try:
642 with open(filename, 'rb') as f:
643 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
644 while True:
645 block = f.read(1024)
646 if not block:
647 break
648 self.serial.write(block)
649 # Wait for output buffer to drain.
650 self.serial.flush()
651 sys.stderr.write('.') # Progress indicator.
652 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
653 except IOError as e:
654 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
655
656 def change_filter(self):
657 """change the i/o transformations"""
658 sys.stderr.write('\n--- Available Filters:\n')
659 sys.stderr.write('\n'.join(
660 '--- {:<10} = {.__doc__}'.format(k, v)
661 for k, v in sorted(TRANSFORMATIONS.items())))
662 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
663 with self.console:
664 new_filters = sys.stdin.readline().lower().split()
665 if new_filters:
666 for f in new_filters:
667 if f not in TRANSFORMATIONS:
Chris Liechtifac1c132017-08-27 23:35:55 +0200668 sys.stderr.write('--- unknown filter: {!r}\n'.format(f))
Chris Liechti45c6f222017-07-17 23:56:24 +0200669 break
670 else:
671 self.filters = new_filters
672 self.update_transformations()
673 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
674
675 def change_encoding(self):
676 """change encoding on the serial port"""
677 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
678 with self.console:
679 new_encoding = sys.stdin.readline().strip()
680 if new_encoding:
681 try:
682 codecs.lookup(new_encoding)
683 except LookupError:
684 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
685 else:
686 self.set_rx_encoding(new_encoding)
687 self.set_tx_encoding(new_encoding)
688 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
689 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
690
691 def change_baudrate(self):
692 """change the baudrate"""
693 sys.stderr.write('\n--- Baudrate: ')
694 sys.stderr.flush()
695 with self.console:
696 backup = self.serial.baudrate
697 try:
698 self.serial.baudrate = int(sys.stdin.readline().strip())
699 except ValueError as e:
700 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
701 self.serial.baudrate = backup
702 else:
703 self.dump_port_settings()
704
705 def change_port(self):
706 """Have a conversation with the user to change the serial port"""
707 with self.console:
708 try:
709 port = ask_for_port()
710 except KeyboardInterrupt:
711 port = None
712 if port and port != self.serial.port:
713 # reader thread needs to be shut down
714 self._stop_reader()
715 # save settings
716 settings = self.serial.getSettingsDict()
717 try:
718 new_serial = serial.serial_for_url(port, do_not_open=True)
719 # restore settings and open
720 new_serial.applySettingsDict(settings)
721 new_serial.rts = self.serial.rts
722 new_serial.dtr = self.serial.dtr
723 new_serial.open()
724 new_serial.break_condition = self.serial.break_condition
725 except Exception as e:
726 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
727 new_serial.close()
728 else:
729 self.serial.close()
730 self.serial = new_serial
731 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
732 # and restart the reader thread
733 self._start_reader()
734
735 def suspend_port(self):
736 """\
737 open port temporarily, allow reconnect, exit and port change to get
738 out of the loop
739 """
740 # reader thread needs to be shut down
741 self._stop_reader()
742 self.serial.close()
743 sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
744 do_change_port = False
745 while not self.serial.is_open:
746 sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
747 exit=key_description(self.exit_character)))
748 k = self.console.getkey()
749 if k == self.exit_character:
750 self.stop() # exit app
751 break
752 elif k in 'pP':
753 do_change_port = True
754 break
755 try:
756 self.serial.open()
757 except Exception as e:
758 sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
759 if do_change_port:
760 self.change_port()
761 else:
762 # and restart the reader thread
763 self._start_reader()
764 sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
765
Chris Liechti442bf512015-08-15 01:42:24 +0200766 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100767 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200768 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200769 return """
770--- pySerial ({version}) - miniterm - help
771---
772--- {exit:8} Exit program
773--- {menu:8} Menu escape key, followed by:
774--- Menu keys:
775--- {menu:7} Send the menu character itself to remote
776--- {exit:7} Send the exit character itself to remote
777--- {info:7} Show info
778--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200779--- {repr:7} encoding
780--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200781--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200782--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
783--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200784---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200785--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200786--- p change port
787--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200788--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200789--- 1 2 3 set stop bits (1, 2, 1.5)
790--- b change baud rate
791--- x X disable/enable software flow control
792--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100793""".format(version=getattr(serial, 'VERSION', 'unknown version'),
794 exit=key_description(self.exit_character),
795 menu=key_description(self.menu_character),
796 rts=key_description('\x12'),
797 dtr=key_description('\x04'),
798 brk=key_description('\x02'),
799 echo=key_description('\x05'),
800 info=key_description('\x09'),
801 upload=key_description('\x15'),
802 repr=key_description('\x01'),
803 filter=key_description('\x06'),
804 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200805
806
Chris Liechtib3df13e2015-08-25 02:20:09 +0200807# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200808# default args can be used to override when calling main() from an other script
809# e.g to create a miniterm-my-device.py
810def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100811 """Command line tool, entry point"""
812
Chris Liechtib7550bd2015-08-15 04:09:10 +0200813 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000814
Chris Liechtib7550bd2015-08-15 04:09:10 +0200815 parser = argparse.ArgumentParser(
Chris Liechti49f19932017-08-30 17:55:39 +0200816 description='Miniterm - A simple terminal program for the serial port.')
cliechti6385f2c2005-09-21 19:51:19 +0000817
Chris Liechti033f17c2015-08-30 21:28:04 +0200818 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200819 'port',
Chris Liechti397cf412016-02-11 00:11:48 +0100820 nargs='?',
Chris Liechti49f19932017-08-30 17:55:39 +0200821 help='serial port name ("-" to show port list)',
Chris Liechti397cf412016-02-11 00:11:48 +0100822 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000823
Chris Liechti033f17c2015-08-30 21:28:04 +0200824 parser.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200825 'baudrate',
Chris Liechti397cf412016-02-11 00:11:48 +0100826 nargs='?',
827 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200828 help='set baud rate, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100829 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000830
Chris Liechti49f19932017-08-30 17:55:39 +0200831 group = parser.add_argument_group('port settings')
cliechti53edb472009-02-06 21:18:46 +0000832
Chris Liechti033f17c2015-08-30 21:28:04 +0200833 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200834 '--parity',
Chris Liechti397cf412016-02-11 00:11:48 +0100835 choices=['N', 'E', 'O', 'S', 'M'],
836 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200837 help='set parity, one of {N E O S M}, default: N',
Chris Liechti397cf412016-02-11 00:11:48 +0100838 default='N')
cliechti53edb472009-02-06 21:18:46 +0000839
Chris Liechti033f17c2015-08-30 21:28:04 +0200840 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200841 '--rtscts',
842 action='store_true',
843 help='enable RTS/CTS flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100844 default=False)
cliechti53edb472009-02-06 21:18:46 +0000845
Chris Liechti033f17c2015-08-30 21:28:04 +0200846 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200847 '--xonxoff',
848 action='store_true',
849 help='enable software flow control (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100850 default=False)
cliechti53edb472009-02-06 21:18:46 +0000851
Chris Liechti033f17c2015-08-30 21:28:04 +0200852 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200853 '--rts',
Chris Liechti397cf412016-02-11 00:11:48 +0100854 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200855 help='set initial RTS line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100856 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000857
Chris Liechti033f17c2015-08-30 21:28:04 +0200858 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200859 '--dtr',
Chris Liechti397cf412016-02-11 00:11:48 +0100860 type=int,
Chris Liechti49f19932017-08-30 17:55:39 +0200861 help='set initial DTR line state (possible values: 0, 1)',
Chris Liechti397cf412016-02-11 00:11:48 +0100862 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000863
Chris Liechti00f84282015-12-24 23:40:34 +0100864 group.add_argument(
zsquarepluscb178d122018-05-07 20:12:59 +0200865 '--non-exclusive',
866 dest='exclusive',
867 action='store_false',
868 help='disable locking for native ports',
Sascha Silbe9c055352018-03-19 20:10:26 +0100869 default=True)
870
871 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200872 '--ask',
873 action='store_true',
874 help='ask again for port when open fails',
Chris Liechti397cf412016-02-11 00:11:48 +0100875 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100876
Chris Liechti49f19932017-08-30 17:55:39 +0200877 group = parser.add_argument_group('data handling')
cliechti5370cee2013-10-13 03:08:19 +0000878
Chris Liechti033f17c2015-08-30 21:28:04 +0200879 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200880 '-e', '--echo',
881 action='store_true',
882 help='enable local echo (default off)',
Chris Liechti397cf412016-02-11 00:11:48 +0100883 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000884
Chris Liechti033f17c2015-08-30 21:28:04 +0200885 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200886 '--encoding',
887 dest='serial_port_encoding',
888 metavar='CODEC',
889 help='set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100890 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000891
Chris Liechti033f17c2015-08-30 21:28:04 +0200892 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200893 '-f', '--filter',
894 action='append',
895 metavar='NAME',
896 help='add text transformation',
Chris Liechti397cf412016-02-11 00:11:48 +0100897 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200898
Chris Liechti033f17c2015-08-30 21:28:04 +0200899 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200900 '--eol',
Chris Liechti397cf412016-02-11 00:11:48 +0100901 choices=['CR', 'LF', 'CRLF'],
902 type=lambda c: c.upper(),
Chris Liechti49f19932017-08-30 17:55:39 +0200903 help='end of line mode',
Chris Liechti397cf412016-02-11 00:11:48 +0100904 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000905
Chris Liechti033f17c2015-08-30 21:28:04 +0200906 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200907 '--raw',
908 action='store_true',
909 help='Do no apply any encodings/transformations',
Chris Liechti397cf412016-02-11 00:11:48 +0100910 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000911
Chris Liechti49f19932017-08-30 17:55:39 +0200912 group = parser.add_argument_group('hotkeys')
cliechtib7d746d2006-03-28 22:44:30 +0000913
Chris Liechti033f17c2015-08-30 21:28:04 +0200914 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200915 '--exit-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100916 type=int,
917 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200918 help='Unicode of special character that is used to exit the application, default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100919 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000920
Chris Liechti033f17c2015-08-30 21:28:04 +0200921 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200922 '--menu-char',
Chris Liechti397cf412016-02-11 00:11:48 +0100923 type=int,
924 metavar='NUM',
Chris Liechti49f19932017-08-30 17:55:39 +0200925 help='Unicode code of special character that is used to control miniterm (menu), default: %(default)s',
Chris Liechti397cf412016-02-11 00:11:48 +0100926 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000927
Chris Liechti49f19932017-08-30 17:55:39 +0200928 group = parser.add_argument_group('diagnostics')
cliechti6385f2c2005-09-21 19:51:19 +0000929
Chris Liechti033f17c2015-08-30 21:28:04 +0200930 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200931 '-q', '--quiet',
932 action='store_true',
933 help='suppress non-error messages',
Chris Liechti397cf412016-02-11 00:11:48 +0100934 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000935
Chris Liechti033f17c2015-08-30 21:28:04 +0200936 group.add_argument(
Chris Liechti49f19932017-08-30 17:55:39 +0200937 '--develop',
938 action='store_true',
939 help='show Python traceback on error',
Chris Liechti397cf412016-02-11 00:11:48 +0100940 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000941
Chris Liechtib7550bd2015-08-15 04:09:10 +0200942 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000943
Chris Liechtib7550bd2015-08-15 04:09:10 +0200944 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000945 parser.error('--exit-char can not be the same as --menu-char')
946
Chris Liechtib3df13e2015-08-25 02:20:09 +0200947 if args.filter:
948 if 'help' in args.filter:
949 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200950 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100951 '{:<10} = {.__doc__}'.format(k, v)
952 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200953 sys.stderr.write('\n')
954 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200955 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200956 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200957 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200958
Chris Liechti00f84282015-12-24 23:40:34 +0100959 while True:
960 # no port given on command line -> ask user now
961 if args.port is None or args.port == '-':
962 try:
963 args.port = ask_for_port()
964 except KeyboardInterrupt:
965 sys.stderr.write('\n')
966 parser.error('user aborted and port is not given')
967 else:
968 if not args.port:
969 parser.error('port is not given')
970 try:
971 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100972 args.port,
973 args.baudrate,
974 parity=args.parity,
975 rtscts=args.rtscts,
976 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100977 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200978
Chris Liechtif542fca2016-05-13 00:20:14 +0200979 if not hasattr(serial_instance, 'cancel_read'):
980 # enable timeout for alive flag polling if cancel_read is not available
981 serial_instance.timeout = 1
982
Chris Liechti00f84282015-12-24 23:40:34 +0100983 if args.dtr is not None:
984 if not args.quiet:
985 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
986 serial_instance.dtr = args.dtr
987 if args.rts is not None:
988 if not args.quiet:
989 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
990 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200991
Sascha Silbe9c055352018-03-19 20:10:26 +0100992 if isinstance(serial_instance, serial.Serial):
993 serial_instance.exclusive = args.exclusive
994
Chris Liechti00f84282015-12-24 23:40:34 +0100995 serial_instance.open()
996 except serial.SerialException as e:
Chris Liechtifac1c132017-08-27 23:35:55 +0200997 sys.stderr.write('could not open port {!r}: {}\n'.format(args.port, e))
Chris Liechti00f84282015-12-24 23:40:34 +0100998 if args.develop:
999 raise
1000 if not args.ask:
1001 sys.exit(1)
1002 else:
1003 args.port = '-'
1004 else:
1005 break
cliechti6385f2c2005-09-21 19:51:19 +00001006
Chris Liechti3b454802015-08-26 23:39:59 +02001007 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +01001008 serial_instance,
1009 echo=args.echo,
1010 eol=args.eol.lower(),
1011 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +02001012 miniterm.exit_character = unichr(args.exit_char)
1013 miniterm.menu_character = unichr(args.menu_char)
1014 miniterm.raw = args.raw
1015 miniterm.set_rx_encoding(args.serial_port_encoding)
1016 miniterm.set_tx_encoding(args.serial_port_encoding)
1017
Chris Liechtib7550bd2015-08-15 04:09:10 +02001018 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +02001019 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +01001020 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +02001021 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +01001022 key_description(miniterm.exit_character),
1023 key_description(miniterm.menu_character),
1024 key_description(miniterm.menu_character),
1025 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +00001026
cliechti6385f2c2005-09-21 19:51:19 +00001027 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +00001028 try:
1029 miniterm.join(True)
1030 except KeyboardInterrupt:
1031 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +02001032 if not args.quiet:
Chris Liechti49f19932017-08-30 17:55:39 +02001033 sys.stderr.write('\n--- exit ---\n')
cliechti6385f2c2005-09-21 19:51:19 +00001034 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +02001035 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +00001036
cliechti5370cee2013-10-13 03:08:19 +00001037# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +00001038if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +00001039 main()
Cefn Hoiled64fb602018-06-08 09:46:28 +01001040