blob: 145996f1284585aac1451727f6c87e505357a7d1 [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:
Chris Liechtic8f3f822016-06-08 03:35:28 +020035 return 'Ctrl+{:c}'.format(ord('@') + ascii_code)
cliechti6c8eb2f2009-07-08 02:10:46 +000036 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):
Chris Liechti89313c92015-09-01 02:33:13 +0200325 sys.stderr.write('--- {:2}: {:20} {}\n'.format(n, port, desc))
326 ports.append(port)
327 while True:
328 port = raw_input('--- Enter port index or full name: ')
329 try:
330 index = int(port) - 1
331 if not 0 <= index < len(ports):
332 sys.stderr.write('--- Invalid index!\n')
333 continue
334 except ValueError:
335 pass
336 else:
337 port = ports[index]
338 return port
cliechti1351dde2012-04-12 16:47:47 +0000339
340
cliechti8c2ea842011-03-18 01:51:46 +0000341class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200342 """\
343 Terminal application. Copy data from serial port to console and vice versa.
344 Handle special keys from the console to show menu etc.
345 """
346
Chris Liechti3b454802015-08-26 23:39:59 +0200347 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200348 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200349 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000350 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200351 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200352 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200353 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200354 self.eol = eol
355 self.filters = filters
356 self.update_transformations()
Chris Liechti442bf512015-08-15 01:42:24 +0200357 self.exit_character = 0x1d # GS/CTRL+]
358 self.menu_character = 0x14 # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100359 self.alive = None
360 self._reader_alive = None
361 self.receiver_thread = None
362 self.rx_decoder = None
363 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000364
cliechti8c2ea842011-03-18 01:51:46 +0000365 def _start_reader(self):
366 """Start reader thread"""
367 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000368 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200369 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
370 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000371 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000372
373 def _stop_reader(self):
374 """Stop reader thread only, wait for clean exit of thread"""
375 self._reader_alive = False
Chris Liechti933a5172016-05-04 16:12:15 +0200376 if hasattr(self.serial, 'cancel_read'):
377 self.serial.cancel_read()
cliechti8c2ea842011-03-18 01:51:46 +0000378 self.receiver_thread.join()
379
cliechti8c2ea842011-03-18 01:51:46 +0000380 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100381 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000382 self.alive = True
383 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000384 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200385 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
386 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000387 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200388 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000389
cliechti6385f2c2005-09-21 19:51:19 +0000390 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100391 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000392 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000393
cliechtibf6bb7d2006-03-30 00:28:18 +0000394 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100395 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000396 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000397 if not transmit_only:
Chris Liechti933a5172016-05-04 16:12:15 +0200398 if hasattr(self.serial, 'cancel_read'):
399 self.serial.cancel_read()
cliechtibf6bb7d2006-03-30 00:28:18 +0000400 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000401
Chris Liechti933a5172016-05-04 16:12:15 +0200402 def close(self):
403 self.serial.close()
404
Chris Liechtib3df13e2015-08-25 02:20:09 +0200405 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100406 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100407 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
408 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200409 self.tx_transformations = [t() for t in transformations]
410 self.rx_transformations = list(reversed(self.tx_transformations))
411
Chris Liechtid698af72015-08-24 20:24:55 +0200412 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100413 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200414 self.input_encoding = encoding
415 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
416
417 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100418 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200419 self.output_encoding = encoding
420 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
421
cliechti6c8eb2f2009-07-08 02:10:46 +0000422 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100423 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200424 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100425 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200426 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100427 ('active' if self.serial.rts else 'inactive'),
428 ('active' if self.serial.dtr else 'inactive'),
429 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000430 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200431 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100432 ('active' if self.serial.cts else 'inactive'),
433 ('active' if self.serial.dsr else 'inactive'),
434 ('active' if self.serial.ri else 'inactive'),
435 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000436 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200437 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000438 # yet received. ignore this error.
439 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200440 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
441 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200442 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
443 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200444 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
445 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000446
cliechti6385f2c2005-09-21 19:51:19 +0000447 def reader(self):
448 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000449 try:
cliechti8c2ea842011-03-18 01:51:46 +0000450 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200451 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200452 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200453 if data:
454 if self.raw:
455 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000456 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200457 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200458 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200459 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200460 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200461 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000462 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200463 self.console.cancel()
464 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000465
cliechti6385f2c2005-09-21 19:51:19 +0000466 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000467 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200468 Loop and copy console->serial until self.exit_character character is
469 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000470 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000471 """
472 menu_active = False
473 try:
474 while self.alive:
475 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200476 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000477 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200478 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200479 if not self.alive:
480 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000481 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200482 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000483 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200484 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200485 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200486 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200487 self.stop() # exit app
488 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000489 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200490 #~ if self.raw:
491 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200492 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200493 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200494 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000495 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200496 echo_text = c
497 for transformation in self.tx_transformations:
498 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200499 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000500 except:
501 self.alive = False
502 raise
cliechti6385f2c2005-09-21 19:51:19 +0000503
Chris Liechti7af7c752015-08-12 15:45:19 +0200504 def handle_menu_key(self, c):
505 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200506 if c == self.menu_character or c == self.exit_character:
507 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200508 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200509 if self.echo:
510 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200511 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti7af7c752015-08-12 15:45:19 +0200512 sys.stderr.write('\n--- File to upload: ')
513 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200514 with self.console:
515 filename = sys.stdin.readline().rstrip('\r\n')
516 if filename:
517 try:
518 with open(filename, 'rb') as f:
519 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
520 while True:
521 block = f.read(1024)
522 if not block:
523 break
524 self.serial.write(block)
525 # Wait for output buffer to drain.
526 self.serial.flush()
527 sys.stderr.write('.') # Progress indicator.
528 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
529 except IOError as e:
530 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200531 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200532 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200533 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200534 self.serial.rts = not self.serial.rts
535 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200536 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200537 self.serial.dtr = not self.serial.dtr
538 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200539 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200540 self.serial.break_condition = not self.serial.break_condition
541 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200542 elif c == '\x05': # CTRL+E -> toggle local echo
543 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200544 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200545 elif c == '\x06': # CTRL+F -> edit filters
546 sys.stderr.write('\n--- Available Filters:\n')
547 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100548 '--- {:<10} = {.__doc__}'.format(k, v)
549 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200550 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
551 with self.console:
552 new_filters = sys.stdin.readline().lower().split()
553 if new_filters:
554 for f in new_filters:
555 if f not in TRANSFORMATIONS:
556 sys.stderr.write('--- unknown filter: {}'.format(repr(f)))
557 break
558 else:
559 self.filters = new_filters
560 self.update_transformations()
561 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
562 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti033f17c2015-08-30 21:28:04 +0200563 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200564 eol = modes.index(self.eol) + 1
565 if eol >= len(modes):
566 eol = 0
567 self.eol = modes[eol]
568 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
569 self.update_transformations()
570 elif c == '\x01': # CTRL+A -> set encoding
571 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
572 with self.console:
573 new_encoding = sys.stdin.readline().strip()
574 if new_encoding:
575 try:
576 codecs.lookup(new_encoding)
577 except LookupError:
578 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
579 else:
580 self.set_rx_encoding(new_encoding)
581 self.set_tx_encoding(new_encoding)
582 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
583 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechti7af7c752015-08-12 15:45:19 +0200584 elif c == '\x09': # CTRL+I -> info
585 self.dump_port_settings()
586 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
587 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
588 elif c in 'pP': # P -> change port
Chris Liechti269f77b2015-08-24 01:31:42 +0200589 with self.console:
590 try:
Chris Liechti89313c92015-09-01 02:33:13 +0200591 port = ask_for_port()
Chris Liechti269f77b2015-08-24 01:31:42 +0200592 except KeyboardInterrupt:
593 port = None
Chris Liechti7af7c752015-08-12 15:45:19 +0200594 if port and port != self.serial.port:
595 # reader thread needs to be shut down
596 self._stop_reader()
597 # save settings
598 settings = self.serial.getSettingsDict()
599 try:
600 new_serial = serial.serial_for_url(port, do_not_open=True)
601 # restore settings and open
602 new_serial.applySettingsDict(settings)
Chris Liechti89313c92015-09-01 02:33:13 +0200603 new_serial.rts = self.serial.rts
604 new_serial.dtr = self.serial.dtr
Chris Liechti7af7c752015-08-12 15:45:19 +0200605 new_serial.open()
Chris Liechti89313c92015-09-01 02:33:13 +0200606 new_serial.break_condition = self.serial.break_condition
Chris Liechti7af7c752015-08-12 15:45:19 +0200607 except Exception as e:
Chris Liechti442bf512015-08-15 01:42:24 +0200608 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200609 new_serial.close()
610 else:
611 self.serial.close()
612 self.serial = new_serial
Chris Liechti442bf512015-08-15 01:42:24 +0200613 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
Chris Liechti7af7c752015-08-12 15:45:19 +0200614 # and restart the reader thread
615 self._start_reader()
616 elif c in 'bB': # B -> change baudrate
617 sys.stderr.write('\n--- Baudrate: ')
618 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200619 with self.console:
620 backup = self.serial.baudrate
621 try:
622 self.serial.baudrate = int(sys.stdin.readline().strip())
623 except ValueError as e:
Chris Liechti4d541e22016-02-13 23:13:59 +0100624 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
Chris Liechti269f77b2015-08-24 01:31:42 +0200625 self.serial.baudrate = backup
626 else:
627 self.dump_port_settings()
Chris Liechti7af7c752015-08-12 15:45:19 +0200628 elif c == '8': # 8 -> change to 8 bits
629 self.serial.bytesize = serial.EIGHTBITS
630 self.dump_port_settings()
631 elif c == '7': # 7 -> change to 8 bits
632 self.serial.bytesize = serial.SEVENBITS
633 self.dump_port_settings()
634 elif c in 'eE': # E -> change to even parity
635 self.serial.parity = serial.PARITY_EVEN
636 self.dump_port_settings()
637 elif c in 'oO': # O -> change to odd parity
638 self.serial.parity = serial.PARITY_ODD
639 self.dump_port_settings()
640 elif c in 'mM': # M -> change to mark parity
641 self.serial.parity = serial.PARITY_MARK
642 self.dump_port_settings()
643 elif c in 'sS': # S -> change to space parity
644 self.serial.parity = serial.PARITY_SPACE
645 self.dump_port_settings()
646 elif c in 'nN': # N -> change to no parity
647 self.serial.parity = serial.PARITY_NONE
648 self.dump_port_settings()
649 elif c == '1': # 1 -> change to 1 stop bits
650 self.serial.stopbits = serial.STOPBITS_ONE
651 self.dump_port_settings()
652 elif c == '2': # 2 -> change to 2 stop bits
653 self.serial.stopbits = serial.STOPBITS_TWO
654 self.dump_port_settings()
655 elif c == '3': # 3 -> change to 1.5 stop bits
656 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
657 self.dump_port_settings()
658 elif c in 'xX': # X -> change software flow control
659 self.serial.xonxoff = (c == 'X')
660 self.dump_port_settings()
661 elif c in 'rR': # R -> change hardware flow control
662 self.serial.rtscts = (c == 'R')
663 self.dump_port_settings()
664 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200665 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
666
667 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100668 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200669 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200670 return """
671--- pySerial ({version}) - miniterm - help
672---
673--- {exit:8} Exit program
674--- {menu:8} Menu escape key, followed by:
675--- Menu keys:
676--- {menu:7} Send the menu character itself to remote
677--- {exit:7} Send the exit character itself to remote
678--- {info:7} Show info
679--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200680--- {repr:7} encoding
681--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200682--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200683--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
684--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200685---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200686--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200687--- p change port
688--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200689--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200690--- 1 2 3 set stop bits (1, 2, 1.5)
691--- b change baud rate
692--- x X disable/enable software flow control
693--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100694""".format(version=getattr(serial, 'VERSION', 'unknown version'),
695 exit=key_description(self.exit_character),
696 menu=key_description(self.menu_character),
697 rts=key_description('\x12'),
698 dtr=key_description('\x04'),
699 brk=key_description('\x02'),
700 echo=key_description('\x05'),
701 info=key_description('\x09'),
702 upload=key_description('\x15'),
703 repr=key_description('\x01'),
704 filter=key_description('\x06'),
705 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200706
707
Chris Liechtib3df13e2015-08-25 02:20:09 +0200708# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200709# default args can be used to override when calling main() from an other script
710# e.g to create a miniterm-my-device.py
711def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100712 """Command line tool, entry point"""
713
Chris Liechtib7550bd2015-08-15 04:09:10 +0200714 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000715
Chris Liechtib7550bd2015-08-15 04:09:10 +0200716 parser = argparse.ArgumentParser(
Chris Liechti397cf412016-02-11 00:11:48 +0100717 description="Miniterm - A simple terminal program for the serial port.")
cliechti6385f2c2005-09-21 19:51:19 +0000718
Chris Liechti033f17c2015-08-30 21:28:04 +0200719 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100720 "port",
721 nargs='?',
722 help="serial port name ('-' to show port list)",
723 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000724
Chris Liechti033f17c2015-08-30 21:28:04 +0200725 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100726 "baudrate",
727 nargs='?',
728 type=int,
729 help="set baud rate, default: %(default)s",
730 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000731
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200732 group = parser.add_argument_group("port settings")
cliechti53edb472009-02-06 21:18:46 +0000733
Chris Liechti033f17c2015-08-30 21:28:04 +0200734 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100735 "--parity",
736 choices=['N', 'E', 'O', 'S', 'M'],
737 type=lambda c: c.upper(),
738 help="set parity, one of {N E O S M}, default: N",
739 default='N')
cliechti53edb472009-02-06 21:18:46 +0000740
Chris Liechti033f17c2015-08-30 21:28:04 +0200741 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100742 "--rtscts",
743 action="store_true",
744 help="enable RTS/CTS flow control (default off)",
745 default=False)
cliechti53edb472009-02-06 21:18:46 +0000746
Chris Liechti033f17c2015-08-30 21:28:04 +0200747 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100748 "--xonxoff",
749 action="store_true",
750 help="enable software flow control (default off)",
751 default=False)
cliechti53edb472009-02-06 21:18:46 +0000752
Chris Liechti033f17c2015-08-30 21:28:04 +0200753 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100754 "--rts",
755 type=int,
756 help="set initial RTS line state (possible values: 0, 1)",
757 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000758
Chris Liechti033f17c2015-08-30 21:28:04 +0200759 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100760 "--dtr",
761 type=int,
762 help="set initial DTR line state (possible values: 0, 1)",
763 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000764
Chris Liechti00f84282015-12-24 23:40:34 +0100765 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100766 "--ask",
767 action="store_true",
768 help="ask again for port when open fails",
769 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100770
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200771 group = parser.add_argument_group("data handling")
cliechti5370cee2013-10-13 03:08:19 +0000772
Chris Liechti033f17c2015-08-30 21:28:04 +0200773 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100774 "-e", "--echo",
775 action="store_true",
776 help="enable local echo (default off)",
777 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000778
Chris Liechti033f17c2015-08-30 21:28:04 +0200779 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100780 "--encoding",
781 dest="serial_port_encoding",
782 metavar="CODEC",
783 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
784 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000785
Chris Liechti033f17c2015-08-30 21:28:04 +0200786 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100787 "-f", "--filter",
788 action="append",
789 metavar="NAME",
790 help="add text transformation",
791 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200792
Chris Liechti033f17c2015-08-30 21:28:04 +0200793 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100794 "--eol",
795 choices=['CR', 'LF', 'CRLF'],
796 type=lambda c: c.upper(),
797 help="end of line mode",
798 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000799
Chris Liechti033f17c2015-08-30 21:28:04 +0200800 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100801 "--raw",
802 action="store_true",
803 help="Do no apply any encodings/transformations",
804 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000805
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200806 group = parser.add_argument_group("hotkeys")
cliechtib7d746d2006-03-28 22:44:30 +0000807
Chris Liechti033f17c2015-08-30 21:28:04 +0200808 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100809 "--exit-char",
810 type=int,
811 metavar='NUM',
812 help="Unicode of special character that is used to exit the application, default: %(default)s",
813 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000814
Chris Liechti033f17c2015-08-30 21:28:04 +0200815 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100816 "--menu-char",
817 type=int,
818 metavar='NUM',
819 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
820 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000821
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200822 group = parser.add_argument_group("diagnostics")
cliechti6385f2c2005-09-21 19:51:19 +0000823
Chris Liechti033f17c2015-08-30 21:28:04 +0200824 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100825 "-q", "--quiet",
826 action="store_true",
827 help="suppress non-error messages",
828 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000829
Chris Liechti033f17c2015-08-30 21:28:04 +0200830 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100831 "--develop",
832 action="store_true",
833 help="show Python traceback on error",
834 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000835
Chris Liechtib7550bd2015-08-15 04:09:10 +0200836 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000837
Chris Liechtib7550bd2015-08-15 04:09:10 +0200838 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000839 parser.error('--exit-char can not be the same as --menu-char')
840
Chris Liechtib3df13e2015-08-25 02:20:09 +0200841 if args.filter:
842 if 'help' in args.filter:
843 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200844 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100845 '{:<10} = {.__doc__}'.format(k, v)
846 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200847 sys.stderr.write('\n')
848 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200849 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200850 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200851 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200852
Chris Liechti00f84282015-12-24 23:40:34 +0100853 while True:
854 # no port given on command line -> ask user now
855 if args.port is None or args.port == '-':
856 try:
857 args.port = ask_for_port()
858 except KeyboardInterrupt:
859 sys.stderr.write('\n')
860 parser.error('user aborted and port is not given')
861 else:
862 if not args.port:
863 parser.error('port is not given')
864 try:
865 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100866 args.port,
867 args.baudrate,
868 parity=args.parity,
869 rtscts=args.rtscts,
870 xonxoff=args.xonxoff,
Chris Liechti397cf412016-02-11 00:11:48 +0100871 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200872
Chris Liechtif542fca2016-05-13 00:20:14 +0200873 if not hasattr(serial_instance, 'cancel_read'):
874 # enable timeout for alive flag polling if cancel_read is not available
875 serial_instance.timeout = 1
876
Chris Liechti00f84282015-12-24 23:40:34 +0100877 if args.dtr is not None:
878 if not args.quiet:
879 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
880 serial_instance.dtr = args.dtr
881 if args.rts is not None:
882 if not args.quiet:
883 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
884 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200885
Chris Liechti00f84282015-12-24 23:40:34 +0100886 serial_instance.open()
887 except serial.SerialException as e:
888 sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
889 if args.develop:
890 raise
891 if not args.ask:
892 sys.exit(1)
893 else:
894 args.port = '-'
895 else:
896 break
cliechti6385f2c2005-09-21 19:51:19 +0000897
Chris Liechti3b454802015-08-26 23:39:59 +0200898 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100899 serial_instance,
900 echo=args.echo,
901 eol=args.eol.lower(),
902 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200903 miniterm.exit_character = unichr(args.exit_char)
904 miniterm.menu_character = unichr(args.menu_char)
905 miniterm.raw = args.raw
906 miniterm.set_rx_encoding(args.serial_port_encoding)
907 miniterm.set_tx_encoding(args.serial_port_encoding)
908
Chris Liechtib7550bd2015-08-15 04:09:10 +0200909 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200910 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100911 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200912 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100913 key_description(miniterm.exit_character),
914 key_description(miniterm.menu_character),
915 key_description(miniterm.menu_character),
916 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +0000917
cliechti6385f2c2005-09-21 19:51:19 +0000918 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +0000919 try:
920 miniterm.join(True)
921 except KeyboardInterrupt:
922 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +0200923 if not args.quiet:
cliechtibf6bb7d2006-03-30 00:28:18 +0000924 sys.stderr.write("\n--- exit ---\n")
cliechti6385f2c2005-09-21 19:51:19 +0000925 miniterm.join()
Chris Liechti933a5172016-05-04 16:12:15 +0200926 miniterm.close()
cliechtibf6bb7d2006-03-30 00:28:18 +0000927
cliechti5370cee2013-10-13 03:08:19 +0000928# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +0000929if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +0000930 main()