blob: 211068884b0f238ecca05d93e433a1ff2a54c056 [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):
130 hwnd = ctypes.windll.kernel32.GetConsoleWindow()
131 ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
132
cliechti576de252002-02-28 23:54:44 +0000133elif os.name == 'posix':
Chris Liechtia1d5c6d2015-08-07 14:41:24 +0200134 import atexit
135 import termios
Chris Liechti9cc696b2015-08-28 00:54:22 +0200136
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200137 class Console(ConsoleBase):
cliechti9c592b32008-06-16 22:00:14 +0000138 def __init__(self):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200139 super(Console, self).__init__()
cliechti9c592b32008-06-16 22:00:14 +0000140 self.fd = sys.stdin.fileno()
Chris Liechti4d989c22015-08-24 00:24:49 +0200141 self.old = termios.tcgetattr(self.fd)
Chris Liechti89eb2472015-08-08 17:06:25 +0200142 atexit.register(self.cleanup)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200143 if sys.version_info < (3, 0):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200144 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
145 else:
146 self.enc_stdin = sys.stdin
cliechti9c592b32008-06-16 22:00:14 +0000147
148 def setup(self):
cliechti9c592b32008-06-16 22:00:14 +0000149 new = termios.tcgetattr(self.fd)
150 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
151 new[6][termios.VMIN] = 1
152 new[6][termios.VTIME] = 0
153 termios.tcsetattr(self.fd, termios.TCSANOW, new)
cliechti53edb472009-02-06 21:18:46 +0000154
cliechti9c592b32008-06-16 22:00:14 +0000155 def getkey(self):
Chris Liechtia7e7b692015-08-25 21:10:28 +0200156 c = self.enc_stdin.read(1)
Chris Liechti9f398812015-09-13 18:50:44 +0200157 if c == unichr(0x7f):
158 c = unichr(8) # map the BS key (which yields DEL) to backspace
Chris Liechti9a720852015-08-25 00:20:38 +0200159 return c
cliechti53edb472009-02-06 21:18:46 +0000160
cliechti9c592b32008-06-16 22:00:14 +0000161 def cleanup(self):
Chris Liechti4d989c22015-08-24 00:24:49 +0200162 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
cliechti9c592b32008-06-16 22:00:14 +0000163
cliechti576de252002-02-28 23:54:44 +0000164else:
Chris Liechti397cf412016-02-11 00:11:48 +0100165 raise NotImplementedError(
166 'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
cliechti576de252002-02-28 23:54:44 +0000167
cliechti6fa76fb2009-07-08 23:53:39 +0000168
Chris Liechti9a720852015-08-25 00:20:38 +0200169# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200170
171class Transform(object):
Chris Liechticbb00b22015-08-13 22:58:49 +0200172 """do-nothing: forward all data unchanged"""
Chris Liechtid698af72015-08-24 20:24:55 +0200173 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200174 """text received from serial port"""
175 return text
176
Chris Liechtid698af72015-08-24 20:24:55 +0200177 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200178 """text to be sent to serial port"""
179 return text
180
181 def echo(self, text):
182 """text to be sent but displayed on console"""
183 return text
184
Chris Liechti442bf512015-08-15 01:42:24 +0200185
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200186class CRLF(Transform):
187 """ENTER sends CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200188
Chris Liechtid698af72015-08-24 20:24:55 +0200189 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200190 return text.replace('\n', '\r\n')
191
Chris Liechti442bf512015-08-15 01:42:24 +0200192
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200193class CR(Transform):
194 """ENTER sends CR"""
Chris Liechtid698af72015-08-24 20:24:55 +0200195
196 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200197 return text.replace('\r', '\n')
198
Chris Liechtid698af72015-08-24 20:24:55 +0200199 def tx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200200 return text.replace('\n', '\r')
201
Chris Liechti442bf512015-08-15 01:42:24 +0200202
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200203class LF(Transform):
204 """ENTER sends LF"""
205
206
207class NoTerminal(Transform):
208 """remove typical terminal control codes from input"""
Chris Liechti9a720852015-08-25 00:20:38 +0200209
210 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 +0100211 REPLACEMENT_MAP.update(
212 {
Chris Liechti033f17c2015-08-30 21:28:04 +0200213 0x7F: 0x2421, # DEL
214 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100215 })
Chris Liechti9a720852015-08-25 00:20:38 +0200216
Chris Liechtid698af72015-08-24 20:24:55 +0200217 def rx(self, text):
Chris Liechti9a720852015-08-25 00:20:38 +0200218 return text.translate(self.REPLACEMENT_MAP)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200219
Chris Liechtid698af72015-08-24 20:24:55 +0200220 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200221
222
Chris Liechti9a720852015-08-25 00:20:38 +0200223class NoControls(NoTerminal):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200224 """Remove all control codes, incl. CR+LF"""
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200225
Chris Liechti9a720852015-08-25 00:20:38 +0200226 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
Chris Liechtiba45c522016-02-06 23:53:23 +0100227 REPLACEMENT_MAP.update(
228 {
Chris Liechtia887c932016-02-13 23:10:14 +0100229 0x20: 0x2423, # visual space
Chris Liechti033f17c2015-08-30 21:28:04 +0200230 0x7F: 0x2421, # DEL
231 0x9B: 0x2425, # CSI
Chris Liechtiba45c522016-02-06 23:53:23 +0100232 })
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200233
234
235class Printable(Transform):
Chris Liechtid698af72015-08-24 20:24:55 +0200236 """Show decimal code for all non-ASCII characters and replace most control codes"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200237
Chris Liechtid698af72015-08-24 20:24:55 +0200238 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200239 r = []
Chris Liechtia887c932016-02-13 23:10:14 +0100240 for c in text:
241 if ' ' <= c < '\x7f' or c in '\r\n\b\t':
242 r.append(c)
243 elif c < ' ':
244 r.append(unichr(0x2400 + ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200245 else:
Chris Liechtia887c932016-02-13 23:10:14 +0100246 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200247 r.append(' ')
248 return ''.join(r)
249
Chris Liechtid698af72015-08-24 20:24:55 +0200250 echo = rx
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200251
252
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200253class Colorize(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200254 """Apply different colors for received and echo"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200255
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200256 def __init__(self):
257 # XXX make it configurable, use colorama?
258 self.input_color = '\x1b[37m'
259 self.echo_color = '\x1b[31m'
260
Chris Liechtid698af72015-08-24 20:24:55 +0200261 def rx(self, text):
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200262 return self.input_color + text
263
264 def echo(self, text):
265 return self.echo_color + text
266
Chris Liechti442bf512015-08-15 01:42:24 +0200267
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200268class DebugIO(Transform):
Chris Liechti442bf512015-08-15 01:42:24 +0200269 """Print what is sent and received"""
Chris Liechtic0c660a2015-08-25 00:55:51 +0200270
Chris Liechtid698af72015-08-24 20:24:55 +0200271 def rx(self, text):
Chris Liechtie1384382015-08-15 17:06:05 +0200272 sys.stderr.write(' [RX:{}] '.format(repr(text)))
273 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200274 return text
275
Chris Liechtid698af72015-08-24 20:24:55 +0200276 def tx(self, text):
Chris Liechtie1384382015-08-15 17:06:05 +0200277 sys.stderr.write(' [TX:{}] '.format(repr(text)))
278 sys.stderr.flush()
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200279 return text
280
Chris Liechti442bf512015-08-15 01:42:24 +0200281
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200282# other ideas:
283# - add date/time for each newline
284# - insert newline after: a) timeout b) packet end character
285
Chris Liechtib3df13e2015-08-25 02:20:09 +0200286EOL_TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100287 'crlf': CRLF,
288 'cr': CR,
289 'lf': LF,
290}
Chris Liechtib3df13e2015-08-25 02:20:09 +0200291
292TRANSFORMATIONS = {
Chris Liechtiba45c522016-02-06 23:53:23 +0100293 'direct': Transform, # no transformation
294 'default': NoTerminal,
295 'nocontrol': NoControls,
296 'printable': Printable,
297 'colorize': Colorize,
298 'debug': DebugIO,
299}
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200300
301
Chris Liechti033f17c2015-08-30 21:28:04 +0200302# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti89313c92015-09-01 02:33:13 +0200303def ask_for_port():
304 """\
305 Show a list of ports and ask the user for a choice. To make selection
306 easier on systems with long device names, also allow the input of an
307 index.
308 """
309 sys.stderr.write('\n--- Available ports:\n')
310 ports = []
311 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
312 #~ sys.stderr.write('--- %-20s %s [%s]\n' % (port, desc, hwid))
313 sys.stderr.write('--- {:2}: {:20} {}\n'.format(n, port, desc))
314 ports.append(port)
315 while True:
316 port = raw_input('--- Enter port index or full name: ')
317 try:
318 index = int(port) - 1
319 if not 0 <= index < len(ports):
320 sys.stderr.write('--- Invalid index!\n')
321 continue
322 except ValueError:
323 pass
324 else:
325 port = ports[index]
326 return port
cliechti1351dde2012-04-12 16:47:47 +0000327
328
cliechti8c2ea842011-03-18 01:51:46 +0000329class Miniterm(object):
Chris Liechti89313c92015-09-01 02:33:13 +0200330 """\
331 Terminal application. Copy data from serial port to console and vice versa.
332 Handle special keys from the console to show menu etc.
333 """
334
Chris Liechti3b454802015-08-26 23:39:59 +0200335 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
Chris Liechti89eb2472015-08-08 17:06:25 +0200336 self.console = Console()
Chris Liechti3b454802015-08-26 23:39:59 +0200337 self.serial = serial_instance
cliechti6385f2c2005-09-21 19:51:19 +0000338 self.echo = echo
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200339 self.raw = False
Chris Liechti442bf512015-08-15 01:42:24 +0200340 self.input_encoding = 'UTF-8'
Chris Liechti442bf512015-08-15 01:42:24 +0200341 self.output_encoding = 'UTF-8'
Chris Liechtib3df13e2015-08-25 02:20:09 +0200342 self.eol = eol
343 self.filters = filters
344 self.update_transformations()
Chris Liechti442bf512015-08-15 01:42:24 +0200345 self.exit_character = 0x1d # GS/CTRL+]
346 self.menu_character = 0x14 # Menu: CTRL+T
Chris Liechti397cf412016-02-11 00:11:48 +0100347 self.alive = None
348 self._reader_alive = None
349 self.receiver_thread = None
350 self.rx_decoder = None
351 self.tx_decoder = None
cliechti576de252002-02-28 23:54:44 +0000352
cliechti8c2ea842011-03-18 01:51:46 +0000353 def _start_reader(self):
354 """Start reader thread"""
355 self._reader_alive = True
cliechti6fa76fb2009-07-08 23:53:39 +0000356 # start serial->console thread
Chris Liechti55ba7d92015-08-15 16:33:51 +0200357 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
358 self.receiver_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000359 self.receiver_thread.start()
cliechti8c2ea842011-03-18 01:51:46 +0000360
361 def _stop_reader(self):
362 """Stop reader thread only, wait for clean exit of thread"""
363 self._reader_alive = False
364 self.receiver_thread.join()
365
cliechti8c2ea842011-03-18 01:51:46 +0000366 def start(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100367 """start worker threads"""
cliechti8c2ea842011-03-18 01:51:46 +0000368 self.alive = True
369 self._start_reader()
cliechti6fa76fb2009-07-08 23:53:39 +0000370 # enter console->serial loop
Chris Liechti55ba7d92015-08-15 16:33:51 +0200371 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
372 self.transmitter_thread.daemon = True
cliechti6385f2c2005-09-21 19:51:19 +0000373 self.transmitter_thread.start()
Chris Liechti89eb2472015-08-08 17:06:25 +0200374 self.console.setup()
cliechti53edb472009-02-06 21:18:46 +0000375
cliechti6385f2c2005-09-21 19:51:19 +0000376 def stop(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100377 """set flag to stop worker threads"""
cliechti6385f2c2005-09-21 19:51:19 +0000378 self.alive = False
cliechti53edb472009-02-06 21:18:46 +0000379
cliechtibf6bb7d2006-03-30 00:28:18 +0000380 def join(self, transmit_only=False):
Chris Liechtia887c932016-02-13 23:10:14 +0100381 """wait for worker threads to terminate"""
cliechti6385f2c2005-09-21 19:51:19 +0000382 self.transmitter_thread.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000383 if not transmit_only:
384 self.receiver_thread.join()
cliechti6385f2c2005-09-21 19:51:19 +0000385
Chris Liechtib3df13e2015-08-25 02:20:09 +0200386 def update_transformations(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100387 """take list of transformation classes and instantiate them for rx and tx"""
Chris Liechti397cf412016-02-11 00:11:48 +0100388 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
389 for f in self.filters]
Chris Liechtib3df13e2015-08-25 02:20:09 +0200390 self.tx_transformations = [t() for t in transformations]
391 self.rx_transformations = list(reversed(self.tx_transformations))
392
Chris Liechtid698af72015-08-24 20:24:55 +0200393 def set_rx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100394 """set encoding for received data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200395 self.input_encoding = encoding
396 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
397
398 def set_tx_encoding(self, encoding, errors='replace'):
Chris Liechtia887c932016-02-13 23:10:14 +0100399 """set encoding for transmitted data"""
Chris Liechtid698af72015-08-24 20:24:55 +0200400 self.output_encoding = encoding
401 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
402
cliechti6c8eb2f2009-07-08 02:10:46 +0000403 def dump_port_settings(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100404 """Write current settings to sys.stderr"""
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200405 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
Chris Liechti397cf412016-02-11 00:11:48 +0100406 p=self.serial))
Chris Liechti442bf512015-08-15 01:42:24 +0200407 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100408 ('active' if self.serial.rts else 'inactive'),
409 ('active' if self.serial.dtr else 'inactive'),
410 ('active' if self.serial.break_condition else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000411 try:
Chris Liechti442bf512015-08-15 01:42:24 +0200412 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100413 ('active' if self.serial.cts else 'inactive'),
414 ('active' if self.serial.dsr else 'inactive'),
415 ('active' if self.serial.ri else 'inactive'),
416 ('active' if self.serial.cd else 'inactive')))
cliechti10114572009-08-05 23:40:50 +0000417 except serial.SerialException:
Chris Liechti55ba7d92015-08-15 16:33:51 +0200418 # on RFC 2217 ports, it can happen if no modem state notification was
cliechti10114572009-08-05 23:40:50 +0000419 # yet received. ignore this error.
420 pass
Chris Liechti442bf512015-08-15 01:42:24 +0200421 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
422 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
Chris Liechti442bf512015-08-15 01:42:24 +0200423 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
424 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200425 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
426 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
cliechti6c8eb2f2009-07-08 02:10:46 +0000427
cliechti6385f2c2005-09-21 19:51:19 +0000428 def reader(self):
429 """loop and copy serial->console"""
cliechti6963b262010-01-02 03:01:21 +0000430 try:
cliechti8c2ea842011-03-18 01:51:46 +0000431 while self.alive and self._reader_alive:
Chris Liechti188cf592015-08-22 00:28:19 +0200432 # read all that is there or wait for one byte
Chris Liechti3b454802015-08-26 23:39:59 +0200433 data = self.serial.read(self.serial.in_waiting or 1)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200434 if data:
435 if self.raw:
436 self.console.write_bytes(data)
cliechti6963b262010-01-02 03:01:21 +0000437 else:
Chris Liechtid698af72015-08-24 20:24:55 +0200438 text = self.rx_decoder.decode(data)
Chris Liechtie1384382015-08-15 17:06:05 +0200439 for transformation in self.rx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200440 text = transformation.rx(text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200441 self.console.write(text)
Chris Liechti033f17c2015-08-30 21:28:04 +0200442 except serial.SerialException:
cliechti6963b262010-01-02 03:01:21 +0000443 self.alive = False
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200444 self.console.cancel()
445 raise # XXX handle instead of re-raise?
cliechti576de252002-02-28 23:54:44 +0000446
cliechti6385f2c2005-09-21 19:51:19 +0000447 def writer(self):
cliechti8c2ea842011-03-18 01:51:46 +0000448 """\
Chris Liechti442bf512015-08-15 01:42:24 +0200449 Loop and copy console->serial until self.exit_character character is
450 found. When self.menu_character is found, interpret the next key
cliechti8c2ea842011-03-18 01:51:46 +0000451 locally.
cliechti6c8eb2f2009-07-08 02:10:46 +0000452 """
453 menu_active = False
454 try:
455 while self.alive:
456 try:
Chris Liechti89eb2472015-08-08 17:06:25 +0200457 c = self.console.getkey()
cliechti6c8eb2f2009-07-08 02:10:46 +0000458 except KeyboardInterrupt:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200459 c = '\x03'
Chris Liechti1eb3f6b2016-04-27 02:12:50 +0200460 if not self.alive:
461 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000462 if menu_active:
Chris Liechti7af7c752015-08-12 15:45:19 +0200463 self.handle_menu_key(c)
cliechti6c8eb2f2009-07-08 02:10:46 +0000464 menu_active = False
Chris Liechti442bf512015-08-15 01:42:24 +0200465 elif c == self.menu_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200466 menu_active = True # next char will be for menu
Chris Liechti442bf512015-08-15 01:42:24 +0200467 elif c == self.exit_character:
Chris Liechti7af7c752015-08-12 15:45:19 +0200468 self.stop() # exit app
469 break
cliechti6c8eb2f2009-07-08 02:10:46 +0000470 else:
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200471 #~ if self.raw:
472 text = c
Chris Liechtie1384382015-08-15 17:06:05 +0200473 for transformation in self.tx_transformations:
Chris Liechtid698af72015-08-24 20:24:55 +0200474 text = transformation.tx(text)
Chris Liechtid698af72015-08-24 20:24:55 +0200475 self.serial.write(self.tx_encoder.encode(text))
cliechti6c8eb2f2009-07-08 02:10:46 +0000476 if self.echo:
Chris Liechti3b454802015-08-26 23:39:59 +0200477 echo_text = c
478 for transformation in self.tx_transformations:
479 echo_text = transformation.echo(echo_text)
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200480 self.console.write(echo_text)
cliechti6c8eb2f2009-07-08 02:10:46 +0000481 except:
482 self.alive = False
483 raise
cliechti6385f2c2005-09-21 19:51:19 +0000484
Chris Liechti7af7c752015-08-12 15:45:19 +0200485 def handle_menu_key(self, c):
486 """Implement a simple menu / settings"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200487 if c == self.menu_character or c == self.exit_character:
488 # Menu/exit character again -> send itself
Chris Liechtid698af72015-08-24 20:24:55 +0200489 self.serial.write(self.tx_encoder.encode(c))
Chris Liechti7af7c752015-08-12 15:45:19 +0200490 if self.echo:
491 self.console.write(c)
Chris Liechtib7550bd2015-08-15 04:09:10 +0200492 elif c == '\x15': # CTRL+U -> upload file
Chris Liechti7af7c752015-08-12 15:45:19 +0200493 sys.stderr.write('\n--- File to upload: ')
494 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200495 with self.console:
496 filename = sys.stdin.readline().rstrip('\r\n')
497 if filename:
498 try:
499 with open(filename, 'rb') as f:
500 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
501 while True:
502 block = f.read(1024)
503 if not block:
504 break
505 self.serial.write(block)
506 # Wait for output buffer to drain.
507 self.serial.flush()
508 sys.stderr.write('.') # Progress indicator.
509 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
510 except IOError as e:
511 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200512 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
Chris Liechti442bf512015-08-15 01:42:24 +0200513 sys.stderr.write(self.get_help_text())
Chris Liechti7af7c752015-08-12 15:45:19 +0200514 elif c == '\x12': # CTRL+R -> Toggle RTS
Chris Liechti3b454802015-08-26 23:39:59 +0200515 self.serial.rts = not self.serial.rts
516 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200517 elif c == '\x04': # CTRL+D -> Toggle DTR
Chris Liechti3b454802015-08-26 23:39:59 +0200518 self.serial.dtr = not self.serial.dtr
519 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200520 elif c == '\x02': # CTRL+B -> toggle BREAK condition
Chris Liechti3b454802015-08-26 23:39:59 +0200521 self.serial.break_condition = not self.serial.break_condition
522 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200523 elif c == '\x05': # CTRL+E -> toggle local echo
524 self.echo = not self.echo
Chris Liechti442bf512015-08-15 01:42:24 +0200525 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200526 elif c == '\x06': # CTRL+F -> edit filters
527 sys.stderr.write('\n--- Available Filters:\n')
528 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100529 '--- {:<10} = {.__doc__}'.format(k, v)
530 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtib3df13e2015-08-25 02:20:09 +0200531 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
532 with self.console:
533 new_filters = sys.stdin.readline().lower().split()
534 if new_filters:
535 for f in new_filters:
536 if f not in TRANSFORMATIONS:
537 sys.stderr.write('--- unknown filter: {}'.format(repr(f)))
538 break
539 else:
540 self.filters = new_filters
541 self.update_transformations()
542 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
543 elif c == '\x0c': # CTRL+L -> EOL mode
Chris Liechti033f17c2015-08-30 21:28:04 +0200544 modes = list(EOL_TRANSFORMATIONS) # keys
Chris Liechtib3df13e2015-08-25 02:20:09 +0200545 eol = modes.index(self.eol) + 1
546 if eol >= len(modes):
547 eol = 0
548 self.eol = modes[eol]
549 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
550 self.update_transformations()
551 elif c == '\x01': # CTRL+A -> set encoding
552 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
553 with self.console:
554 new_encoding = sys.stdin.readline().strip()
555 if new_encoding:
556 try:
557 codecs.lookup(new_encoding)
558 except LookupError:
559 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
560 else:
561 self.set_rx_encoding(new_encoding)
562 self.set_tx_encoding(new_encoding)
563 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
564 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
Chris Liechti7af7c752015-08-12 15:45:19 +0200565 elif c == '\x09': # CTRL+I -> info
566 self.dump_port_settings()
567 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
568 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
569 elif c in 'pP': # P -> change port
Chris Liechti269f77b2015-08-24 01:31:42 +0200570 with self.console:
571 try:
Chris Liechti89313c92015-09-01 02:33:13 +0200572 port = ask_for_port()
Chris Liechti269f77b2015-08-24 01:31:42 +0200573 except KeyboardInterrupt:
574 port = None
Chris Liechti7af7c752015-08-12 15:45:19 +0200575 if port and port != self.serial.port:
576 # reader thread needs to be shut down
577 self._stop_reader()
578 # save settings
579 settings = self.serial.getSettingsDict()
580 try:
581 new_serial = serial.serial_for_url(port, do_not_open=True)
582 # restore settings and open
583 new_serial.applySettingsDict(settings)
Chris Liechti89313c92015-09-01 02:33:13 +0200584 new_serial.rts = self.serial.rts
585 new_serial.dtr = self.serial.dtr
Chris Liechti7af7c752015-08-12 15:45:19 +0200586 new_serial.open()
Chris Liechti89313c92015-09-01 02:33:13 +0200587 new_serial.break_condition = self.serial.break_condition
Chris Liechti7af7c752015-08-12 15:45:19 +0200588 except Exception as e:
Chris Liechti442bf512015-08-15 01:42:24 +0200589 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
Chris Liechti7af7c752015-08-12 15:45:19 +0200590 new_serial.close()
591 else:
592 self.serial.close()
593 self.serial = new_serial
Chris Liechti442bf512015-08-15 01:42:24 +0200594 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
Chris Liechti7af7c752015-08-12 15:45:19 +0200595 # and restart the reader thread
596 self._start_reader()
597 elif c in 'bB': # B -> change baudrate
598 sys.stderr.write('\n--- Baudrate: ')
599 sys.stderr.flush()
Chris Liechti269f77b2015-08-24 01:31:42 +0200600 with self.console:
601 backup = self.serial.baudrate
602 try:
603 self.serial.baudrate = int(sys.stdin.readline().strip())
604 except ValueError as e:
Chris Liechti4d541e22016-02-13 23:13:59 +0100605 sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
Chris Liechti269f77b2015-08-24 01:31:42 +0200606 self.serial.baudrate = backup
607 else:
608 self.dump_port_settings()
Chris Liechti7af7c752015-08-12 15:45:19 +0200609 elif c == '8': # 8 -> change to 8 bits
610 self.serial.bytesize = serial.EIGHTBITS
611 self.dump_port_settings()
612 elif c == '7': # 7 -> change to 8 bits
613 self.serial.bytesize = serial.SEVENBITS
614 self.dump_port_settings()
615 elif c in 'eE': # E -> change to even parity
616 self.serial.parity = serial.PARITY_EVEN
617 self.dump_port_settings()
618 elif c in 'oO': # O -> change to odd parity
619 self.serial.parity = serial.PARITY_ODD
620 self.dump_port_settings()
621 elif c in 'mM': # M -> change to mark parity
622 self.serial.parity = serial.PARITY_MARK
623 self.dump_port_settings()
624 elif c in 'sS': # S -> change to space parity
625 self.serial.parity = serial.PARITY_SPACE
626 self.dump_port_settings()
627 elif c in 'nN': # N -> change to no parity
628 self.serial.parity = serial.PARITY_NONE
629 self.dump_port_settings()
630 elif c == '1': # 1 -> change to 1 stop bits
631 self.serial.stopbits = serial.STOPBITS_ONE
632 self.dump_port_settings()
633 elif c == '2': # 2 -> change to 2 stop bits
634 self.serial.stopbits = serial.STOPBITS_TWO
635 self.dump_port_settings()
636 elif c == '3': # 3 -> change to 1.5 stop bits
637 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
638 self.dump_port_settings()
639 elif c in 'xX': # X -> change software flow control
640 self.serial.xonxoff = (c == 'X')
641 self.dump_port_settings()
642 elif c in 'rR': # R -> change hardware flow control
643 self.serial.rtscts = (c == 'R')
644 self.dump_port_settings()
645 else:
Chris Liechti442bf512015-08-15 01:42:24 +0200646 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
647
648 def get_help_text(self):
Chris Liechtia887c932016-02-13 23:10:14 +0100649 """return the help text"""
Chris Liechti55ba7d92015-08-15 16:33:51 +0200650 # help text, starts with blank line!
Chris Liechti442bf512015-08-15 01:42:24 +0200651 return """
652--- pySerial ({version}) - miniterm - help
653---
654--- {exit:8} Exit program
655--- {menu:8} Menu escape key, followed by:
656--- Menu keys:
657--- {menu:7} Send the menu character itself to remote
658--- {exit:7} Send the exit character itself to remote
659--- {info:7} Show info
660--- {upload:7} Upload file (prompt will be shown)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200661--- {repr:7} encoding
662--- {filter:7} edit filters
Chris Liechti442bf512015-08-15 01:42:24 +0200663--- Toggles:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200664--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
665--- {echo:7} echo {eol:7} EOL
Chris Liechti442bf512015-08-15 01:42:24 +0200666---
Chris Liechti55ba7d92015-08-15 16:33:51 +0200667--- Port settings ({menu} followed by the following):
Chris Liechti442bf512015-08-15 01:42:24 +0200668--- p change port
669--- 7 8 set data bits
Chris Liechtib7550bd2015-08-15 04:09:10 +0200670--- N E O S M change parity (None, Even, Odd, Space, Mark)
Chris Liechti442bf512015-08-15 01:42:24 +0200671--- 1 2 3 set stop bits (1, 2, 1.5)
672--- b change baud rate
673--- x X disable/enable software flow control
674--- r R disable/enable hardware flow control
Chris Liechtia887c932016-02-13 23:10:14 +0100675""".format(version=getattr(serial, 'VERSION', 'unknown version'),
676 exit=key_description(self.exit_character),
677 menu=key_description(self.menu_character),
678 rts=key_description('\x12'),
679 dtr=key_description('\x04'),
680 brk=key_description('\x02'),
681 echo=key_description('\x05'),
682 info=key_description('\x09'),
683 upload=key_description('\x15'),
684 repr=key_description('\x01'),
685 filter=key_description('\x06'),
686 eol=key_description('\x0c'))
Chris Liechti7af7c752015-08-12 15:45:19 +0200687
688
Chris Liechtib3df13e2015-08-25 02:20:09 +0200689# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Chris Liechti55ba7d92015-08-15 16:33:51 +0200690# default args can be used to override when calling main() from an other script
691# e.g to create a miniterm-my-device.py
692def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
Chris Liechtia887c932016-02-13 23:10:14 +0100693 """Command line tool, entry point"""
694
Chris Liechtib7550bd2015-08-15 04:09:10 +0200695 import argparse
cliechti6385f2c2005-09-21 19:51:19 +0000696
Chris Liechtib7550bd2015-08-15 04:09:10 +0200697 parser = argparse.ArgumentParser(
Chris Liechti397cf412016-02-11 00:11:48 +0100698 description="Miniterm - A simple terminal program for the serial port.")
cliechti6385f2c2005-09-21 19:51:19 +0000699
Chris Liechti033f17c2015-08-30 21:28:04 +0200700 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100701 "port",
702 nargs='?',
703 help="serial port name ('-' to show port list)",
704 default=default_port)
cliechti5370cee2013-10-13 03:08:19 +0000705
Chris Liechti033f17c2015-08-30 21:28:04 +0200706 parser.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100707 "baudrate",
708 nargs='?',
709 type=int,
710 help="set baud rate, default: %(default)s",
711 default=default_baudrate)
cliechti6385f2c2005-09-21 19:51:19 +0000712
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200713 group = parser.add_argument_group("port settings")
cliechti53edb472009-02-06 21:18:46 +0000714
Chris Liechti033f17c2015-08-30 21:28:04 +0200715 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100716 "--parity",
717 choices=['N', 'E', 'O', 'S', 'M'],
718 type=lambda c: c.upper(),
719 help="set parity, one of {N E O S M}, default: N",
720 default='N')
cliechti53edb472009-02-06 21:18:46 +0000721
Chris Liechti033f17c2015-08-30 21:28:04 +0200722 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100723 "--rtscts",
724 action="store_true",
725 help="enable RTS/CTS flow control (default off)",
726 default=False)
cliechti53edb472009-02-06 21:18:46 +0000727
Chris Liechti033f17c2015-08-30 21:28:04 +0200728 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100729 "--xonxoff",
730 action="store_true",
731 help="enable software flow control (default off)",
732 default=False)
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 "--rts",
736 type=int,
737 help="set initial RTS line state (possible values: 0, 1)",
738 default=default_rts)
cliechti5370cee2013-10-13 03:08:19 +0000739
Chris Liechti033f17c2015-08-30 21:28:04 +0200740 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100741 "--dtr",
742 type=int,
743 help="set initial DTR line state (possible values: 0, 1)",
744 default=default_dtr)
cliechti5370cee2013-10-13 03:08:19 +0000745
Chris Liechti00f84282015-12-24 23:40:34 +0100746 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100747 "--ask",
748 action="store_true",
749 help="ask again for port when open fails",
750 default=False)
Chris Liechti00f84282015-12-24 23:40:34 +0100751
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200752 group = parser.add_argument_group("data handling")
cliechti5370cee2013-10-13 03:08:19 +0000753
Chris Liechti033f17c2015-08-30 21:28:04 +0200754 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100755 "-e", "--echo",
756 action="store_true",
757 help="enable local echo (default off)",
758 default=False)
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 "--encoding",
762 dest="serial_port_encoding",
763 metavar="CODEC",
764 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
765 default='UTF-8')
cliechti5370cee2013-10-13 03:08:19 +0000766
Chris Liechti033f17c2015-08-30 21:28:04 +0200767 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100768 "-f", "--filter",
769 action="append",
770 metavar="NAME",
771 help="add text transformation",
772 default=[])
Chris Liechti2b1b3552015-08-12 15:35:33 +0200773
Chris Liechti033f17c2015-08-30 21:28:04 +0200774 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100775 "--eol",
776 choices=['CR', 'LF', 'CRLF'],
777 type=lambda c: c.upper(),
778 help="end of line mode",
779 default='CRLF')
cliechti53edb472009-02-06 21:18:46 +0000780
Chris Liechti033f17c2015-08-30 21:28:04 +0200781 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100782 "--raw",
783 action="store_true",
784 help="Do no apply any encodings/transformations",
785 default=False)
cliechti6385f2c2005-09-21 19:51:19 +0000786
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200787 group = parser.add_argument_group("hotkeys")
cliechtib7d746d2006-03-28 22:44:30 +0000788
Chris Liechti033f17c2015-08-30 21:28:04 +0200789 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100790 "--exit-char",
791 type=int,
792 metavar='NUM',
793 help="Unicode of special character that is used to exit the application, default: %(default)s",
794 default=0x1d) # GS/CTRL+]
cliechtibf6bb7d2006-03-30 00:28:18 +0000795
Chris Liechti033f17c2015-08-30 21:28:04 +0200796 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100797 "--menu-char",
798 type=int,
799 metavar='NUM',
800 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
801 default=0x14) # Menu: CTRL+T
cliechti9c592b32008-06-16 22:00:14 +0000802
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200803 group = parser.add_argument_group("diagnostics")
cliechti6385f2c2005-09-21 19:51:19 +0000804
Chris Liechti033f17c2015-08-30 21:28:04 +0200805 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100806 "-q", "--quiet",
807 action="store_true",
808 help="suppress non-error messages",
809 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000810
Chris Liechti033f17c2015-08-30 21:28:04 +0200811 group.add_argument(
Chris Liechti397cf412016-02-11 00:11:48 +0100812 "--develop",
813 action="store_true",
814 help="show Python traceback on error",
815 default=False)
cliechti5370cee2013-10-13 03:08:19 +0000816
Chris Liechtib7550bd2015-08-15 04:09:10 +0200817 args = parser.parse_args()
cliechti5370cee2013-10-13 03:08:19 +0000818
Chris Liechtib7550bd2015-08-15 04:09:10 +0200819 if args.menu_char == args.exit_char:
cliechti6c8eb2f2009-07-08 02:10:46 +0000820 parser.error('--exit-char can not be the same as --menu-char')
821
Chris Liechtib3df13e2015-08-25 02:20:09 +0200822 if args.filter:
823 if 'help' in args.filter:
824 sys.stderr.write('Available filters:\n')
Chris Liechti442bf512015-08-15 01:42:24 +0200825 sys.stderr.write('\n'.join(
Chris Liechti397cf412016-02-11 00:11:48 +0100826 '{:<10} = {.__doc__}'.format(k, v)
827 for k, v in sorted(TRANSFORMATIONS.items())))
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200828 sys.stderr.write('\n')
829 sys.exit(1)
Chris Liechtib3df13e2015-08-25 02:20:09 +0200830 filters = args.filter
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200831 else:
Chris Liechtib3df13e2015-08-25 02:20:09 +0200832 filters = ['default']
Chris Liechtic7a5d4c2015-08-11 23:32:20 +0200833
Chris Liechti00f84282015-12-24 23:40:34 +0100834 while True:
835 # no port given on command line -> ask user now
836 if args.port is None or args.port == '-':
837 try:
838 args.port = ask_for_port()
839 except KeyboardInterrupt:
840 sys.stderr.write('\n')
841 parser.error('user aborted and port is not given')
842 else:
843 if not args.port:
844 parser.error('port is not given')
845 try:
846 serial_instance = serial.serial_for_url(
Chris Liechti397cf412016-02-11 00:11:48 +0100847 args.port,
848 args.baudrate,
849 parity=args.parity,
850 rtscts=args.rtscts,
851 xonxoff=args.xonxoff,
852 timeout=1,
853 do_not_open=True)
Chris Liechti3b454802015-08-26 23:39:59 +0200854
Chris Liechti00f84282015-12-24 23:40:34 +0100855 if args.dtr is not None:
856 if not args.quiet:
857 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
858 serial_instance.dtr = args.dtr
859 if args.rts is not None:
860 if not args.quiet:
861 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
862 serial_instance.rts = args.rts
Chris Liechti3b454802015-08-26 23:39:59 +0200863
Chris Liechti00f84282015-12-24 23:40:34 +0100864 serial_instance.open()
865 except serial.SerialException as e:
866 sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
867 if args.develop:
868 raise
869 if not args.ask:
870 sys.exit(1)
871 else:
872 args.port = '-'
873 else:
874 break
cliechti6385f2c2005-09-21 19:51:19 +0000875
Chris Liechti3b454802015-08-26 23:39:59 +0200876 miniterm = Miniterm(
Chris Liechti397cf412016-02-11 00:11:48 +0100877 serial_instance,
878 echo=args.echo,
879 eol=args.eol.lower(),
880 filters=filters)
Chris Liechti3b454802015-08-26 23:39:59 +0200881 miniterm.exit_character = unichr(args.exit_char)
882 miniterm.menu_character = unichr(args.menu_char)
883 miniterm.raw = args.raw
884 miniterm.set_rx_encoding(args.serial_port_encoding)
885 miniterm.set_tx_encoding(args.serial_port_encoding)
886
Chris Liechtib7550bd2015-08-15 04:09:10 +0200887 if not args.quiet:
Chris Liechti1f7ac6c2015-08-15 15:16:37 +0200888 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100889 p=miniterm.serial))
Chris Liechtib7550bd2015-08-15 04:09:10 +0200890 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
Chris Liechti397cf412016-02-11 00:11:48 +0100891 key_description(miniterm.exit_character),
892 key_description(miniterm.menu_character),
893 key_description(miniterm.menu_character),
894 key_description('\x08')))
cliechti6fa76fb2009-07-08 23:53:39 +0000895
cliechti6385f2c2005-09-21 19:51:19 +0000896 miniterm.start()
cliechti258ab0a2011-03-21 23:03:45 +0000897 try:
898 miniterm.join(True)
899 except KeyboardInterrupt:
900 pass
Chris Liechtib7550bd2015-08-15 04:09:10 +0200901 if not args.quiet:
cliechtibf6bb7d2006-03-30 00:28:18 +0000902 sys.stderr.write("\n--- exit ---\n")
cliechti6385f2c2005-09-21 19:51:19 +0000903 miniterm.join()
cliechtibf6bb7d2006-03-30 00:28:18 +0000904
cliechti5370cee2013-10-13 03:08:19 +0000905# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
cliechti8b3ad392002-03-03 20:12:21 +0000906if __name__ == '__main__':
cliechti6385f2c2005-09-21 19:51:19 +0000907 main()