blob: 420bf1271203f6efdd97b509e0216aff61870096 [file] [log] [blame]
cliechti576de252002-02-28 23:54:44 +00001#!/usr/bin/env python
Chris Liechtifbdd8a02015-08-09 02:37:45 +02002#
cliechtia128a702004-07-21 22:13:31 +00003# Very simple serial terminal
Chris Liechtifbdd8a02015-08-09 02:37:45 +02004#
Chris Liechti3e02f702015-12-16 23:06:04 +01005# This file is part of pySerial. https://github.com/pyserial/pyserial
Chris Liechti68340d72015-08-03 14:15:48 +02006# (C)2002-2015 Chris Liechti <cliechti@gmx.net>
Chris Liechtifbdd8a02015-08-09 02:37:45 +02007#
8# SPDX-License-Identifier: BSD-3-Clause
cliechtifc9eb382002-03-05 01:12:29 +00009
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020010import codecs
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020011import os
12import sys
13import threading
cliechti576de252002-02-28 23:54:44 +000014
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020015import serial
Chris Liechti55ba7d92015-08-15 16:33:51 +020016from serial.tools.list_ports import comports
Chris Liechti168704f2015-09-30 16:50:29 +020017from serial.tools import hexlify_codec
18
Chris Liechtia887c932016-02-13 23:10:14 +010019# pylint: disable=wrong-import-order,wrong-import-position
20
Chris Liechti168704f2015-09-30 16:50:29 +020021codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None)
Chris Liechtia1d5c6d2015-08-07 14:41:24 +020022
Chris Liechti68340d72015-08-03 14:15:48 +020023try:
24 raw_input
25except NameError:
Chris Liechtia887c932016-02-13 23:10:14 +010026 # pylint: disable=redefined-builtin,invalid-name
Chris Liechti68340d72015-08-03 14:15:48 +020027 raw_input = input # in python3 it's "raw"
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020028 unichr = chr
Chris Liechti68340d72015-08-03 14:15:48 +020029
cliechti6c8eb2f2009-07-08 02:10:46 +000030
31def key_description(character):
32 """generate a readable description for a key"""
33 ascii_code = ord(character)
34 if ascii_code < 32:
35 return 'Ctrl+%c' % (ord('@') + ascii_code)
36 else:
37 return repr(character)
38
cliechti91165532011-03-18 02:02:52 +000039
Chris Liechti9a720852015-08-25 00:20:38 +020040# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020041class ConsoleBase(object):
Chris Liechti397cf412016-02-11 00:11:48 +010042 """OS abstraction for console (input/output codec, no echo)"""
43
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020044 def __init__(self):
45 if sys.version_info >= (3, 0):
46 self.byte_output = sys.stdout.buffer
47 else:
48 self.byte_output = sys.stdout
49 self.output = sys.stdout
cliechtif467aa82013-10-13 21:36:49 +000050
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020051 def setup(self):
Chris Liechti397cf412016-02-11 00:11:48 +010052 """Set console to read single characters, no echo"""
cliechtif467aa82013-10-13 21:36:49 +000053
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020054 def cleanup(self):
Chris Liechti397cf412016-02-11 00:11:48 +010055 """Restore default console settings"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020056
57 def getkey(self):
Chris Liechti397cf412016-02-11 00:11:48 +010058 """Read a single key from the console"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020059 return None
60
Chris Liechtia887c932016-02-13 23:10:14 +010061 def write_bytes(self, byte_string):
Chris Liechti397cf412016-02-11 00:11:48 +010062 """Write bytes (already encoded)"""
Chris Liechtia887c932016-02-13 23:10:14 +010063 self.byte_output.write(byte_string)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020064 self.byte_output.flush()
65
Chris Liechtia887c932016-02-13 23:10:14 +010066 def write(self, text):
Chris Liechti397cf412016-02-11 00:11:48 +010067 """Write string"""
Chris Liechtia887c932016-02-13 23:10:14 +010068 self.output.write(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020069 self.output.flush()
70
Chris Liechti1eb3f6b2016-04-27 02:12:50 +020071 def cancel(self):
72 """Cancel getkey operation"""
73
Chris Liechti269f77b2015-08-24 01:31:42 +020074 # - - - - - - - - - - - - - - - - - - - - - - - -
75 # context manager:
76 # switch terminal temporary to normal mode (e.g. to get user input)
77
78 def __enter__(self):
79 self.cleanup()
80 return self
81
82 def __exit__(self, *args, **kwargs):
83 self.setup()
84
cliechti9c592b32008-06-16 22:00:14 +000085
Chris Liechtiba45c522016-02-06 23:53:23 +010086if os.name == 'nt': # noqa
cliechti576de252002-02-28 23:54:44 +000087 import msvcrt
Chris Liechtic7a5d4c2015-08-11 23:32:20 +020088 import ctypes
Chris Liechti9cc696b2015-08-28 00:54:22 +020089
90 class Out(object):
Chris Liechtia887c932016-02-13 23:10:14 +010091 """file-like wrapper that uses os.write"""
92
Chris Liechti9cc696b2015-08-28 00:54:22 +020093 def __init__(self, fd):
94 self.fd = fd
95
96 def flush(self):
97 pass
98
99 def write(self, s):
100 os.write(self.fd, s)
101
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200102 class Console(ConsoleBase):
Chris Liechticbb00b22015-08-13 22:58:49 +0200103 def __init__(self):
104 super(Console, self).__init__()
Chris Liechti1df28272015-08-27 23:37:38 +0200105 self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
106 self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
Chris Liechticbb00b22015-08-13 22:58:49 +0200107 ctypes.windll.kernel32.SetConsoleOutputCP(65001)
108 ctypes.windll.kernel32.SetConsoleCP(65001)
Chris Liechti9cc696b2015-08-28 00:54:22 +0200109 self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
110 # the change of the code page is not propagated to Python, manually fix it
111 sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
112 sys.stdout = self.output
Chris Liechti168704f2015-09-30 16:50:29 +0200113 self.output.encoding = 'UTF-8' # needed for input
Chris Liechticbb00b22015-08-13 22:58:49 +0200114
Chris Liechti1df28272015-08-27 23:37:38 +0200115 def __del__(self):
116 ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
117 ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
118
cliechti3a8bf092008-09-17 11:26:53 +0000119 def getkey(self):
cliechti91165532011-03-18 02:02:52 +0000120 while True:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200121 z = msvcrt.getwch()
Chris Liechti9f398812015-09-13 18:50:44 +0200122 if z == unichr(13):
123 return unichr(10)
124 elif z in (unichr(0), unichr(0x0e)): # functions keys, ignore
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200125 msvcrt.getwch()
cliechti9c592b32008-06-16 22:00:14 +0000126 else:
cliechti9c592b32008-06-16 22:00:14 +0000127 return z
cliechti53edb472009-02-06 21:18:46 +0000128
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200129 def cancel(self):
Chris Liechtic20c3732016-05-14 02:25:13 +0200130 # CancelIo, CancelSynchronousIo do not seem to work when using
131 # getwch, so instead, send a key to the window with the console
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200132 hwnd = ctypes.windll.kernel32.GetConsoleWindow()
133 ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
134
cliechti576de252002-02-28 23:54:44 +0000135elif os.name == 'posix':
Chris Liechtia1d5c6d2015-08-07 14:41:24 +0200136 import atexit
137 import termios
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200138 import select
Chris Liechti9cc696b2015-08-28 00:54:22 +0200139
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200140 class Console(ConsoleBase):
cliechti9c592b32008-06-16 22:00:14 +0000141 def __init__(self):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200142 super(Console, self).__init__()
cliechti9c592b32008-06-16 22:00:14 +0000143 self.fd = sys.stdin.fileno()
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200144 # an additional pipe is used in getkey, so that the cancel method
145 # can abort the waiting getkey method
146 self.pipe_r, self.pipe_w = os.pipe()
Chris Liechti4d989c22015-08-24 00:24:49 +0200147 self.old = termios.tcgetattr(self.fd)
Chris Liechti89eb2472015-08-08 17:06:25 +0200148 atexit.register(self.cleanup)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200149 if sys.version_info < (3, 0):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200150 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
151 else:
152 self.enc_stdin = sys.stdin
cliechti9c592b32008-06-16 22:00:14 +0000153
154 def setup(self):
cliechti9c592b32008-06-16 22:00:14 +0000155 new = termios.tcgetattr(self.fd)
156 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
157 new[6][termios.VMIN] = 1
158 new[6][termios.VTIME] = 0
159 termios.tcsetattr(self.fd, termios.TCSANOW, new)
cliechti53edb472009-02-06 21:18:46 +0000160
cliechti9c592b32008-06-16 22:00:14 +0000161 def getkey(self):
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200162 ready, _, _ = select.select([self.enc_stdin, self.pipe_r], [], [], None)
163 if self.pipe_r in ready:
164 os.read(self.pipe_r, 1)
165 return
Chris Liechtia7e7b692015-08-25 21:10:28 +0200166 c = self.enc_stdin.read(1)
Chris Liechti9f398812015-09-13 18:50:44 +0200167 if c == unichr(0x7f):
168 c = unichr(8) # map the BS key (which yields DEL) to backspace
Chris Liechti9a720852015-08-25 00:20:38 +0200169 return c
cliechti53edb472009-02-06 21:18:46 +0000170
Chris Liechti16a8b5e2016-05-09 22:46:06 +0200171 def cancel(self):
172 os.write(self.pipe_w, "x")
173
cliechti9c592b32008-06-16 22:00:14 +0000174 def cleanup(self):
Chris Liechti4d989c22015-08-24 00:24:49 +0200175 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
cliechti9c592b32008-06-16 22:00:14 +0000176
cliechti576de252002-02-28 23:54:44 +0000177else:
Chris Liechti397cf412016-02-11 00:11:48 +0100178 raise NotImplementedError(
179 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
cliechti576de252002-02-28 23:54:44 +0000180
cliechti6fa76fb2009-07-08 23:53:39 +0000181
Chris Liechti9a720852015-08-25 00:20:38 +0200182# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200183
184class Transform(object):
Chris Liechticbb00b22015-08-13 22:58:49 +0200185 """do-nothing: forward all data unchanged"""
Chris Liechtid698af72015-08-24 20:24:55 +0200186 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200187 """text received from serial port"""
188 return text
189
Chris Liechtid698af72015-08-24 20:24:55 +0200190 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200191 """text to be sent to serial port"""
192 return text
193
194 def echo(self, text):
195 """text to be sent but displayed on console"""
196 return text
197
Chris Liechti442bf512015-08-15 01:42:24 +0200198
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200199class CRLF(Transform):
200 """ENTER sends CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200201
Chris Liechtid698af72015-08-24 20:24:55 +0200202 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200203 return text.replace('\n', '\r\n')
204
Chris Liechti442bf512015-08-15 01:42:24 +0200205
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200206class CR(Transform):
207 """ENTER sends CR"""
Chris Liechtid698af72015-08-24 20:24:55 +0200208
209 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200210 return text.replace('\r', '\n')
211
Chris Liechtid698af72015-08-24 20:24:55 +0200212 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200213 return text.replace('\n', '\r')
214
Chris Liechti442bf512015-08-15 01:42:24 +0200215
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200216class LF(Transform):
217 """ENTER sends LF"""
218
219
220class NoTerminal(Transform):
221 """remove typical terminal control codes from input"""
Chris Liechti9a720852015-08-25 00:20:38 +0200222
223 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 +0100224 REPLACEMENT_MAP.update(
225 {
Chris Liechti033f17c2015-08-30 21:28:04 +0200226 0x7F: 0x2421, # DEL
227 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100228 })
Chris Liechti9a720852015-08-25 00:20:38 +0200229
Chris Liechtid698af72015-08-24 20:24:55 +0200230 def rx(self, text):
Chris Liechti9a720852015-08-25 00:20:38 +0200231 return text.translate(self.REPLACEMENT_MAP)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200232
Chris Liechtid698af72015-08-24 20:24:55 +0200233 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200234
235
Chris Liechti9a720852015-08-25 00:20:38 +0200236class NoControls(NoTerminal):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200237 """Remove all control codes, incl. CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200238
Chris Liechti9a720852015-08-25 00:20:38 +0200239 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
Chris Liechtiba45c522016-02-06 23:53:23 +0100240 REPLACEMENT_MAP.update(
241 {
Chris Liechtia887c932016-02-13 23:10:14 +0100242 0x20: 0x2423, # visual space
Chris Liechti033f17c2015-08-30 21:28:04 +0200243 0x7F: 0x2421, # DEL
244 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100245 })
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200246
247
248class Printable(Transform):
Chris Liechtid698af72015-08-24 20:24:55 +0200249 """Show decimal code for all non-ASCII characters and replace most control codes"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200250
Chris Liechtid698af72015-08-24 20:24:55 +0200251 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200252 r = []
Chris Liechtia887c932016-02-13 23:10:14 +0100253 for c in text:
254 if ' ' <= c < '\x7f' or c in '\r\n\b\t':
255 r.append(c)
256 elif c < ' ':
257 r.append(unichr(0x2400 + ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200258 else:
Chris Liechtia887c932016-02-13 23:10:14 +0100259 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200260 r.append(' ')
261 return ''.join(r)
262
Chris Liechtid698af72015-08-24 20:24:55 +0200263 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200264
265
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200266class Colorize(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200267 """Apply different colors for received and echo"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200268
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200269 def __init__(self):
270 # XXX make it configurable, use colorama?
271 self.input_color = '\x1b[37m'
272 self.echo_color = '\x1b[31m'
273
Chris Liechtid698af72015-08-24 20:24:55 +0200274 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200275 return self.input_color + text
276
277 def echo(self, text):
278 return self.echo_color + text
279
Chris Liechti442bf512015-08-15 01:42:24 +0200280
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200281class DebugIO(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200282 """Print what is sent and received"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200283
Chris Liechtid698af72015-08-24 20:24:55 +0200284 def rx(self, text):
Chris Liechtie1384382015-08-15 17:06:05 +0200285 sys.stderr.write(' [RX:{}] '.format(repr(text)))
286 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200287 return text
288
Chris Liechtid698af72015-08-24 20:24:55 +0200289 def tx(self, text):
Chris Liechtie1384382015-08-15 17:06:05 +0200290 sys.stderr.write(' [TX:{}] '.format(repr(text)))
291 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200292 return text
293
Chris Liechti442bf512015-08-15 01:42:24 +0200294
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200295# other ideas:
296# - add date/time for each newline
297# - insert newline after: a) timeout b) packet end character
298
Chris Liechtib3df13e2015-08-25 02:20:09 +0200299EOL_TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100300 'crlf': CRLF,
301 'cr': CR,
302 'lf': LF,
303}
Chris Liechtib3df13e2015-08-25 02:20:09 +0200304
305TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100306 'direct': Transform, # no transformation
307 'default': NoTerminal,
308 'nocontrol': NoControls,
309 'printable': Printable,
310 'colorize': Colorize,
311 'debug': DebugIO,
312}
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200313
314
Chris Liechti033f17c2015-08-30 21:28:04 +0200315# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti89313c92015-09-01 02:33:13 +0200316def ask_for_port():
317 """\
318 Show a list of ports and ask the user for a choice. To make selection
319 easier on systems with long device names, also allow the input of an
320 index.
321 """
322 sys.stderr.write('\n--- Available ports:\n')
323 ports = []
324 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
325 #~ sys.stderr.write('--- %-20s %s [%s]\n' % (port, desc, hwid))
326 sys.stderr.write('--- {:2}: {:20} {}\n'.format(n, port, desc))
327 ports.append(port)
328 while True:
329 port = raw_input('--- Enter port index or full name: ')
330 try:
331 index = int(port) - 1
332 if not 0 <= index < len(ports):
333 sys.stderr.write('--- Invalid index!\n')
334 continue
335 except ValueError:
336 pass
337 else:
338 port = ports[index]
339 return port
cliechti1351dde2012-04-12 16:47:47 +0000340
341
cliechti8c2ea842011-03-18 01:51:46 +0000342class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200343 """\
344 Terminal application. Copy data from serial port to console and vice versa.
345 Handle special keys from the console to show menu etc.
346 """
347
Chris Liechti3b454802015-08-26 23:39:59 +0200348 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200349 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200350 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000351 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200352 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200353 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200354 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200355 self.eol = eol
356 self.filters = filters
357 self.update_transformations()
Chris Liechti442bf512015-08-15 01:42:24 +0200358 self.exit_character = 0x1d # GS/CTRL+]
359 self.menu_character = 0x14 # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100360 self.alive = None
361 self._reader_alive = None
362 self.receiver_thread = None
363 self.rx_decoder = None
364 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000365
cliechti8c2ea842011-03-18 01:51:46 +0000366 def _start_reader(self):
367 """Start reader thread"""
368 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000369 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200370 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
371 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000372 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000373
374 def _stop_reader(self):
375 """Stop reader thread only, wait for clean exit of thread"""
376 self._reader_alive = False
Chris Liechti933a5172016-05-04 16:12:15 +0200377 if hasattr(self.serial, 'cancel_read'):
378 self.serial.cancel_read()
cliechti8c2ea842011-03-18 01:51:46 +0000379 self.receiver_thread.join()
380
cliechti8c2ea842011-03-18 01:51:46 +0000381 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100382 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000383 self.alive = True
384 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000385 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200386 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
387 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000388 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200389 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000390
cliechti6385f2c2005-09-21 19:51:19 +0000391 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100392 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000393 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000394
cliechtibf6bb7d2006-03-30 00:28:18 +0000395 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100396 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000397 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000398 if not transmit_only:
Chris Liechti933a5172016-05-04 16:12:15 +0200399 if hasattr(self.serial, 'cancel_read'):
400 self.serial.cancel_read()
cliechtibf6bb7d2006-03-30 00:28:18 +0000401 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000402
Chris Liechti933a5172016-05-04 16:12:15 +0200403 def close(self):
404 self.serial.close()
405
Chris Liechtib3df13e2015-08-25 02:20:09 +0200406 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100407 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100408 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
409 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200410 self.tx_transformations = [t() for t in transformations]
411 self.rx_transformations = list(reversed(self.tx_transformations))
412
Chris Liechtid698af72015-08-24 20:24:55 +0200413 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100414 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200415 self.input_encoding = encoding
416 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
417
418 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100419 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200420 self.output_encoding = encoding
421 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
422
cliechti6c8eb2f2009-07-08 02:10:46 +0000423 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100424 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200425 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100426 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200427 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100428 ('active' if self.serial.rts else 'inactive'),
429 ('active' if self.serial.dtr else 'inactive'),
430 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000431 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200432 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100433 ('active' if self.serial.cts else 'inactive'),
434 ('active' if self.serial.dsr else 'inactive'),
435 ('active' if self.serial.ri else 'inactive'),
436 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000437 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200438 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000439 # yet received. ignore this error.
440 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200441 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
442 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200443 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
444 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200445 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
446 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000447
cliechti6385f2c2005-09-21 19:51:19 +0000448 def reader(self):
449 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000450 try:
cliechti8c2ea842011-03-18 01:51:46 +0000451 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200452 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200453 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200454 if data:
455 if self.raw:
456 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000457 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200458 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200459 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200460 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200461 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200462 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000463 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200464 self.console.cancel()
465 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000466
cliechti6385f2c2005-09-21 19:51:19 +0000467 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000468 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200469 Loop and copy console->serial until self.exit_character character is
470 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000471 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000472 """
473 menu_active = False
474 try:
475 while self.alive:
476 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200477 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000478 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200479 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200480 if not self.alive:
481 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000482 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200483 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000484 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200485 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200486 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200487 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200488 self.stop() # exit app
489 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000490 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200491 #~ if self.raw:
492 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200493 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200494 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200495 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000496 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200497 echo_text = c
498 for transformation in self.tx_transformations:
499 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200500 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000501 except:
502 self.alive = False
503 raise
cliechti6385f2c2005-09-21 19:51:19 +0000504
Chris Liechti7af7c752015-08-12 15:45:19 +0200505 def handle_menu_key(self, c):
506 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200507 if c == self.menu_character or c == self.exit_character:
508 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200509 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200510 if self.echo:
511 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200512 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti7af7c752015-08-12 15:45:19 +0200513 sys.stderr.write('\n--- File to upload: ')
514 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200515 with self.console:
516 filename = sys.stdin.readline().rstrip('\r\n')
517 if filename:
518 try:
519 with open(filename, 'rb') as f:
520 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
521 while True:
522 block = f.read(1024)
523 if not block:
524 break
525 self.serial.write(block)
526 # Wait for output buffer to drain.
527 self.serial.flush()
528 sys.stderr.write('.') # Progress indicator.
529 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
530 except IOError as e:
531 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200532 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200533 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200534 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200535 self.serial.rts = not self.serial.rts
536 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200537 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200538 self.serial.dtr = not self.serial.dtr
539 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200540 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200541 self.serial.break_condition = not self.serial.break_condition
542 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200543 elif c == '\x05': # CTRL+E -> toggle local echo
544 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200545 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200546 elif c == '\x06': # CTRL+F -> edit filters
547 sys.stderr.write('\n--- Available Filters:\n')
548 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100549 '--- {:<10} = {.__doc__}'.format(k, v)
550 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200551 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
552 with self.console:
553 new_filters = sys.stdin.readline().lower().split()
554 if new_filters:
555 for f in new_filters:
556 if f not in TRANSFORMATIONS:
557 sys.stderr.write('--- unknown filter: {}'.format(repr(f)))
558 break
559 else:
560 self.filters = new_filters
561 self.update_transformations()
562 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
563 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti033f17c2015-08-30 21:28:04 +0200564 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200565 eol = modes.index(self.eol) + 1
566 if eol >= len(modes):
567 eol = 0
568 self.eol = modes[eol]
569 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
570 self.update_transformations()
571 elif c == '\x01': # CTRL+A -> set encoding
572 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
573 with self.console:
574 new_encoding = sys.stdin.readline().strip()
575 if new_encoding:
576 try:
577 codecs.lookup(new_encoding)
578 except LookupError:
579 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
580 else:
581 self.set_rx_encoding(new_encoding)
582 self.set_tx_encoding(new_encoding)
583 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
584 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_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 Liechti269f77b2015-08-24 01:31:42 +0200590 with self.console:
591 try:
Chris Liechti89313c92015-09-01 02:33:13 +0200592 port = ask_for_port()
Chris Liechti269f77b2015-08-24 01:31:42 +0200593 except KeyboardInterrupt:
594 port = None
Chris Liechti7af7c752015-08-12 15:45:19 +0200595 if port and port != self.serial.port:
596 # reader thread needs to be shut down
597 self._stop_reader()
598 # save settings
599 settings = self.serial.getSettingsDict()
600 try:
601 new_serial = serial.serial_for_url(port, do_not_open=True)
602 # restore settings and open
603 new_serial.applySettingsDict(settings)
Chris Liechti89313c92015-09-01 02:33:13 +0200604 new_serial.rts = self.serial.rts
605 new_serial.dtr = self.serial.dtr
Chris Liechti7af7c752015-08-12 15:45:19 +0200606 new_serial.open()
Chris Liechti89313c92015-09-01 02:33:13 +0200607 new_serial.break_condition = self.serial.break_condition
Chris Liechti7af7c752015-08-12 15:45:19 +0200608 except Exception as e:
Chris Liechti442bf512015-08-15 01:42:24 +0200609 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200610 new_serial.close()
611 else:
612 self.serial.close()
613 self.serial = new_serial
Chris Liechti442bf512015-08-15 01:42:24 +0200614 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
Chris Liechti7af7c752015-08-12 15:45:19 +0200615 # and restart the reader thread
616 self._start_reader()
617 elif c in 'bB': # B -> change baudrate
618 sys.stderr.write('\n--- Baudrate: ')
619 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200620 with self.console:
621 backup = self.serial.baudrate
622 try:
623 self.serial.baudrate = int(sys.stdin.readline().strip())
624 except ValueError as e:
Chris Liechti4d541e22016-02-13 23:13:59 +0100625 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
Chris Liechti269f77b2015-08-24 01:31:42 +0200626 self.serial.baudrate = backup
627 else:
628 self.dump_port_settings()
Chris Liechti7af7c752015-08-12 15:45:19 +0200629 elif c == '8': # 8 -> change to 8 bits
630 self.serial.bytesize = serial.EIGHTBITS
631 self.dump_port_settings()
632 elif c == '7': # 7 -> change to 8 bits
633 self.serial.bytesize = serial.SEVENBITS
634 self.dump_port_settings()
635 elif c in 'eE': # E -> change to even parity
636 self.serial.parity = serial.PARITY_EVEN
637 self.dump_port_settings()
638 elif c in 'oO': # O -> change to odd parity
639 self.serial.parity = serial.PARITY_ODD
640 self.dump_port_settings()
641 elif c in 'mM': # M -> change to mark parity
642 self.serial.parity = serial.PARITY_MARK
643 self.dump_port_settings()
644 elif c in 'sS': # S -> change to space parity
645 self.serial.parity = serial.PARITY_SPACE
646 self.dump_port_settings()
647 elif c in 'nN': # N -> change to no parity
648 self.serial.parity = serial.PARITY_NONE
649 self.dump_port_settings()
650 elif c == '1': # 1 -> change to 1 stop bits
651 self.serial.stopbits = serial.STOPBITS_ONE
652 self.dump_port_settings()
653 elif c == '2': # 2 -> change to 2 stop bits
654 self.serial.stopbits = serial.STOPBITS_TWO
655 self.dump_port_settings()
656 elif c == '3': # 3 -> change to 1.5 stop bits
657 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
658 self.dump_port_settings()
659 elif c in 'xX': # X -> change software flow control
660 self.serial.xonxoff = (c == 'X')
661 self.dump_port_settings()
662 elif c in 'rR': # R -> change hardware flow control
663 self.serial.rtscts = (c == 'R')
664 self.dump_port_settings()
665 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200666 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
667
668 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100669 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200670 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200671 return """
672--- pySerial ({version}) - miniterm - help
673---
674--- {exit:8} Exit program
675--- {menu:8} Menu escape key, followed by:
676--- Menu keys:
677--- {menu:7} Send the menu character itself to remote
678--- {exit:7} Send the exit character itself to remote
679--- {info:7} Show info
680--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200681--- {repr:7} encoding
682--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200683--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200684--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
685--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200686---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200687--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200688--- p change port
689--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200690--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200691--- 1 2 3 set stop bits (1, 2, 1.5)
692--- b change baud rate
693--- x X disable/enable software flow control
694--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100695""".format(version=getattr(serial, 'VERSION', 'unknown version'),
696 exit=key_description(self.exit_character),
697 menu=key_description(self.menu_character),
698 rts=key_description('\x12'),
699 dtr=key_description('\x04'),
700 brk=key_description('\x02'),
701 echo=key_description('\x05'),
702 info=key_description('\x09'),
703 upload=key_description('\x15'),
704 repr=key_description('\x01'),
705 filter=key_description('\x06'),
706 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200707
708
Chris Liechtib3df13e2015-08-25 02:20:09 +0200709# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200710# default args can be used to override when calling main() from an other script
711# e.g to create a miniterm-my-device.py
712def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100713 """Command line tool, entry point"""
714
Chris Liechtib7550bd2015-08-15 04:09:10 +0200715 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000716
Chris Liechtib7550bd2015-08-15 04:09:10 +0200717 parser = argparse.ArgumentParser(
Chris Liechti397cf412016-02-11 00:11:48 +0100718 description="Miniterm - A simple terminal program for the serial port.")
cliechti6385f2c2005-09-21 19:51:19 +0000719
Chris Liechti033f17c2015-08-30 21:28:04 +0200720 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100721 "port",
722 nargs='?',
723 help="serial port name ('-' to show port list)",
724 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000725
Chris Liechti033f17c2015-08-30 21:28:04 +0200726 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100727 "baudrate",
728 nargs='?',
729 type=int,
730 help="set baud rate, default: %(default)s",
731 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000732
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200733 group = parser.add_argument_group("port settings")
cliechti53edb472009-02-06 21:18:46 +0000734
Chris Liechti033f17c2015-08-30 21:28:04 +0200735 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100736 "--parity",
737 choices=['N', 'E', 'O', 'S', 'M'],
738 type=lambda c: c.upper(),
739 help="set parity, one of {N E O S M}, default: N",
740 default='N')
cliechti53edb472009-02-06 21:18:46 +0000741
Chris Liechti033f17c2015-08-30 21:28:04 +0200742 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100743 "--rtscts",
744 action="store_true",
745 help="enable RTS/CTS flow control (default off)",
746 default=False)
cliechti53edb472009-02-06 21:18:46 +0000747
Chris Liechti033f17c2015-08-30 21:28:04 +0200748 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100749 "--xonxoff",
750 action="store_true",
751 help="enable software flow control (default off)",
752 default=False)
cliechti53edb472009-02-06 21:18:46 +0000753
Chris Liechti033f17c2015-08-30 21:28:04 +0200754 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100755 "--rts",
756 type=int,
757 help="set initial RTS line state (possible values: 0, 1)",
758 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000759
Chris Liechti033f17c2015-08-30 21:28:04 +0200760 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100761 "--dtr",
762 type=int,
763 help="set initial DTR line state (possible values: 0, 1)",
764 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000765
Chris Liechti00f84282015-12-24 23:40:34 +0100766 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100767 "--ask",
768 action="store_true",
769 help="ask again for port when open fails",
770 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100771
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200772 group = parser.add_argument_group("data handling")
cliechti5370cee2013-10-13 03:08:19 +0000773
Chris Liechti033f17c2015-08-30 21:28:04 +0200774 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100775 "-e", "--echo",
776 action="store_true",
777 help="enable local echo (default off)",
778 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000779
Chris Liechti033f17c2015-08-30 21:28:04 +0200780 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100781 "--encoding",
782 dest="serial_port_encoding",
783 metavar="CODEC",
784 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
785 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000786
Chris Liechti033f17c2015-08-30 21:28:04 +0200787 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100788 "-f", "--filter",
789 action="append",
790 metavar="NAME",
791 help="add text transformation",
792 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200793
Chris Liechti033f17c2015-08-30 21:28:04 +0200794 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100795 "--eol",
796 choices=['CR', 'LF', 'CRLF'],
797 type=lambda c: c.upper(),
798 help="end of line mode",
799 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000800
Chris Liechti033f17c2015-08-30 21:28:04 +0200801 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100802 "--raw",
803 action="store_true",
804 help="Do no apply any encodings/transformations",
805 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000806
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200807 group = parser.add_argument_group("hotkeys")
cliechtib7d746d2006-03-28 22:44:30 +0000808
Chris Liechti033f17c2015-08-30 21:28:04 +0200809 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100810 "--exit-char",
811 type=int,
812 metavar='NUM',
813 help="Unicode of special character that is used to exit the application, default: %(default)s",
814 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000815
Chris Liechti033f17c2015-08-30 21:28:04 +0200816 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100817 "--menu-char",
818 type=int,
819 metavar='NUM',
820 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
821 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000822
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200823 group = parser.add_argument_group("diagnostics")
cliechti6385f2c2005-09-21 19:51:19 +0000824
Chris Liechti033f17c2015-08-30 21:28:04 +0200825 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100826 "-q", "--quiet",
827 action="store_true",
828 help="suppress non-error messages",
829 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000830
Chris Liechti033f17c2015-08-30 21:28:04 +0200831 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100832 "--develop",
833 action="store_true",
834 help="show Python traceback on error",
835 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000836
Chris Liechtib7550bd2015-08-15 04:09:10 +0200837 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000838
Chris Liechtib7550bd2015-08-15 04:09:10 +0200839 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000840 parser.error('--exit-char can not be the same as --menu-char')
841
Chris Liechtib3df13e2015-08-25 02:20:09 +0200842 if args.filter:
843 if 'help' in args.filter:
844 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200845 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100846 '{:<10} = {.__doc__}'.format(k, v)
847 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200848 sys.stderr.write('\n')
849 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200850 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200851 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200852 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200853
Chris Liechti00f84282015-12-24 23:40:34 +0100854 while True:
855 # no port given on command line -> ask user now
856 if args.port is None or args.port == '-':
857 try:
858 args.port = ask_for_port()
859 except KeyboardInterrupt:
860 sys.stderr.write('\n')
861 parser.error('user aborted and port is not given')
862 else:
863 if not args.port:
864 parser.error('port is not given')
865 try:
866 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100867 args.port,
868 args.baudrate,
869 parity=args.parity,
870 rtscts=args.rtscts,
871 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100872 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200873
Chris Liechtif542fca2016-05-13 00:20:14 +0200874 if not hasattr(serial_instance, 'cancel_read'):
875 # enable timeout for alive flag polling if cancel_read is not available
876 serial_instance.timeout = 1
877
Chris Liechti00f84282015-12-24 23:40:34 +0100878 if args.dtr is not None:
879 if not args.quiet:
880 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
881 serial_instance.dtr = args.dtr
882 if args.rts is not None:
883 if not args.quiet:
884 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
885 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200886
Chris Liechti00f84282015-12-24 23:40:34 +0100887 serial_instance.open()
888 except serial.SerialException as e:
889 sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
890 if args.develop:
891 raise
892 if not args.ask:
893 sys.exit(1)
894 else:
895 args.port = '-'
896 else:
897 break
cliechti6385f2c2005-09-21 19:51:19 +0000898
Chris Liechti3b454802015-08-26 23:39:59 +0200899 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100900 serial_instance,
901 echo=args.echo,
902 eol=args.eol.lower(),
903 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200904 miniterm.exit_character = unichr(args.exit_char)
905 miniterm.menu_character = unichr(args.menu_char)
906 miniterm.raw = args.raw
907 miniterm.set_rx_encoding(args.serial_port_encoding)
908 miniterm.set_tx_encoding(args.serial_port_encoding)
909
Chris Liechtib7550bd2015-08-15 04:09:10 +0200910 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200911 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100912 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200913 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100914 key_description(miniterm.exit_character),
915 key_description(miniterm.menu_character),
916 key_description(miniterm.menu_character),
917 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +0000918
cliechti6385f2c2005-09-21 19:51:19 +0000919 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +0000920 try:
921 miniterm.join(True)
922 except KeyboardInterrupt:
923 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +0200924 if not args.quiet:
cliechtibf6bb7d2006-03-30 00:28:18 +0000925 sys.stderr.write("\n--- exit ---\n")
cliechti6385f2c2005-09-21 19:51:19 +0000926 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +0200927 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +0000928
cliechti5370cee2013-10-13 03:08:19 +0000929# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +0000930if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +0000931 main()