miniterm.py 33 KB

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