miniterm.py 37 KB

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