blob: 186ac10aac5735bf4e0a9ec5f7ce4ec921a34d6f [file] [log] [blame]
rjw1f884582022-01-06 17:20:42 +08001#!/usr/bin/env python
2#
3# Very simple serial terminal
4#
5# This file is part of pySerial. https://github.com/pyserial/pyserial
6# (C)2002-2015 Chris Liechti <cliechti@gmx.net>
7#
8# SPDX-License-Identifier: BSD-3-Clause
9
10import codecs
11import os
12import sys
13import threading
14
15import pyserial as serial
16from pyserial.tools.list_ports import comports
17from pyserial.tools import hexlify_codec
18
19codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None)
20
21try:
22 raw_input
23except NameError:
24 raw_input = input # in python3 it's "raw"
25 unichr = chr
26
27
28def key_description(character):
29 """generate a readable description for a key"""
30 ascii_code = ord(character)
31 if ascii_code < 32:
32 return 'Ctrl+%c' % (ord('@') + ascii_code)
33 else:
34 return repr(character)
35
36
37# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
38class ConsoleBase(object):
39 def __init__(self):
40 if sys.version_info >= (3, 0):
41 self.byte_output = sys.stdout.buffer
42 else:
43 self.byte_output = sys.stdout
44 self.output = sys.stdout
45
46 def setup(self):
47 pass
48
49 def cleanup(self):
50 pass
51
52 def getkey(self):
53 return None
54
55 def write_bytes(self, s):
56 self.byte_output.write(s)
57 self.byte_output.flush()
58
59 def write(self, s):
60 self.output.write(s)
61 self.output.flush()
62
63 # - - - - - - - - - - - - - - - - - - - - - - - -
64 # context manager:
65 # switch terminal temporary to normal mode (e.g. to get user input)
66
67 def __enter__(self):
68 self.cleanup()
69 return self
70
71 def __exit__(self, *args, **kwargs):
72 self.setup()
73
74
75if os.name == 'nt':
76 import msvcrt
77 import ctypes
78
79 class Out(object):
80 def __init__(self, fd):
81 self.fd = fd
82
83 def flush(self):
84 pass
85
86 def write(self, s):
87 os.write(self.fd, s)
88
89 class Console(ConsoleBase):
90 def __init__(self):
91 super(Console, self).__init__()
92 self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
93 self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
94 ctypes.windll.kernel32.SetConsoleOutputCP(65001)
95 ctypes.windll.kernel32.SetConsoleCP(65001)
96 self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
97 # the change of the code page is not propagated to Python, manually fix it
98 sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
99 sys.stdout = self.output
100 self.output.encoding = 'UTF-8' # needed for input
101
102 def __del__(self):
103 ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
104 ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
105
106 def getkey(self):
107 while True:
108 z = msvcrt.getwch()
109 if z == unichr(13):
110 return unichr(10)
111 elif z in (unichr(0), unichr(0x0e)): # functions keys, ignore
112 msvcrt.getwch()
113 else:
114 return z
115
116elif os.name == 'posix':
117 import atexit
118 import termios
119
120 class Console(ConsoleBase):
121 def __init__(self):
122 super(Console, self).__init__()
123 self.fd = sys.stdin.fileno()
124 self.old = termios.tcgetattr(self.fd)
125 atexit.register(self.cleanup)
126 if sys.version_info < (3, 0):
127 self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
128 else:
129 self.enc_stdin = sys.stdin
130
131 def setup(self):
132 new = termios.tcgetattr(self.fd)
133 new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
134 new[6][termios.VMIN] = 1
135 new[6][termios.VTIME] = 0
136 termios.tcsetattr(self.fd, termios.TCSANOW, new)
137
138 def getkey(self):
139 c = self.enc_stdin.read(1)
140 if c == unichr(0x7f):
141 c = unichr(8) # map the BS key (which yields DEL) to backspace
142 return c
143
144 def cleanup(self):
145 termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
146
147else:
148 raise NotImplementedError("Sorry no implementation for your platform (%s) available." % sys.platform)
149
150
151# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
152
153class Transform(object):
154 """do-nothing: forward all data unchanged"""
155 def rx(self, text):
156 """text received from serial port"""
157 return text
158
159 def tx(self, text):
160 """text to be sent to serial port"""
161 return text
162
163 def echo(self, text):
164 """text to be sent but displayed on console"""
165 return text
166
167
168class CRLF(Transform):
169 """ENTER sends CR+LF"""
170
171 def tx(self, text):
172 return text.replace('\n', '\r\n')
173
174
175class CR(Transform):
176 """ENTER sends CR"""
177
178 def rx(self, text):
179 return text.replace('\r', '\n')
180
181 def tx(self, text):
182 return text.replace('\n', '\r')
183
184
185class LF(Transform):
186 """ENTER sends LF"""
187
188
189class NoTerminal(Transform):
190 """remove typical terminal control codes from input"""
191
192 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t')
193 REPLACEMENT_MAP.update({
194 0x7F: 0x2421, # DEL
195 0x9B: 0x2425, # CSI
196 })
197
198 def rx(self, text):
199 return text.translate(self.REPLACEMENT_MAP)
200
201 echo = rx
202
203
204class NoControls(NoTerminal):
205 """Remove all control codes, incl. CR+LF"""
206
207 REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
208 REPLACEMENT_MAP.update({
209 32: 0x2423, # visual space
210 0x7F: 0x2421, # DEL
211 0x9B: 0x2425, # CSI
212 })
213
214
215class Printable(Transform):
216 """Show decimal code for all non-ASCII characters and replace most control codes"""
217
218 def rx(self, text):
219 r = []
220 for t in text:
221 if ' ' <= t < '\x7f' or t in '\r\n\b\t':
222 r.append(t)
223 elif t < ' ':
224 r.append(unichr(0x2400 + ord(t)))
225 else:
226 r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(t)))
227 r.append(' ')
228 return ''.join(r)
229
230 echo = rx
231
232
233class Colorize(Transform):
234 """Apply different colors for received and echo"""
235
236 def __init__(self):
237 # XXX make it configurable, use colorama?
238 self.input_color = '\x1b[37m'
239 self.echo_color = '\x1b[31m'
240
241 def rx(self, text):
242 return self.input_color + text
243
244 def echo(self, text):
245 return self.echo_color + text
246
247
248class DebugIO(Transform):
249 """Print what is sent and received"""
250
251 def rx(self, text):
252 sys.stderr.write(' [RX:{}] '.format(repr(text)))
253 sys.stderr.flush()
254 return text
255
256 def tx(self, text):
257 sys.stderr.write(' [TX:{}] '.format(repr(text)))
258 sys.stderr.flush()
259 return text
260
261
262# other ideas:
263# - add date/time for each newline
264# - insert newline after: a) timeout b) packet end character
265
266EOL_TRANSFORMATIONS = {
267 'crlf': CRLF,
268 'cr': CR,
269 'lf': LF,
270 }
271
272TRANSFORMATIONS = {
273 'direct': Transform, # no transformation
274 'default': NoTerminal,
275 'nocontrol': NoControls,
276 'printable': Printable,
277 'colorize': Colorize,
278 'debug': DebugIO,
279 }
280
281
282# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
283def ask_for_port():
284 """\
285 Show a list of ports and ask the user for a choice. To make selection
286 easier on systems with long device names, also allow the input of an
287 index.
288 """
289 sys.stderr.write('\n--- Available ports:\n')
290 ports = []
291 for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
292 #~ sys.stderr.write('--- %-20s %s [%s]\n' % (port, desc, hwid))
293 sys.stderr.write('--- {:2}: {:20} {}\n'.format(n, port, desc))
294 ports.append(port)
295 while True:
296 port = raw_input('--- Enter port index or full name: ')
297 try:
298 index = int(port) - 1
299 if not 0 <= index < len(ports):
300 sys.stderr.write('--- Invalid index!\n')
301 continue
302 except ValueError:
303 pass
304 else:
305 port = ports[index]
306 return port
307
308
309class Miniterm(object):
310 """\
311 Terminal application. Copy data from serial port to console and vice versa.
312 Handle special keys from the console to show menu etc.
313 """
314
315 def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
316 self.console = Console()
317 self.serial = serial_instance
318 self.echo = echo
319 self.raw = False
320 self.input_encoding = 'UTF-8'
321 self.output_encoding = 'UTF-8'
322 self.eol = eol
323 self.filters = filters
324 self.update_transformations()
325 self.exit_character = 0x1d # GS/CTRL+]
326 self.menu_character = 0x14 # Menu: CTRL+T
327
328 def _start_reader(self):
329 """Start reader thread"""
330 self._reader_alive = True
331 # start serial->console thread
332 self.receiver_thread = threading.Thread(target=self.reader, name='rx')
333 self.receiver_thread.daemon = True
334 self.receiver_thread.start()
335
336 def _stop_reader(self):
337 """Stop reader thread only, wait for clean exit of thread"""
338 self._reader_alive = False
339 self.receiver_thread.join()
340
341 def start(self):
342 self.alive = True
343 self._start_reader()
344 # enter console->serial loop
345 self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
346 self.transmitter_thread.daemon = True
347 self.transmitter_thread.start()
348 self.console.setup()
349
350 def stop(self):
351 self.alive = False
352
353 def join(self, transmit_only=False):
354 self.transmitter_thread.join()
355 if not transmit_only:
356 self.receiver_thread.join()
357
358 def update_transformations(self):
359 transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f] for f in self.filters]
360 self.tx_transformations = [t() for t in transformations]
361 self.rx_transformations = list(reversed(self.tx_transformations))
362
363 def set_rx_encoding(self, encoding, errors='replace'):
364 self.input_encoding = encoding
365 self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
366
367 def set_tx_encoding(self, encoding, errors='replace'):
368 self.output_encoding = encoding
369 self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
370
371 def dump_port_settings(self):
372 sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
373 p=self.serial))
374 sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
375 ('active' if self.serial.rts else 'inactive'),
376 ('active' if self.serial.dtr else 'inactive'),
377 ('active' if self.serial.break_condition else 'inactive')))
378 try:
379 sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
380 ('active' if self.serial.cts else 'inactive'),
381 ('active' if self.serial.dsr else 'inactive'),
382 ('active' if self.serial.ri else 'inactive'),
383 ('active' if self.serial.cd else 'inactive')))
384 except serial.SerialException:
385 # on RFC 2217 ports, it can happen if no modem state notification was
386 # yet received. ignore this error.
387 pass
388 sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
389 sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
390 #~ sys.stderr.write('--- data escaping: %s linefeed: %s\n' % (
391 #~ REPR_MODES[self.repr_mode],
392 #~ LF_MODES[self.convert_outgoing]))
393 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
394 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
395 sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
396 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
397
398 def reader(self):
399 """loop and copy serial->console"""
400 try:
401 while self.alive and self._reader_alive:
402 # read all that is there or wait for one byte
403 data = self.serial.read(self.serial.in_waiting or 1)
404 if data:
405 if self.raw:
406 self.console.write_bytes(data)
407 else:
408 text = self.rx_decoder.decode(data)
409 for transformation in self.rx_transformations:
410 text = transformation.rx(text)
411 self.console.write(text)
412 except serial.SerialException:
413 self.alive = False
414 # XXX would be nice if the writer could be interrupted at this
415 # point... to exit completely
416 raise
417
418 def writer(self):
419 """\
420 Loop and copy console->serial until self.exit_character character is
421 found. When self.menu_character is found, interpret the next key
422 locally.
423 """
424 menu_active = False
425 try:
426 while self.alive:
427 try:
428 c = self.console.getkey()
429 except KeyboardInterrupt:
430 c = '\x03'
431 if menu_active:
432 self.handle_menu_key(c)
433 menu_active = False
434 elif c == self.menu_character:
435 menu_active = True # next char will be for menu
436 elif c == self.exit_character:
437 self.stop() # exit app
438 break
439 else:
440 #~ if self.raw:
441 text = c
442 for transformation in self.tx_transformations:
443 text = transformation.tx(text)
444 self.serial.write(self.tx_encoder.encode(text))
445 if self.echo:
446 echo_text = c
447 for transformation in self.tx_transformations:
448 echo_text = transformation.echo(echo_text)
449 self.console.write(echo_text)
450 except:
451 self.alive = False
452 raise
453
454 def handle_menu_key(self, c):
455 """Implement a simple menu / settings"""
456 if c == self.menu_character or c == self.exit_character:
457 # Menu/exit character again -> send itself
458 self.serial.write(self.tx_encoder.encode(c))
459 if self.echo:
460 self.console.write(c)
461 elif c == '\x15': # CTRL+U -> upload file
462 sys.stderr.write('\n--- File to upload: ')
463 sys.stderr.flush()
464 with self.console:
465 filename = sys.stdin.readline().rstrip('\r\n')
466 if filename:
467 try:
468 with open(filename, 'rb') as f:
469 sys.stderr.write('--- Sending file {} ---\n'.format(filename))
470 while True:
471 block = f.read(1024)
472 if not block:
473 break
474 self.serial.write(block)
475 # Wait for output buffer to drain.
476 self.serial.flush()
477 sys.stderr.write('.') # Progress indicator.
478 sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
479 except IOError as e:
480 sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
481 elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
482 sys.stderr.write(self.get_help_text())
483 elif c == '\x12': # CTRL+R -> Toggle RTS
484 self.serial.rts = not self.serial.rts
485 sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
486 elif c == '\x04': # CTRL+D -> Toggle DTR
487 self.serial.dtr = not self.serial.dtr
488 sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
489 elif c == '\x02': # CTRL+B -> toggle BREAK condition
490 self.serial.break_condition = not self.serial.break_condition
491 sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
492 elif c == '\x05': # CTRL+E -> toggle local echo
493 self.echo = not self.echo
494 sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
495 elif c == '\x06': # CTRL+F -> edit filters
496 sys.stderr.write('\n--- Available Filters:\n')
497 sys.stderr.write('\n'.join(
498 '--- {:<10} = {.__doc__}'.format(k, v)
499 for k, v in sorted(TRANSFORMATIONS.items())))
500 sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
501 with self.console:
502 new_filters = sys.stdin.readline().lower().split()
503 if new_filters:
504 for f in new_filters:
505 if f not in TRANSFORMATIONS:
506 sys.stderr.write('--- unknown filter: {}'.format(repr(f)))
507 break
508 else:
509 self.filters = new_filters
510 self.update_transformations()
511 sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
512 elif c == '\x0c': # CTRL+L -> EOL mode
513 modes = list(EOL_TRANSFORMATIONS) # keys
514 eol = modes.index(self.eol) + 1
515 if eol >= len(modes):
516 eol = 0
517 self.eol = modes[eol]
518 sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
519 self.update_transformations()
520 elif c == '\x01': # CTRL+A -> set encoding
521 sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
522 with self.console:
523 new_encoding = sys.stdin.readline().strip()
524 if new_encoding:
525 try:
526 codecs.lookup(new_encoding)
527 except LookupError:
528 sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
529 else:
530 self.set_rx_encoding(new_encoding)
531 self.set_tx_encoding(new_encoding)
532 sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
533 sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
534 elif c == '\x09': # CTRL+I -> info
535 self.dump_port_settings()
536 #~ elif c == '\x01': # CTRL+A -> cycle escape mode
537 #~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
538 elif c in 'pP': # P -> change port
539 with self.console:
540 try:
541 port = ask_for_port()
542 except KeyboardInterrupt:
543 port = None
544 if port and port != self.serial.port:
545 # reader thread needs to be shut down
546 self._stop_reader()
547 # save settings
548 settings = self.serial.getSettingsDict()
549 try:
550 new_serial = serial.serial_for_url(port, do_not_open=True)
551 # restore settings and open
552 new_serial.applySettingsDict(settings)
553 new_serial.rts = self.serial.rts
554 new_serial.dtr = self.serial.dtr
555 new_serial.open()
556 new_serial.break_condition = self.serial.break_condition
557 except Exception as e:
558 sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
559 new_serial.close()
560 else:
561 self.serial.close()
562 self.serial = new_serial
563 sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
564 # and restart the reader thread
565 self._start_reader()
566 elif c in 'bB': # B -> change baudrate
567 sys.stderr.write('\n--- Baudrate: ')
568 sys.stderr.flush()
569 with self.console:
570 backup = self.serial.baudrate
571 try:
572 self.serial.baudrate = int(sys.stdin.readline().strip())
573 except ValueError as e:
574 sys.stderr.write('--- ERROR setting baudrate: %s ---\n'.format(e))
575 self.serial.baudrate = backup
576 else:
577 self.dump_port_settings()
578 elif c == '8': # 8 -> change to 8 bits
579 self.serial.bytesize = serial.EIGHTBITS
580 self.dump_port_settings()
581 elif c == '7': # 7 -> change to 8 bits
582 self.serial.bytesize = serial.SEVENBITS
583 self.dump_port_settings()
584 elif c in 'eE': # E -> change to even parity
585 self.serial.parity = serial.PARITY_EVEN
586 self.dump_port_settings()
587 elif c in 'oO': # O -> change to odd parity
588 self.serial.parity = serial.PARITY_ODD
589 self.dump_port_settings()
590 elif c in 'mM': # M -> change to mark parity
591 self.serial.parity = serial.PARITY_MARK
592 self.dump_port_settings()
593 elif c in 'sS': # S -> change to space parity
594 self.serial.parity = serial.PARITY_SPACE
595 self.dump_port_settings()
596 elif c in 'nN': # N -> change to no parity
597 self.serial.parity = serial.PARITY_NONE
598 self.dump_port_settings()
599 elif c == '1': # 1 -> change to 1 stop bits
600 self.serial.stopbits = serial.STOPBITS_ONE
601 self.dump_port_settings()
602 elif c == '2': # 2 -> change to 2 stop bits
603 self.serial.stopbits = serial.STOPBITS_TWO
604 self.dump_port_settings()
605 elif c == '3': # 3 -> change to 1.5 stop bits
606 self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
607 self.dump_port_settings()
608 elif c in 'xX': # X -> change software flow control
609 self.serial.xonxoff = (c == 'X')
610 self.dump_port_settings()
611 elif c in 'rR': # R -> change hardware flow control
612 self.serial.rtscts = (c == 'R')
613 self.dump_port_settings()
614 else:
615 sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
616
617 def get_help_text(self):
618 # help text, starts with blank line!
619 return """
620--- pySerial ({version}) - miniterm - help
621---
622--- {exit:8} Exit program
623--- {menu:8} Menu escape key, followed by:
624--- Menu keys:
625--- {menu:7} Send the menu character itself to remote
626--- {exit:7} Send the exit character itself to remote
627--- {info:7} Show info
628--- {upload:7} Upload file (prompt will be shown)
629--- {repr:7} encoding
630--- {filter:7} edit filters
631--- Toggles:
632--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
633--- {echo:7} echo {eol:7} EOL
634---
635--- Port settings ({menu} followed by the following):
636--- p change port
637--- 7 8 set data bits
638--- N E O S M change parity (None, Even, Odd, Space, Mark)
639--- 1 2 3 set stop bits (1, 2, 1.5)
640--- b change baud rate
641--- x X disable/enable software flow control
642--- r R disable/enable hardware flow control
643""".format(
644 version=getattr(serial, 'VERSION', 'unknown version'),
645 exit=key_description(self.exit_character),
646 menu=key_description(self.menu_character),
647 rts=key_description('\x12'),
648 dtr=key_description('\x04'),
649 brk=key_description('\x02'),
650 echo=key_description('\x05'),
651 info=key_description('\x09'),
652 upload=key_description('\x15'),
653 repr=key_description('\x01'),
654 filter=key_description('\x06'),
655 eol=key_description('\x0c'),
656 )
657
658
659# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
660# default args can be used to override when calling main() from an other script
661# e.g to create a miniterm-my-device.py
662def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
663 import argparse
664
665 parser = argparse.ArgumentParser(
666 description="Miniterm - A simple terminal program for the serial port.")
667
668 parser.add_argument(
669 "port",
670 nargs='?',
671 help="serial port name ('-' to show port list)",
672 default=default_port)
673
674 parser.add_argument(
675 "baudrate",
676 nargs='?',
677 type=int,
678 help="set baud rate, default: %(default)s",
679 default=default_baudrate)
680
681 group = parser.add_argument_group("port settings")
682
683 group.add_argument(
684 "--parity",
685 choices=['N', 'E', 'O', 'S', 'M'],
686 type=lambda c: c.upper(),
687 help="set parity, one of {N E O S M}, default: N",
688 default='N')
689
690 group.add_argument(
691 "--rtscts",
692 action="store_true",
693 help="enable RTS/CTS flow control (default off)",
694 default=False)
695
696 group.add_argument(
697 "--xonxoff",
698 action="store_true",
699 help="enable software flow control (default off)",
700 default=False)
701
702 group.add_argument(
703 "--rts",
704 type=int,
705 help="set initial RTS line state (possible values: 0, 1)",
706 default=default_rts)
707
708 group.add_argument(
709 "--dtr",
710 type=int,
711 help="set initial DTR line state (possible values: 0, 1)",
712 default=default_dtr)
713
714 group.add_argument(
715 "--ask",
716 action="store_true",
717 help="ask again for port when open fails",
718 default=False)
719
720 group = parser.add_argument_group("data handling")
721
722 group.add_argument(
723 "-e", "--echo",
724 action="store_true",
725 help="enable local echo (default off)",
726 default=False)
727
728 group.add_argument(
729 "--encoding",
730 dest="serial_port_encoding",
731 metavar="CODEC",
732 help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
733 default='UTF-8')
734
735 group.add_argument(
736 "-f", "--filter",
737 action="append",
738 metavar="NAME",
739 help="add text transformation",
740 default=[])
741
742 group.add_argument(
743 "--eol",
744 choices=['CR', 'LF', 'CRLF'],
745 type=lambda c: c.upper(),
746 help="end of line mode",
747 default='CRLF')
748
749 group.add_argument(
750 "--raw",
751 action="store_true",
752 help="Do no apply any encodings/transformations",
753 default=False)
754
755 group = parser.add_argument_group("hotkeys")
756
757 group.add_argument(
758 "--exit-char",
759 type=int,
760 metavar='NUM',
761 help="Unicode of special character that is used to exit the application, default: %(default)s",
762 default=0x1d # GS/CTRL+]
763 )
764
765 group.add_argument(
766 "--menu-char",
767 type=int,
768 metavar='NUM',
769 help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
770 default=0x14 # Menu: CTRL+T
771 )
772
773 group = parser.add_argument_group("diagnostics")
774
775 group.add_argument(
776 "-q", "--quiet",
777 action="store_true",
778 help="suppress non-error messages",
779 default=False)
780
781 group.add_argument(
782 "--develop",
783 action="store_true",
784 help="show Python traceback on error",
785 default=False)
786
787 args = parser.parse_args()
788
789 if args.menu_char == args.exit_char:
790 parser.error('--exit-char can not be the same as --menu-char')
791
792 if args.filter:
793 if 'help' in args.filter:
794 sys.stderr.write('Available filters:\n')
795 sys.stderr.write('\n'.join(
796 '{:<10} = {.__doc__}'.format(k, v)
797 for k, v in sorted(TRANSFORMATIONS.items())))
798 sys.stderr.write('\n')
799 sys.exit(1)
800 filters = args.filter
801 else:
802 filters = ['default']
803
804 while True:
805 # no port given on command line -> ask user now
806 if args.port is None or args.port == '-':
807 try:
808 args.port = ask_for_port()
809 except KeyboardInterrupt:
810 sys.stderr.write('\n')
811 parser.error('user aborted and port is not given')
812 else:
813 if not args.port:
814 parser.error('port is not given')
815 try:
816 serial_instance = serial.serial_for_url(
817 args.port,
818 args.baudrate,
819 parity=args.parity,
820 rtscts=args.rtscts,
821 xonxoff=args.xonxoff,
822 timeout=1,
823 do_not_open=True)
824
825 if args.dtr is not None:
826 if not args.quiet:
827 sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
828 serial_instance.dtr = args.dtr
829 if args.rts is not None:
830 if not args.quiet:
831 sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
832 serial_instance.rts = args.rts
833
834 serial_instance.open()
835 except serial.SerialException as e:
836 sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
837 if args.develop:
838 raise
839 if not args.ask:
840 sys.exit(1)
841 else:
842 args.port = '-'
843 else:
844 break
845
846 miniterm = Miniterm(
847 serial_instance,
848 echo=args.echo,
849 eol=args.eol.lower(),
850 filters=filters)
851 miniterm.exit_character = unichr(args.exit_char)
852 miniterm.menu_character = unichr(args.menu_char)
853 miniterm.raw = args.raw
854 miniterm.set_rx_encoding(args.serial_port_encoding)
855 miniterm.set_tx_encoding(args.serial_port_encoding)
856
857 if not args.quiet:
858 sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
859 p=miniterm.serial))
860 sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
861 key_description(miniterm.exit_character),
862 key_description(miniterm.menu_character),
863 key_description(miniterm.menu_character),
864 key_description('\x08'),
865 ))
866
867 miniterm.start()
868 try:
869 miniterm.join(True)
870 except KeyboardInterrupt:
871 pass
872 if not args.quiet:
873 sys.stderr.write("\n--- exit ---\n")
874 miniterm.join()
875
876# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
877if __name__ == '__main__':
878 main()