miniterm.py 34 KB

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