├── .gitignore ├── LICENSE.txt ├── README.rst ├── requirements.txt ├── serial_terminal ├── __init__.py ├── __main__.py ├── console │ ├── __init__.py │ ├── base.py │ ├── posix.py │ ├── tk_widget.py │ └── windows.py ├── emulation │ └── simple.py ├── features │ ├── README.rst │ ├── api.py │ ├── ask_for_port.py │ ├── menu.py │ ├── print_port_settings.py │ ├── send_file.py │ └── startup_message.py └── terminal │ ├── constants.py │ ├── escape_decoder.py │ └── escape_encoder.py ├── setup.cfg ├── setup.py └── test ├── terminal_wrapper.py ├── test_colors.py ├── tk_console.py └── vt220_colors.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | *.pyc 3 | *.pyo 4 | build 5 | dist 6 | *.egg-info 7 | /MANIFEST 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Chris Liechti 2 | All Rights Reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | --------------------------------------------------------------------------- 33 | Note: 34 | Individual files contain the following tag instead of the full license text. 35 | 36 | SPDX-License-Identifier: BSD-3-Clause 37 | 38 | This enables machine processing of license information based on the SPDX 39 | License Identifiers that are here available: http://spdx.org/licenses/ 40 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | pySerial-Terminal 3 | ================= 4 | 5 | A more capable(*), modular terminal (* than pySerial's miniterm) 6 | 7 | Currently a work in progress. 8 | 9 | 10 | Misc 11 | ==== 12 | There are some layers and indirections. The aim is to be able to implement 13 | different I/O devices without re-implementing the escape decoder logic each 14 | time. 15 | 16 | console 17 | 18 | - read keys (return key names) 19 | - output operations using API (query/move cursor, erase parts) 20 | - backends: windows, [posix, GUIs] 21 | 22 | terminal 23 | 24 | - escape_decoder: decode escape sequences and call methods on emulation object 25 | - providing constants 26 | 27 | emulation 28 | 29 | - mapping escape_decoder calls to console 30 | - simple version decoding colors and movements, not supporting some of the features 31 | - [aiming for] nearly full VT220 (e.g. no printing support) 32 | 33 | 34 | :: 35 | 36 | ┏━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━┓ ┏━━━━━━━━━┓ 37 | ┃ input stream ┃───>┃ escape_decoder ┃───>┃ emulation ┃───>┃ console ┃ 38 | ┗━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━┛ ┗━━━━━━━━━┛ 39 | ┏━━━━━━━━━┓ ┏━━━━━━━━━━━┓ ┏━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━┓ 40 | ┃ console ┃───>┃ emulation ┃───>┃ terminal ┃───>┃ output stream ┃ 41 | ┗━━━━━━━━━┛ ┗━━━━━━━━━━━┛ ┗━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛ 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyserial>=3.2 2 | 3 | -------------------------------------------------------------------------------- /serial_terminal/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # This is a wrapper module for different platform implementations 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial 6 | # (C) 2001-2017 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from __future__ import absolute_import 11 | 12 | __version__ = '0.1' 13 | -------------------------------------------------------------------------------- /serial_terminal/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # pySerial-terminal 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2002-2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | import codecs 11 | import os 12 | import sys 13 | import threading 14 | import traceback 15 | 16 | from .console import Console 17 | from .terminal.escape_decoder import EscapeDecoder 18 | from .terminal.escape_encoder import EscapeEncoder 19 | from .emulation.simple import SimpleTerminal 20 | from .features import menu, ask_for_port, startup_message 21 | 22 | import serial 23 | from serial.tools import hexlify_codec 24 | from . import __version__ 25 | 26 | # pylint: disable=wrong-import-order,wrong-import-position 27 | 28 | codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None) 29 | 30 | try: 31 | unichr 32 | except NameError: 33 | # pylint: disable=redefined-builtin,invalid-name 34 | unichr = chr 35 | 36 | 37 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 38 | class Transform(object): 39 | """do-nothing: forward all data unchanged""" 40 | def rx(self, text): 41 | """text received from serial port""" 42 | return text 43 | 44 | def tx(self, text): 45 | """text to be sent to serial port""" 46 | return text 47 | 48 | def echo(self, text): 49 | """text to be sent but displayed on console""" 50 | return text 51 | 52 | 53 | class CRLF(Transform): 54 | """ENTER sends CR+LF""" 55 | 56 | def tx(self, text): 57 | return text.replace('\n', '\r\n') 58 | 59 | 60 | class CR(Transform): 61 | """ENTER sends CR""" 62 | 63 | def rx(self, text): 64 | return text.replace('\r', '\n') 65 | 66 | def tx(self, text): 67 | return text.replace('\n', '\r') 68 | 69 | 70 | class LF(Transform): 71 | """ENTER sends LF""" 72 | 73 | 74 | class NoTerminal(Transform): 75 | """remove typical terminal control codes from input""" 76 | 77 | REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t') 78 | REPLACEMENT_MAP.update( 79 | { 80 | 0x7F: 0x2421, # DEL 81 | 0x9B: 0x2425, # CSI 82 | }) 83 | 84 | def rx(self, text): 85 | return text.translate(self.REPLACEMENT_MAP) 86 | 87 | echo = rx 88 | 89 | 90 | class NoControls(NoTerminal): 91 | """Remove all control codes, incl. CR+LF""" 92 | 93 | REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32)) 94 | REPLACEMENT_MAP.update( 95 | { 96 | 0x20: 0x2423, # visual space 97 | 0x7F: 0x2421, # DEL 98 | 0x9B: 0x2425, # CSI 99 | }) 100 | 101 | 102 | class Printable(Transform): 103 | """Show decimal code for all non-ASCII characters and replace most control codes""" 104 | 105 | def rx(self, text): 106 | r = [] 107 | for c in text: 108 | if ' ' <= c < '\x7f' or c in '\r\n\b\t': 109 | r.append(c) 110 | elif c < ' ': 111 | r.append(unichr(0x2400 + ord(c))) 112 | else: 113 | r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c))) 114 | r.append(' ') 115 | return ''.join(r) 116 | 117 | echo = rx 118 | 119 | 120 | class Colorize(Transform): 121 | """Apply different colors for received and echo""" 122 | 123 | def __init__(self): 124 | # XXX make it configurable, use colorama? 125 | self.input_color = '\x1b[37m' 126 | self.echo_color = '\x1b[31m' 127 | 128 | def rx(self, text): 129 | return self.input_color + text 130 | 131 | def echo(self, text): 132 | return self.echo_color + text 133 | 134 | 135 | # other ideas: 136 | # - add date/time for each newline 137 | # - insert newline after: a) timeout b) packet end character 138 | 139 | EOL_TRANSFORMATIONS = { 140 | 'crlf': CRLF, 141 | 'cr': CR, 142 | 'lf': LF, 143 | } 144 | 145 | TRANSFORMATIONS = { 146 | 'direct': Transform, # no transformation 147 | 'default': NoTerminal, 148 | 'nocontrol': NoControls, 149 | 'printable': Printable, 150 | 'colorize': Colorize, 151 | } 152 | 153 | 154 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 155 | 156 | class Miniterm(object): 157 | """\ 158 | Terminal application. Copy data from serial port to console and vice versa. 159 | Handle special keys from the console to show menu etc. 160 | """ 161 | 162 | def __init__(self, serial_instance, echo=False, eol='crlf', filters=(), features=(), exit_key='Ctrl+]'): 163 | self.console = Console() 164 | self.terminal = SimpleTerminal(self.console) 165 | self.escape_decoder = EscapeDecoder(self.terminal) 166 | self.escape_encoder = EscapeEncoder() 167 | self.serial = serial_instance 168 | self.echo = echo 169 | self.input_encoding = 'UTF-8' 170 | self.output_encoding = 'UTF-8' 171 | self.eol = eol 172 | self.filters = filters 173 | self.update_transformations() 174 | self.exit_key = exit_key 175 | self.alive = None 176 | self._reader_alive = None 177 | self.receiver_thread = None 178 | self.rx_decoder = None 179 | self.tx_decoder = None 180 | self.hotkeys = {} 181 | self.hotkeys[self.exit_key] = self.handle_exit_key 182 | self._features = [f(self, **kwargs) for f, kwargs in features] 183 | 184 | def _start_reader(self): 185 | """Start reader thread""" 186 | self._reader_alive = True 187 | # start serial->console thread 188 | self.receiver_thread = threading.Thread(target=self.reader, name='rx') 189 | self.receiver_thread.daemon = True 190 | self.receiver_thread.start() 191 | 192 | def _stop_reader(self): 193 | """Stop reader thread only, wait for clean exit of thread""" 194 | self._reader_alive = False 195 | if hasattr(self.serial, 'cancel_read'): 196 | self.serial.cancel_read() 197 | self.receiver_thread.join() 198 | 199 | def start(self): 200 | """start worker threads""" 201 | self.alive = True 202 | self._start_reader() 203 | # enter console->serial loop 204 | self.transmitter_thread = threading.Thread(target=self.writer, name='tx') 205 | self.transmitter_thread.daemon = True 206 | self.transmitter_thread.start() 207 | self.console.setup() 208 | for f in self._features: 209 | f.start() 210 | 211 | def stop(self): 212 | """set flag to stop worker threads""" 213 | self.alive = False 214 | 215 | def join(self, transmit_only=False): 216 | """wait for worker threads to terminate""" 217 | self.transmitter_thread.join() 218 | if not transmit_only: 219 | if hasattr(self.serial, 'cancel_read'): 220 | self.serial.cancel_read() 221 | self.receiver_thread.join() 222 | 223 | def close(self): 224 | self.serial.close() 225 | 226 | def update_transformations(self): 227 | """take list of transformation classes and instantiate them for rx and tx""" 228 | transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f] 229 | for f in self.filters] 230 | self.tx_transformations = [t() for t in transformations] 231 | self.rx_transformations = list(reversed(self.tx_transformations)) 232 | 233 | def set_rx_encoding(self, encoding, errors='replace'): 234 | """set encoding for received data""" 235 | self.input_encoding = encoding 236 | self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors) 237 | 238 | def set_tx_encoding(self, encoding, errors='replace'): 239 | """set encoding for transmitted data""" 240 | self.output_encoding = encoding 241 | self.tx_encoder = codecs.getincrementalencoder(encoding)(errors) 242 | 243 | def reader(self): 244 | """loop and copy serial->console""" 245 | try: 246 | while self.alive and self._reader_alive: 247 | # read all that is there or wait for one byte 248 | data = self.serial.read(self.serial.in_waiting or 1) 249 | for byte in serial.iterbytes(data): 250 | try: 251 | self.escape_decoder.handle(byte) 252 | except Exception as e: 253 | traceback.print_exc() 254 | # text = self.rx_decoder.decode(data) 255 | # for transformation in self.rx_transformations: 256 | # text = transformation.rx(text) 257 | # self.console.write(text) 258 | except serial.SerialException: 259 | self.alive = False 260 | self.console.cancel() 261 | raise # XXX handle instead of re-raise? 262 | 263 | def send_key(self, key_name): 264 | if len(key_name) > 1: 265 | key_name = self.escape_encoder.translate_named_key(key_name) 266 | text = key_name 267 | for transformation in self.tx_transformations: 268 | text = transformation.tx(text) 269 | self.serial.write(self.tx_encoder.encode(text)) 270 | if self.echo: 271 | echo_text = key_name 272 | for transformation in self.tx_transformations: 273 | echo_text = transformation.echo(echo_text) 274 | self.console.write(echo_text) 275 | 276 | def handle_exit_key(self, key_name): 277 | self.stop() # exit app 278 | 279 | def writer(self): 280 | """Loop and copy console->serial.""" 281 | try: 282 | while self.alive: 283 | try: 284 | key_name = self.console.getkey() 285 | except KeyboardInterrupt: 286 | key_name = '\x03' 287 | if not self.alive: 288 | break 289 | callback = self.hotkeys.get(key_name, self.send_key) 290 | callback(key_name) 291 | except: 292 | self.alive = False 293 | raise 294 | 295 | 296 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 297 | # default args can be used to override when calling main() from an other script 298 | # e.g to create a miniterm-my-device.py 299 | def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None, serial_instance=None): 300 | """Command line tool, entry point""" 301 | 302 | import argparse 303 | 304 | MODIFIERS = ['Ctrl', 'Shift', 'Alt'] 305 | USEFUL_KEYS = [ 306 | 'Esc', 'Tab', 'Insert', 'Delete', 307 | 'F1', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12' 308 | ] + list('ABCDEFGHJKLMNOPQRSTXYZ\\]^_') # keys with alternate names removed 309 | 310 | def check_key_name(key_name): 311 | parts = key_name.split('+') 312 | if len(parts) < 1: 313 | raise ValueError('key name missing') 314 | if parts.pop() not in USEFUL_KEYS: 315 | raise ValueError('invalid key name') 316 | for part in parts: 317 | if part not in MODIFIERS: 318 | raise ValueError('{!r} is not a valid modifier ({})'.format(part, MODIFIERS)) 319 | return key_name 320 | 321 | parser = argparse.ArgumentParser(description="pySerial-terminal") 322 | 323 | parser.add_argument( 324 | "port", 325 | nargs='?', 326 | help="serial port name ('-' to show port list)", 327 | default=default_port) 328 | 329 | parser.add_argument( 330 | "baudrate", 331 | nargs='?', 332 | type=int, 333 | help="set baud rate, default: %(default)s", 334 | default=default_baudrate) 335 | 336 | group = parser.add_argument_group("port settings") 337 | 338 | group.add_argument( 339 | "--parity", 340 | choices=['N', 'E', 'O', 'S', 'M'], 341 | type=lambda c: c.upper(), 342 | help="set parity, one of {N E O S M}, default: N", 343 | default='N') 344 | 345 | group.add_argument( 346 | "--rtscts", 347 | action="store_true", 348 | help="enable RTS/CTS flow control (default off)", 349 | default=False) 350 | 351 | group.add_argument( 352 | "--xonxoff", 353 | action="store_true", 354 | help="enable software flow control (default off)", 355 | default=False) 356 | 357 | group.add_argument( 358 | "--rts", 359 | type=int, 360 | help="set initial RTS line state (possible values: 0, 1)", 361 | default=default_rts) 362 | 363 | group.add_argument( 364 | "--dtr", 365 | type=int, 366 | help="set initial DTR line state (possible values: 0, 1)", 367 | default=default_dtr) 368 | 369 | group.add_argument( 370 | "--ask", 371 | action="store_true", 372 | help="ask again for port when open fails", 373 | default=False) 374 | 375 | group = parser.add_argument_group("data handling") 376 | 377 | group.add_argument( 378 | "-e", "--echo", 379 | action="store_true", 380 | help="enable local echo (default off)", 381 | default=False) 382 | 383 | group.add_argument( 384 | "--encoding", 385 | metavar="CODEC", 386 | help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s", 387 | default='UTF-8') 388 | 389 | group.add_argument( 390 | "-f", "--filter", 391 | action="append", 392 | metavar="NAME", 393 | help="add text transformation", 394 | default=[]) 395 | 396 | group.add_argument( 397 | "--eol", 398 | choices=['CR', 'LF', 'CRLF'], 399 | type=lambda c: c.upper(), 400 | help="end of line mode", 401 | default='CRLF') 402 | 403 | group = parser.add_argument_group("hotkeys") 404 | 405 | group.add_argument( 406 | "--exit-key", 407 | type=check_key_name, 408 | metavar='KEY', 409 | help="Key that is used to exit the application, default: %(default)s", 410 | default='Ctrl+]') 411 | 412 | group.add_argument( 413 | "--menu-key", 414 | type=check_key_name, 415 | metavar='KEY', 416 | help="Key that is used to control miniterm (menu), default: %(default)s", 417 | default='Ctrl+T') 418 | 419 | group = parser.add_argument_group("diagnostics") 420 | 421 | group.add_argument( 422 | "--develop", 423 | action="store_true", 424 | help="show Python traceback on error", 425 | default=False) 426 | 427 | args = parser.parse_args() 428 | 429 | if args.menu_key == args.exit_key: 430 | parser.error('--exit-key can not be the same as --menu-key') 431 | 432 | if args.filter: 433 | if 'help' in args.filter: 434 | sys.stderr.write('Available filters:\n') 435 | sys.stderr.write('\n'.join( 436 | '{:<10} = {.__doc__}'.format(k, v) 437 | for k, v in sorted(TRANSFORMATIONS.items()))) 438 | sys.stderr.write('\n') 439 | sys.exit(1) 440 | filters = args.filter 441 | else: 442 | filters = ['default'] 443 | 444 | miniterm = Miniterm( 445 | None, 446 | echo=args.echo, 447 | eol=args.eol.lower(), 448 | filters=filters, 449 | features=[ 450 | (startup_message.StartupMessage, {}), 451 | (menu.Menu, {'hot_key': args.menu_key}), 452 | ], 453 | exit_key=args.exit_key) 454 | 455 | while serial_instance is None: 456 | # no port given on command line -> ask user now 457 | if args.port is None or args.port == '-': 458 | try: 459 | args.port = ask_for_port.AskForPort(miniterm).ask_for_port() 460 | except KeyboardInterrupt: 461 | miniterm.console.write('\n') 462 | parser.error('user aborted and port is not given') 463 | else: 464 | if not args.port: 465 | parser.error('port is not given') 466 | try: 467 | serial_instance = serial.serial_for_url( 468 | args.port, 469 | args.baudrate, 470 | parity=args.parity, 471 | rtscts=args.rtscts, 472 | xonxoff=args.xonxoff, 473 | do_not_open=True) 474 | 475 | if args.dtr is not None: 476 | miniterm.console.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive')) 477 | serial_instance.dtr = args.dtr 478 | if args.rts is not None: 479 | miniterm.console.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive')) 480 | serial_instance.rts = args.rts 481 | 482 | serial_instance.open() 483 | except serial.SerialException as e: 484 | serial_instance = None 485 | miniterm.console.write('could not open port {}: {}\n'.format(repr(args.port), e)) 486 | if args.develop: 487 | raise 488 | if not args.ask: 489 | sys.exit(1) 490 | else: 491 | args.port = '-' 492 | else: 493 | break 494 | 495 | if not hasattr(serial_instance, 'cancel_read'): 496 | # enable timeout for alive flag polling if cancel_read is not available 497 | serial_instance.timeout = 1 498 | 499 | miniterm.serial = serial_instance 500 | miniterm.set_rx_encoding(args.encoding) 501 | miniterm.set_tx_encoding(args.encoding) 502 | 503 | miniterm.start() 504 | try: 505 | miniterm.join(True) 506 | except KeyboardInterrupt: 507 | pass 508 | miniterm.console.write("\r\n--- exit ---\r\n") 509 | miniterm.join() 510 | miniterm.close() 511 | 512 | 513 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 514 | if __name__ == '__main__': 515 | main() 516 | -------------------------------------------------------------------------------- /serial_terminal/console/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # This is a wrapper module for different platform implementations 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial 6 | # (C) 2001-2017 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from __future__ import absolute_import 11 | 12 | import os 13 | 14 | if os.name == 'nt': # noqa 15 | from .windows import Console 16 | elif os.name == 'posix': 17 | from .posix import Console 18 | else: 19 | raise NotImplementedError( 20 | 'Sorry no implementation for your platform ({}) available.'.format(os.name)) 21 | -------------------------------------------------------------------------------- /serial_terminal/console/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Core functionality / API 4 | # 5 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 6 | # (C)2002-2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | import sys 11 | 12 | class ConsoleBase(object): 13 | """OS abstraction for console (input/output codec, no echo)""" 14 | 15 | def __init__(self): 16 | if sys.version_info >= (3, 0): 17 | self.byte_output = sys.stdout.buffer 18 | else: 19 | self.byte_output = sys.stdout 20 | self.output = sys.stdout 21 | 22 | def setup(self): 23 | """Set console to read single characters, no echo""" 24 | 25 | def cleanup(self): 26 | """Restore default console settings""" 27 | 28 | def getkey(self): 29 | """Read a single key from the console""" 30 | return None 31 | 32 | def write_bytes(self, byte_string): 33 | """Write bytes (already encoded)""" 34 | self.byte_output.write(byte_string) 35 | self.byte_output.flush() 36 | 37 | def write(self, text): 38 | """Write string""" 39 | self.output.write(text) 40 | self.output.flush() 41 | 42 | def cancel(self): 43 | """Cancel getkey operation""" 44 | 45 | # - - - - - - - - - - - - - - - - - - - - - - - - 46 | # context manager: 47 | # switch terminal temporary to normal mode (e.g. to get user input) 48 | 49 | def __enter__(self): 50 | self.cleanup() 51 | return self 52 | 53 | def __exit__(self, *args, **kwargs): 54 | self.setup() 55 | 56 | 57 | -------------------------------------------------------------------------------- /serial_terminal/console/posix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Posix specific functions 4 | # 5 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 6 | # (C)2002-2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from .base import ConsoleBase 11 | 12 | import atexit 13 | import fcntl 14 | import sys 15 | import termios 16 | import codecs 17 | 18 | 19 | MAP_CONTROL_KEYS = { 20 | '\x00': 'Ctrl+Space', 21 | '\x01': 'Ctrl+A', 22 | '\x02': 'Ctrl+B', 23 | '\x03': 'Ctrl+C', 24 | '\x04': 'Ctrl+D', 25 | '\x05': 'Ctrl+E', 26 | '\x06': 'Ctrl+F', 27 | '\x07': 'Ctrl+G', 28 | '\x08': 'Ctrl+H', 29 | '\x09': 'Tab', # 'Ctrl+I', 30 | '\x0a': 'Ctrl+J', 31 | '\x0b': 'Ctrl+K', 32 | '\x0c': 'Ctrl+L', 33 | '\x0d': 'Ctrl+M', 34 | '\x0e': 'Ctrl+N', 35 | '\x0f': 'Ctrl+O', 36 | '\x10': 'Ctrl+P', 37 | '\x11': 'Ctrl+Q', 38 | '\x12': 'Ctrl+R', 39 | '\x13': 'Ctrl+S', 40 | '\x14': 'Ctrl+T', 41 | '\x15': 'Ctrl+U', 42 | '\x16': 'Ctrl+V', 43 | '\x17': 'Ctrl+W', 44 | '\x18': 'Ctrl+X', 45 | '\x19': 'Ctrl+Y', 46 | '\x1a': 'Ctrl+Z', 47 | '\x1b': 'Esc', # 'Ctrl+[', 48 | '\x1c': 'Ctrl+\\', 49 | '\x1d': 'Ctrl+]', 50 | '\x1e': 'Ctrl+^', 51 | '\x1f': 'Ctrl+_', 52 | } 53 | 54 | CSI_CODES = { 55 | 'H': 'Home', 56 | 'F': 'End', 57 | '1;2H': 'Shift+Home', 58 | '1;2F': 'Shift+End', 59 | '1;3H': 'Alt+Home', 60 | '1;3F': 'Alt+End', 61 | '1;5H': 'Ctrl+Home', 62 | '1;5F': 'Ctrl+End', 63 | '2~': 'Insert', 64 | '3~': 'Delete', 65 | '5~': 'Page Up', 66 | '6~': 'Page Down', 67 | '2;2~': 'Shift+Insert', 68 | '3;2~': 'Shift+Delete', 69 | '5;2~': 'Shift+Page Up', 70 | '6;2~': 'Shift+Page Down', 71 | '2;3~': 'Alt+Insert', 72 | '3;3~': 'Alt+Delete', 73 | '5;3~': 'Alt+Page Up', 74 | '6;3~': 'Alt+Page Down', 75 | '2;5~': 'Ctrl+Insert', 76 | '3;5~': 'Ctrl+Delete', 77 | '5;5~': 'Ctrl+Page Up', 78 | '6;5~': 'Ctrl+Page Down', 79 | '15~': 'F5', 80 | '16~': 'F6', 81 | '17~': 'F7', 82 | '18~': 'F8', 83 | '20~': 'F9', 84 | '21~': 'F10', 85 | '23~': 'F11', 86 | '24~': 'F12', 87 | '1;2P': 'Shift+F1', 88 | '1;2Q': 'Shift+F2', 89 | '1;2R': 'Shift+F3', 90 | '1;2S': 'Shift+F4', 91 | '15;2~': 'Shift+F5', 92 | '16;2~': 'Shift+F6', 93 | '17;2~': 'Shift+F7', 94 | '18;2~': 'Shift+F8', 95 | '20;2~': 'Shift+F9', 96 | '21;2~': 'Shift+F10', 97 | '23;2~': 'Shift+F11', 98 | '24;2~': 'Shift+F12', 99 | '1;3P': 'Alt+F1', 100 | '1;3Q': 'Alt+F2', 101 | '1;3R': 'Alt+F3', 102 | '1;3S': 'Alt+F4', 103 | '15;3~': 'Alt+F5', 104 | '16;3~': 'Alt+F6', 105 | '17;3~': 'Alt+F7', 106 | '18;3~': 'Alt+F8', 107 | '20;3~': 'Alt+F9', 108 | '21;3~': 'Alt+F10', 109 | '23;3~': 'Alt+F11', 110 | '24;3~': 'Alt+F12', 111 | '1;5P': 'Ctrl+F1', 112 | '1;5Q': 'Ctrl+F2', 113 | '1;5R': 'Ctrl+F3', 114 | '1;5S': 'Ctrl+F4', 115 | '15;5~': 'Ctrl+F5', 116 | '16;5~': 'Ctrl+F6', 117 | '17;5~': 'Ctrl+F7', 118 | '18;5~': 'Ctrl+F8', 119 | '20;5~': 'Ctrl+F9', 120 | '21;5~': 'Ctrl+F10', 121 | '23;5~': 'Ctrl+F11', 122 | '24;5~': 'Ctrl+F12', 123 | 'A': 'Up', 124 | 'B': 'Down', 125 | 'C': 'Right', 126 | 'D': 'Left', 127 | '1;2A': 'Shift+Up', 128 | '1;2B': 'Shift+Down', 129 | '1;2C': 'Shift+Right', 130 | '1;2D': 'Shift+Left', 131 | '1;3A': 'Alt+Up', 132 | '1;3B': 'Alt+Down', 133 | '1;3C': 'Alt+Right', 134 | '1;3D': 'Alt+Left', 135 | '1;5A': 'Ctrl+Up', 136 | '1;5B': 'Ctrl+Down', 137 | '1;5C': 'Ctrl+Right', 138 | '1;5D': 'Ctrl+Left', 139 | 'E': 'KP_5', 140 | '1;2E': 'Shift+KP_5', 141 | '1;3E': 'Alt+KP_5', 142 | '1;5E': 'Ctrl+KP_5', 143 | 'Z': 'Shift+Tab', 144 | } 145 | 146 | SS3_CODES = { 147 | 'P': 'F1', 148 | 'Q': 'F2', 149 | 'R': 'F3', 150 | 'S': 'F4', 151 | } 152 | 153 | 154 | class Console(ConsoleBase): 155 | def __init__(self): 156 | super(Console, self).__init__() 157 | self.fd = sys.stdin.fileno() 158 | self.old = termios.tcgetattr(self.fd) 159 | atexit.register(self.cleanup) 160 | if sys.version_info < (3, 0): 161 | self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin) 162 | else: 163 | self.enc_stdin = sys.stdin 164 | 165 | def setup(self): 166 | new = termios.tcgetattr(self.fd) 167 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG 168 | new[6][termios.VMIN] = 1 169 | new[6][termios.VTIME] = 0 170 | termios.tcsetattr(self.fd, termios.TCSANOW, new) 171 | 172 | def getkey(self): 173 | c = self.enc_stdin.read(1) 174 | if c == '\x1b': 175 | return self.handle_esc() 176 | elif c and c < '\x20': 177 | return MAP_CONTROL_KEYS[c] 178 | elif c == '\x7f': 179 | return 'Remove' 180 | else: 181 | return c 182 | 183 | def handle_esc(self): 184 | c = self.enc_stdin.read(1) # should read with timeout so that single ESC press can be handled 185 | if c == '[': # CSI 186 | return self.handle_csi() 187 | elif c == 'O': # SS3 188 | return self.handle_SS3() 189 | elif c == '\x1b': # ESC 190 | return 'Esc' 191 | else: 192 | return 'unknown escape {!r}'.format(c) 193 | 194 | def handle_csi(self): 195 | parameter = [] 196 | while True: 197 | c = self.enc_stdin.read(1) 198 | if '0' <= c <= '9' or c == ';': 199 | parameter.append(c) 200 | else: 201 | try: 202 | return CSI_CODES[''.join(parameter + [c])] 203 | except KeyError: 204 | return 'unknown CSI {}{}'.format(''.join(parameter), c) 205 | 206 | def handle_SS3(self): 207 | c = self.enc_stdin.read(1) 208 | try: 209 | return SS3_CODES[c] 210 | except KeyError: 211 | return 'unknown SS3 code {}'.format(c) 212 | 213 | 214 | def cancel(self): 215 | fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0') 216 | 217 | def cleanup(self): 218 | termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old) 219 | 220 | def set_ansi_color(self, colorcodes): 221 | """set color/intensity for next write(s)""" 222 | 223 | def get_position_and_size(self): 224 | """get cursor position (zero based) and window size""" # XXX buffer size on windows :/ 225 | 226 | def set_cursor_position(self, x, y): 227 | """set cursor position (zero based)""" 228 | # print('setpos', x, y) 229 | 230 | def move_or_scroll_down(self): 231 | """move cursor down, extend and scroll if needed""" 232 | self.write('\n') 233 | 234 | def move_or_scroll_up(self): 235 | """move cursor up, extend and scroll if needed""" 236 | # not entirely correct 237 | x, y, w, h = self.get_position_and_size() 238 | self.set_cursor_position(x, y - 1) 239 | 240 | def erase(self, x, y, width, height, selective=False): 241 | """erase rectangular area""" 242 | # print('erase', x, y, width, height, selective) 243 | 244 | 245 | if __name__ == "__main__": 246 | # test code to show what key codes are generated 247 | console = Console() 248 | console.setup() 249 | while True: 250 | key = console.getkey() 251 | print(repr(key)) 252 | if key == 'Ctrl+D': 253 | break 254 | -------------------------------------------------------------------------------- /serial_terminal/console/tk_widget.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Python 3+ only tkinter terminal widget. 4 | # 5 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | import queue 11 | import tkinter as tk 12 | from tkinter import scrolledtext 13 | 14 | # from .base import ConsoleBase 15 | from ..terminal import constants 16 | 17 | keymap = { 18 | '': 'F1', 19 | '': 'F2', 20 | '': 'F3', 21 | '': 'F4', 22 | '': 'F5', 23 | '': 'F6', 24 | '': 'F7', 25 | '': 'F8', 26 | '': 'F9', 27 | '': 'F10', 28 | '': 'F11', 29 | '': 'F12', 30 | '': 'Shift+F1', 31 | '': 'Shift+F2', 32 | '': 'Shift+F3', 33 | '': 'Shift+F4', 34 | '': 'Shift+F5', 35 | '': 'Shift+F6', 36 | '': 'Shift+F7', 37 | '': 'Shift+F8', 38 | '': 'Shift+F9', 39 | '': 'Shift+F0', 40 | '': 'Shift+F1', 41 | '': 'Shift+F2', 42 | '': 'Ctrl+F1', 43 | '': 'Ctrl+F2', 44 | '': 'Ctrl+F3', 45 | '': 'Ctrl+F4', 46 | '': 'Ctrl+F5', 47 | '': 'Ctrl+F6', 48 | '': 'Ctrl+F7', 49 | '': 'Ctrl+F8', 50 | '': 'Ctrl+F9', 51 | '': 'Ctrl+F10', 52 | '': 'Ctrl+F11', 53 | '': 'Ctrl+F12', 54 | '': 'Shift+Ctrl+F1', 55 | '': 'Shift+Ctrl+F2', 56 | '': 'Shift+Ctrl+F3', 57 | '': 'Shift+Ctrl+F4', 58 | '': 'Shift+Ctrl+F5', 59 | '': 'Shift+Ctrl+F6', 60 | '': 'Shift+Ctrl+F7', 61 | '': 'Shift+Ctrl+F8', 62 | '': 'Shift+Ctrl+F9', 63 | '': 'Shift+Ctrl+F10', 64 | '': 'Up', 65 | '': 'Down', 66 | '': 'Left', 67 | '': 'Right', 68 | '': 'Home', 69 | '': 'End', 70 | '': 'Insert', 71 | '': 'Delete', 72 | '': 'Page Up', 73 | '': 'Page Down', 74 | } 75 | 76 | 77 | MAP_CONTROL_KEYS = { 78 | '\x00': 'Ctrl+Space', 79 | '\x01': 'Ctrl+A', 80 | '\x02': 'Ctrl+B', 81 | '\x03': 'Ctrl+C', 82 | '\x04': 'Ctrl+D', 83 | '\x05': 'Ctrl+E', 84 | '\x06': 'Ctrl+F', 85 | '\x07': 'Ctrl+G', 86 | '\x08': 'Ctrl+H', 87 | '\x09': 'Tab', # 'Ctrl+I', 88 | '\x0a': 'Ctrl+J', 89 | '\x0b': 'Ctrl+K', 90 | '\x0c': 'Ctrl+L', 91 | '\x0d': 'Ctrl+M', 92 | '\x0e': 'Ctrl+N', 93 | '\x0f': 'Ctrl+O', 94 | '\x10': 'Ctrl+P', 95 | '\x11': 'Ctrl+Q', 96 | '\x12': 'Ctrl+R', 97 | '\x13': 'Ctrl+S', 98 | '\x14': 'Ctrl+T', 99 | '\x15': 'Ctrl+U', 100 | '\x16': 'Ctrl+V', 101 | '\x17': 'Ctrl+W', 102 | '\x18': 'Ctrl+X', 103 | '\x19': 'Ctrl+Y', 104 | '\x1a': 'Ctrl+Z', 105 | '\x1b': 'Esc', # 'Ctrl+[', 106 | '\x1c': 'Ctrl+\\', 107 | '\x1d': 'Ctrl+]', 108 | '\x1e': 'Ctrl+^', 109 | '\x1f': 'Ctrl+_', 110 | } 111 | 112 | 113 | class Console(scrolledtext.ScrolledText): 114 | def __init__(self, *args, **kwargs): 115 | # super().__init__(width=80, height=25) 116 | super().__init__(width=132, height=52) 117 | # self.insert(tk.INSERT, '\n'.join(' ' * 132 for line in range(52))) 118 | # self.mark_set(tk.INSERT, 1.0) 119 | self._font = ("DejaVu Sans Mono", 10) 120 | self._bold_font = ("DejaVu Sans Mono", 10, "bold") 121 | self.config(background='#000000', foreground='#bbbbbb', insertbackground='#99ff99', font=self._font) 122 | self.tag_config('30', foreground='#000000') 123 | self.tag_config('31', foreground='#bb0000') 124 | self.tag_config('32', foreground='#00bb00') 125 | self.tag_config('33', foreground='#bbbb00') 126 | self.tag_config('34', foreground='#0000bb') 127 | self.tag_config('35', foreground='#bb00bb') 128 | self.tag_config('36', foreground='#00bbbb') 129 | self.tag_config('37', foreground='#bbbbbb') 130 | self.tag_config('40', background='#000000') 131 | self.tag_config('41', background='#bb0000') 132 | self.tag_config('42', background='#00bb00') 133 | self.tag_config('43', background='#bbbb00') 134 | self.tag_config('44', background='#0000bb') 135 | self.tag_config('45', background='#bb00bb') 136 | self.tag_config('46', background='#00bbbb') 137 | self.tag_config('47', background='#bbbbbb') 138 | self.tag_config('90', foreground='#555555') 139 | self.tag_config('91', foreground='#ff5555') 140 | self.tag_config('92', foreground='#55ff55') 141 | self.tag_config('93', foreground='#ffff55') 142 | self.tag_config('94', foreground='#5555ff') 143 | self.tag_config('95', foreground='#ff55ff') 144 | self.tag_config('96', foreground='#55ffff') 145 | self.tag_config('97', foreground='#ffffff') 146 | self.tag_config('100', background='#555555') 147 | self.tag_config('101', background='#ff5555') 148 | self.tag_config('102', background='#55ff55') 149 | self.tag_config('103', background='#ffff55') 150 | self.tag_config('104', background='#5555ff') 151 | self.tag_config('105', background='#ff55ff') 152 | self.tag_config('106', background='#55ffff') 153 | self.tag_config('107', background='#ffffff') 154 | # self.tag_config('1', font=("Courier", 10, "bold")) 155 | # self.tag_config('0', font=("Courier", 10)) 156 | self.tag_config('1', font=self._bold_font) 157 | self.tag_config('0', font=self._font) 158 | self._fg = '' 159 | self._bg = '' 160 | self._bold = '' 161 | for key, key_name in keymap.items(): 162 | self.bind(key, lambda event, key_name=key_name: self._send_key(key_name)) 163 | self.bind('', lambda event: self._send_key(event.char)) 164 | # avoid selection and direct cursor positioning 165 | self.bind('', lambda event: 'break') 166 | self.bind('', lambda event: 'break') 167 | self._input_queue = queue.Queue() 168 | 169 | def setup(self): 170 | pass 171 | 172 | def _send_key(self, key): 173 | if key in MAP_CONTROL_KEYS: 174 | self._input_queue.put(MAP_CONTROL_KEYS[key]) 175 | else: 176 | self._input_queue.put(key) 177 | return "break" # stop event propagation 178 | 179 | def getkey(self): 180 | """read (named keys) from console""" 181 | return self._input_queue.get() 182 | 183 | def cancel(self): 184 | self._input_queue.put(None) 185 | 186 | def write_bytes(self, text): 187 | self.insert(tk.INSERT, text, (self._fg, self._bg, self._bold)) 188 | 189 | def flush(self): 190 | pass 191 | 192 | def write(self, text): 193 | """write text""" 194 | contents = self.get(tk.INSERT, 'insert+{}c'.format(len(text))) 195 | if contents: 196 | self.delete(tk.INSERT, 'insert+{}c'.format(len(contents))) 197 | self.insert(tk.INSERT, text, (self._fg, self._bg, self._bold)) 198 | 199 | def set_ansi_color(self, colorcodes): 200 | """set color/intensity for next write(s)""" 201 | for colorcode in colorcodes: 202 | if colorcode == 0: 203 | self._fg = '' 204 | self._bg = '' 205 | if colorcode in constants.Foreground.__dict__.values(): 206 | self._fg = colorcode 207 | if colorcode in constants.Background.__dict__.values(): 208 | self._bg = colorcode 209 | if colorcode in (0, 1): 210 | self._bold = colorcode 211 | 212 | def get_position_and_size(self): 213 | """get cursor position (zero based) and window size""" 214 | index = self.index(tk.INSERT) 215 | y, x = [int(s) for s in index.split('.')] 216 | width = self.cget('width') 217 | height = self.cget('height') 218 | # print('getpos', x, y - 1, width, height) 219 | return x, y - 1, width, height 220 | 221 | def set_cursor_position(self, x, y): 222 | """set cursor position (zero based)""" 223 | # print('setpos', x, y) 224 | self.mark_set(tk.INSERT, '{}.{}'.format(y + 1, x)) 225 | 226 | def move_or_scroll_down(self): 227 | """move cursor down, extend and scroll if needed""" 228 | y, x = [int(s) for s in self.index(tk.INSERT).split('.')] 229 | end_y, end_x = [int(s) for s in self.index(tk.END).split('.')] 230 | if y + 1 >= end_y: 231 | self.insert(tk.END, '\n\n' + ' ' * x) 232 | self.mark_set(tk.INSERT, '{}.{}'.format(y + 1, x)) 233 | self.see(tk.INSERT) 234 | 235 | def move_or_scroll_up(self): 236 | """move cursor up, extend and scroll if needed""" 237 | y, x = [int(s) for s in self.index(tk.INSERT).split('.')] 238 | if y - 1 <= 0: 239 | self.insert(1.0, '\n\n' + ' ' * x) 240 | self.mark_set(tk.INSERT, '{}.{}'.format(y - 1, x)) 241 | self.see(tk.INSERT) 242 | 243 | def erase(self, x, y, width, height, selective=False): 244 | """erase rectangular area""" 245 | # print('erase', x, y, width, height, selective) 246 | for _y in range(y + 1, y + height + 1): 247 | self.delete('{}.{}'.format(_y, x), '{}.{}'.format(_y, x + width)) 248 | self.insert('{}.{}'.format(_y, x), ' ' * width) 249 | # if not selective: 250 | # attrs 251 | 252 | 253 | if __name__ == "__main__": 254 | import threading 255 | # test code to show what key codes are generated 256 | root = tk.Tk() 257 | root.title('pySerial-Terminal tk_widget test') 258 | console = Console(root) 259 | console.pack() 260 | console.focus_set() 261 | 262 | def input_thread(): 263 | while True: 264 | key = console.getkey() 265 | console.write('{!r} '.format(key)) 266 | if key == 'Ctrl+D': 267 | root.quit() 268 | t = threading.Thread(target=input_thread, daemon=True) 269 | t.start() 270 | root.mainloop() 271 | -------------------------------------------------------------------------------- /serial_terminal/console/windows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Windows specific functions 4 | # 5 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 6 | # (C)2002-2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | import ctypes, ctypes.wintypes 11 | import codecs 12 | import msvcrt 13 | import sys 14 | import os 15 | from .base import ConsoleBase 16 | from ..terminal import constants 17 | 18 | try: 19 | chr = unichr 20 | except NameError: 21 | pass 22 | 23 | MAP_CONTROL_KEYS = { 24 | '\x00': 'Ctrl+Space', 25 | '\x01': 'Ctrl+A', 26 | '\x02': 'Ctrl+B', 27 | '\x03': 'Ctrl+C', 28 | '\x04': 'Ctrl+D', 29 | '\x05': 'Ctrl+E', 30 | '\x06': 'Ctrl+F', 31 | '\x07': 'Ctrl+G', 32 | '\x08': 'Ctrl+H', 33 | '\x09': 'Tab', # 'Ctrl+I', 34 | '\x0a': 'Ctrl+J', 35 | '\x0b': 'Ctrl+K', 36 | '\x0c': 'Ctrl+L', 37 | '\x0d': 'Ctrl+M', 38 | '\x0e': 'Ctrl+N', 39 | '\x0f': 'Ctrl+O', 40 | '\x10': 'Ctrl+P', 41 | '\x11': 'Ctrl+Q', 42 | '\x12': 'Ctrl+R', 43 | '\x13': 'Ctrl+S', 44 | '\x14': 'Ctrl+T', 45 | '\x15': 'Ctrl+U', 46 | '\x16': 'Ctrl+V', 47 | '\x17': 'Ctrl+W', 48 | '\x18': 'Ctrl+X', 49 | '\x19': 'Ctrl+Y', 50 | '\x1a': 'Ctrl+Z', 51 | '\x1b': 'Esc', # 'Ctrl+[', 52 | '\x1c': 'Ctrl+\\', 53 | '\x1d': 'Ctrl+]', 54 | '\x1e': 'Ctrl+^', 55 | '\x1f': 'Ctrl+_', 56 | } 57 | 58 | MAP_F_KEYS = { 59 | '\x03': 'Ctrl+Space', # actually CTRL+2 but linux has this as alias for Ctrl+Space too 60 | '\x3b': 'F1', 61 | '\x3c': 'F2', 62 | '\x3d': 'F3', 63 | '\x3e': 'F4', 64 | '\x3f': 'F5', 65 | '\x40': 'F6', 66 | '\x41': 'F7', 67 | '\x42': 'F8', 68 | '\x43': 'F9', 69 | '\x44': 'F10', 70 | '\x47': 'KP_7', 71 | '\x48': 'KP_8', 72 | '\x49': 'KP_9', 73 | '\x4b': 'KP_4', 74 | '\x4d': 'KP_6', 75 | '\x4f': 'KP_1', 76 | '\x50': 'KP_2', 77 | '\x51': 'KP_3', 78 | '\x52': 'KP_0', 79 | '\x53': 'KP_Dot', 80 | '\x54': 'Shift+F1', 81 | '\x55': 'Shift+F2', 82 | '\x56': 'Shift+F3', 83 | '\x57': 'Shift+F4', 84 | '\x58': 'Shift+F5', 85 | '\x59': 'Shift+F6', 86 | '\x5a': 'Shift+F7', 87 | '\x5b': 'Shift+F8', 88 | '\x5c': 'Shift+F9', 89 | '\x5d': 'Shift+F10', 90 | '\x5e': 'Ctrl+F1', 91 | '\x5f': 'Ctrl+F2', 92 | '\x60': 'Ctrl+F3', 93 | '\x61': 'Ctrl+F4', 94 | '\x62': 'Ctrl+F5', 95 | '\x63': 'Ctrl+F6', 96 | '\x64': 'Ctrl+F7', 97 | '\x65': 'Ctrl+F8', 98 | '\x66': 'Ctrl+F9', 99 | '\x67': 'Ctrl+F10', 100 | '\x68': 'Alt+F1', 101 | '\x69': 'Alt+F2', 102 | '\x6a': 'Alt+F3', 103 | '\x6b': 'Alt+F4', 104 | '\x6c': 'Alt+F5', 105 | '\x6d': 'Alt+F6', 106 | '\x6e': 'Alt+F7', 107 | '\x6f': 'Alt+F8', 108 | '\x70': 'Alt+F9', 109 | '\x71': 'Alt+F10', 110 | '\x73': 'Ctrl+KP_4', 111 | '\x74': 'Ctrl+KP_6', 112 | '\x75': 'Ctrl+KP_1', 113 | '\x76': 'Ctrl+KP_3', 114 | '\x77': 'Ctrl+KP_7', 115 | '\x83': 'Ctrl+^', 116 | '\x84': 'Ctrl+KP_9', 117 | '\x8d': 'Ctrl+KP_8', 118 | '\x91': 'Ctrl+KP_2', 119 | '\x92': 'Ctrl+KP_0', 120 | '\x93': 'Ctrl+KP_Dot', 121 | '\x94': 'Ctrl+Tab', 122 | '\x95': 'Ctrl+KP_Divide', 123 | '\x98': 'Alt+Up', 124 | '\x9b': 'Alt+Left', 125 | '\x9d': 'Alt+Right', 126 | '\x97': 'Alt+Home', 127 | '\x99': 'Alt+Page Up', 128 | '\x9f': 'Alt+End', 129 | '\xa0': 'Alt+Down', 130 | '\xa1': 'Alt+Page Down', 131 | '\xa2': 'Alt+Insert', 132 | '\xa3': 'Alt+Delete', 133 | } 134 | 135 | MAP_SPECIAL_KEYS = { 136 | '\x47': 'Home', 137 | '\x48': 'Up', 138 | '\x49': 'Page Up', 139 | '\x4b': 'Left', 140 | '\x4d': 'Right', 141 | '\x4f': 'End', 142 | '\x50': 'Down', 143 | '\x51': 'Page Down', 144 | '\x52': 'Insert', 145 | '\x53': 'Delete', 146 | '\x73': 'Ctrl+Right', 147 | '\x74': 'Ctrl+Left', 148 | '\x75': 'Ctrl+End', 149 | '\x76': 'Ctrl+Page Down', # inconsistency, Ctrl+Page Up reports the same as F12 150 | '\x77': 'Ctrl+Home', 151 | '\x85': 'F11', 152 | '\x86': 'F12', 153 | '\x87': 'Shift+F11', 154 | '\x88': 'Shift+F12', 155 | '\x89': 'Ctrl+F11', 156 | '\x8a': 'Ctrl+F12', 157 | '\x8b': 'Alt+F11', 158 | '\x8c': 'Alt+F12', 159 | '\x8d': 'Ctrl+Up', 160 | '\x91': 'Ctrl+Down', 161 | '\x92': 'Ctrl+Insert', 162 | '\x93': 'Ctrl+Delete', 163 | '\xa2': 'Alt+Insert', 164 | '\xa3': 'Alt+Delete', 165 | } 166 | 167 | 168 | class Out(object): 169 | """file-like wrapper that uses os.write""" 170 | 171 | def __init__(self, fd): 172 | self.fd = fd 173 | 174 | def flush(self): 175 | pass 176 | 177 | def write(self, s): 178 | os.write(self.fd, s) 179 | 180 | 181 | STDOUT = -11 182 | STDERR = -12 183 | 184 | 185 | BLACK = 0 186 | BLUE = 1 187 | GREEN = 2 188 | CYAN = 3 189 | RED = 4 190 | MAGENTA = 5 191 | YELLOW = 6 192 | GREY = 7 193 | 194 | NORMAL = 0x00 195 | BRIGHT = 0x08 196 | 197 | 198 | terminal_colors_to_windows_colors = { 199 | 0: (255, GREY), 200 | constants.Foreground.BLACK: (7, BLACK), 201 | constants.Foreground.RED: (7, RED), 202 | constants.Foreground.GREEN: (7, GREEN), 203 | constants.Foreground.YELLOW: (7, YELLOW), 204 | constants.Foreground.BLUE: (7, BLUE), 205 | constants.Foreground.MAGENTA: (7, MAGENTA), 206 | constants.Foreground.CYAN: (7, CYAN), 207 | constants.Foreground.WHITE: (7, GREY), 208 | constants.Foreground.LIGHTBLACK: (0x0f, BLACK | BRIGHT), 209 | constants.Foreground.LIGHTRED: (0x0f, RED | BRIGHT), 210 | constants.Foreground.LIGHTGREEN: (0x0f, GREEN | BRIGHT), 211 | constants.Foreground.LIGHTYELLOW: (0x0f, YELLOW | BRIGHT), 212 | constants.Foreground.LIGHTBLUE: (0x0f, BLUE | BRIGHT), 213 | constants.Foreground.LIGHTMAGENTA: (0x0f, MAGENTA | BRIGHT), 214 | constants.Foreground.LIGHTCYAN: (0x0f, CYAN | BRIGHT), 215 | constants.Foreground.LIGHTWHITE: (0x0f, GREY | BRIGHT), 216 | constants.Background.BLACK: (7 << 4, BLACK << 4), 217 | constants.Background.RED: (7 << 4, RED << 4), 218 | constants.Background.GREEN: (7 << 4, GREEN << 4), 219 | constants.Background.YELLOW: (7 << 4, YELLOW << 4), 220 | constants.Background.BLUE: (7 << 4, BLUE << 4), 221 | constants.Background.MAGENTA: (7 << 4, MAGENTA << 4), 222 | constants.Background.CYAN: (7 << 4, CYAN << 4), 223 | constants.Background.WHITE: (7 << 4, GREY << 4), 224 | constants.Background.LIGHTBLACK: (0xf0, (BLACK | BRIGHT) << 4), 225 | constants.Background.LIGHTRED: (0xf0, (RED | BRIGHT) << 4), 226 | constants.Background.LIGHTGREEN: (0xf0, (GREEN | BRIGHT) << 4), 227 | constants.Background.LIGHTYELLOW: (0xf0, (YELLOW | BRIGHT) << 4), 228 | constants.Background.LIGHTBLUE: (0xf0, (BLUE | BRIGHT) << 4), 229 | constants.Background.LIGHTMAGENTA: (0xf0, (MAGENTA | BRIGHT) << 4), 230 | constants.Background.LIGHTCYAN: (0xf0, (CYAN | BRIGHT) << 4), 231 | constants.Background.LIGHTWHITE: (0xf0, (GREY | BRIGHT) << 4), 232 | constants.Style.BRIGHT: (0x08, BRIGHT), 233 | constants.Style.DIM: (0x08, NORMAL), 234 | constants.Style.NORMAL: (0x08, NORMAL), 235 | } 236 | 237 | 238 | class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): 239 | _fields_ = [ 240 | ("dwSize", ctypes.wintypes._COORD), 241 | ("dwCursorPosition", ctypes.wintypes._COORD), 242 | ("wAttributes", ctypes.wintypes.WORD), 243 | ("srWindow", ctypes.wintypes.SMALL_RECT), 244 | ("dwMaximumWindowSize", ctypes.wintypes._COORD), 245 | ] 246 | 247 | 248 | class Console(ConsoleBase): 249 | def __init__(self): 250 | super(Console, self).__init__() 251 | self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP() 252 | self._saved_icp = ctypes.windll.kernel32.GetConsoleCP() 253 | ctypes.windll.kernel32.SetConsoleOutputCP(65001) 254 | ctypes.windll.kernel32.SetConsoleCP(65001) 255 | #~ self.output = colorama.AnsiToWin32(codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')).stream 256 | self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace') 257 | # the change of the code page is not propagated to Python, manually fix it 258 | sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace') 259 | sys.stdout = self.output 260 | self.output.encoding = 'UTF-8' # needed for input 261 | self.handle = ctypes.windll.kernel32.GetStdHandle(STDOUT) 262 | self.console_handle = ctypes.windll.kernel32.GetConsoleWindow() 263 | 264 | def __del__(self): 265 | ctypes.windll.kernel32.SetConsoleTextAttribute(self.handle, GREY) 266 | ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp) 267 | ctypes.windll.kernel32.SetConsoleCP(self._saved_icp) 268 | 269 | def getkey(self): 270 | """read (named keys) from console""" 271 | while True: 272 | z = msvcrt.getwch() 273 | if z in chr(0): # functions keys 274 | code = msvcrt.getwch() 275 | try: 276 | return MAP_F_KEYS[code] 277 | except KeyError: 278 | return 'unknown F key {!r} {:02x}'.format(code, ord(code)) 279 | elif z in chr(0xe0): # cursor keys, home/end 280 | code = msvcrt.getwch() 281 | try: 282 | return MAP_SPECIAL_KEYS[code] 283 | except KeyError: 284 | return 'unknown special key {!r} {:02x}'.format(code, ord(code)) 285 | elif z < '\x20': 286 | return MAP_CONTROL_KEYS[z] 287 | else: 288 | return z 289 | 290 | def cancel(self): 291 | # CancelIo, CancelSynchronousIo do not seem to work when using 292 | # getwch, so instead, send a key to the window with the console 293 | ctypes.windll.user32.PostMessageA(self.console_handle, 0x100, 0x0d, 0) 294 | 295 | def write_bytes(self, byte_str): 296 | self.byte_output.write(byte_str) 297 | self.byte_output.flush() 298 | 299 | def write(self, text): 300 | """write text""" 301 | chars_written = ctypes.wintypes.DWORD() 302 | ctypes.windll.kernel32.WriteConsoleW( 303 | self.handle, 304 | ctypes.c_wchar_p(text), 305 | len(text), 306 | ctypes.byref(chars_written), 307 | None) 308 | 309 | def set_ansi_color(self, colorcodes): 310 | """set color/intensity for next write(s)""" 311 | attrs = 0 312 | for colorcode in colorcodes: 313 | mask, code = terminal_colors_to_windows_colors[colorcode] 314 | # print(attrs, bin((~mask) & 0xffff), code) 315 | attrs = (attrs & ~mask) | code 316 | # print('xxx', self.handle, attrs) 317 | ctypes.windll.kernel32.SetConsoleTextAttribute(self.handle, attrs) 318 | 319 | def get_position_and_size(self): 320 | """get cursor position (zero based) and window size""" # XXX buffer size on windows :/ 321 | info = CONSOLE_SCREEN_BUFFER_INFO() 322 | ctypes.windll.kernel32.GetConsoleScreenBufferInfo(self.handle, ctypes.byref(info)) 323 | # print('getpos', info.dwCursorPosition.X, info.dwCursorPosition.Y, info.dwSize.X, info.dwSize.Y) 324 | return info.dwCursorPosition.X, info.dwCursorPosition.Y, info.dwSize.X, info.dwSize.Y 325 | 326 | def set_cursor_position(self, x, y): 327 | """set cursor position (zero based)""" 328 | # print('setpos', x, y) 329 | # XXX should limit to last line visible? the windows console starts at the top of the buffer instead of the end 330 | ctypes.windll.kernel32.SetConsoleCursorPosition( 331 | self.handle, 332 | ctypes.wintypes._COORD(x, y)) 333 | 334 | def move_or_scroll_down(self): 335 | """move cursor down, extend and scroll if needed""" 336 | self.write('\n') 337 | 338 | def move_or_scroll_up(self): 339 | """move cursor up, extend and scroll if needed""" 340 | # not entirely correct 341 | x, y, w, h = self.get_position_and_size() 342 | self.set_cursor_position(x, y - 1) 343 | 344 | def erase(self, x, y, width, height, selective=False): 345 | """erase rectangular area""" 346 | # print('erase', x, y, width, height, selective) 347 | chars_written = ctypes.wintypes.DWORD() 348 | spaces = ctypes.c_wchar_p(' ' * width) 349 | attrs = (ctypes.wintypes.WORD * width)() 350 | for _y in range(y, y + height): 351 | ctypes.windll.kernel32.WriteConsoleOutputCharacterW( 352 | self.handle, 353 | spaces, 354 | width, 355 | ctypes.wintypes._COORD(x, _y), # dwWriteCoord 356 | ctypes.byref(chars_written)) 357 | if not selective: 358 | ctypes.windll.kernel32.WriteConsoleOutputAttribute( 359 | self.handle, 360 | ctypes.byref(attrs), 361 | width, 362 | ctypes.wintypes._COORD(x, _y), # dwWriteCoord 363 | ctypes.byref(chars_written)) 364 | 365 | 366 | if __name__ == "__main__": 367 | # test code to show what key codes are generated 368 | console = Console() 369 | while True: 370 | key = console.getkey() 371 | print(repr(key)) 372 | if key == 'Ctrl+D': 373 | break 374 | -------------------------------------------------------------------------------- /serial_terminal/emulation/simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Terminal emulation with some basic color and cursor movement. 4 | # 5 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | import codecs 11 | 12 | 13 | class SimpleTerminal: 14 | def __init__(self, console): 15 | self.console = console 16 | self.decoder = codecs.getincrementaldecoder('utf-8')('replace') 17 | 18 | def select_graphic_rendition(self, colorcodes): 19 | if not colorcodes: 20 | colorcodes = [0] 21 | self.console.set_ansi_color(colorcodes) 22 | 23 | def carriage_return(self): 24 | x, y, width, height = self.console.get_position_and_size() 25 | self.console.set_cursor_position(0, y) 26 | # self.console.write_bytes(b'\r') 27 | 28 | def line_feed(self): 29 | self.console.move_or_scroll_down() 30 | 31 | def index(self): 32 | self.console.move_or_scroll_down() 33 | 34 | def reverse_index(self): 35 | self.console.move_or_scroll_up() 36 | 37 | def backspace(self): 38 | """move the cursor to the left""" 39 | x, y, width, height = self.console.get_position_and_size() 40 | self.console.set_cursor_position(x - 1, y) 41 | # self.console.write_bytes(b'\b') 42 | 43 | def delete(self): 44 | """move the cursor to the left, overprinting the character with a space""" 45 | x, y, width, height = self.console.get_position_and_size() 46 | self.console.erase(x - 1, y, 1, 1, False) 47 | self.console.set_cursor_position(x - 1, y) 48 | # self.console.write_bytes(b'\b \b') 49 | 50 | def bell(self): 51 | self.console.write_bytes(b'\g') 52 | 53 | def horizontal_tab(self): 54 | x, y, width, height = self.console.get_position_and_size() 55 | self.console.set_cursor_position(x + (8 - x % 8), y) 56 | # self.console.write_bytes(b'\t') 57 | 58 | # def enquiry(self): 59 | # def vertical_tabulation(self): 60 | # def form_feed(self): 61 | # def shift_out(self): 62 | # def shift_in(self): 63 | # def device_control_1(self): 64 | # def device_control_2(self): 65 | # def device_control_3(self): 66 | # def device_control_4(self): 67 | # def cancel(self): 68 | # def substitute(self): 69 | # def next_line(self): 70 | # def horizontal_tab_set(self): 71 | # def single_shift_G2(self): 72 | # def single_shift_G3(self): 73 | # def device_control_string(self): 74 | # def string_terminator(self): 75 | # def operating_system_command(self): 76 | # def application_program_command(self): 77 | # def privacy_message(self): 78 | # def keypad_as_application(self): 79 | # def keypad_as_numeric(self): 80 | # def save_cursor(self): 81 | # def restore_cursor(self): 82 | # def convert_c1_codes(self, flag): 83 | # def handle_flag(self, flag_index, is_extra, value): 84 | 85 | def cursor_up(self, count): 86 | if count == 0: 87 | count = 1 88 | x, y, width, height = self.console.get_position_and_size() 89 | self.console.set_cursor_position(x, max(0, y - count)) 90 | 91 | def cursor_down(self, count): 92 | if count == 0: 93 | count = 1 94 | x, y, width, height = self.console.get_position_and_size() 95 | self.console.set_cursor_position(x, min(height - 1, y + count)) 96 | 97 | def cursor_forward(self, count): 98 | if count == 0: 99 | count = 1 100 | x, y, width, height = self.console.get_position_and_size() 101 | self.console.set_cursor_position(min(width - 1, x + count), y) 102 | 103 | def cursor_backward(self, count): 104 | if count == 0: 105 | count = 1 106 | x, y, width, height = self.console.get_position_and_size() 107 | self.console.set_cursor_position(max(0, x - count), y) 108 | 109 | def cursor_position(self, column, line): 110 | y = (line - 1) if line > 0 else 0 111 | x = (column - 1) if column > 0 else 0 112 | self.console.set_cursor_position(x, y) 113 | 114 | # def insert_line(self, mode): 115 | # def delete_line(self, mode): 116 | # def insert_character(self, mode): 117 | # def delete_character(self, mode): 118 | # def erase_display(self, mode, selective=False): 119 | 120 | def erase_in_line(self, mode, selective=False): 121 | x, y, width, height = self.console.get_position_and_size() 122 | if mode == 0: # erase to end of line 123 | self.console.erase(x, y, width - x, 1, selective) 124 | elif mode == 1: # erase to start of line 125 | self.console.erase(0, y, x, 1, selective) 126 | elif mode == 2: # erase the complete line 127 | self.console.erase(0, y, width, 1, selective) 128 | else: 129 | raise ValueError('bad mode selection: {}'.format(mode)) 130 | self.console.set_cursor_position(x, y) 131 | 132 | # def erase_character(self, mode, selective=False): 133 | # def clear_tabulation(self, mode): 134 | # def set_scroll_region_margins(self, a, b): 135 | 136 | def write(self, byte): 137 | data = self.decoder.decode(byte) 138 | self.console.write(data) 139 | -------------------------------------------------------------------------------- /serial_terminal/features/README.rst: -------------------------------------------------------------------------------- 1 | Here are some plug-in like modules that provide features to the terminal 2 | application. 3 | 4 | e.g. 5 | 6 | - send files 7 | - dump port settings 8 | - ... 9 | -------------------------------------------------------------------------------- /serial_terminal/features/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Base class for extensions of pySerial-terminal 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | 11 | class Feature: 12 | """Provide a base class for extensions of the terminal application""" 13 | 14 | def __init__(self, miniterm): 15 | self.miniterm = miniterm 16 | self.console = miniterm.console 17 | 18 | @property 19 | def serial(self): 20 | return self.miniterm.serial 21 | 22 | def register_hotkey(self, key_name, callback): 23 | self.miniterm.hotkeys[key_name] = callback 24 | 25 | def start(self): 26 | """called by application when it is ready""" 27 | 28 | def message(self, text): 29 | """print a message to the console""" 30 | self.console.write(text.replace('\n', '\r\n')) 31 | 32 | def ask_string(self, question=None): 33 | if question: 34 | self.message(question) 35 | text = [] 36 | while True: 37 | key = self.console.getkey() 38 | if key in ('\r', 'Enter', 'Ctrl+J', 'Ctrl+M'): 39 | break 40 | elif key in ('Esc', 'Crtl+C', 'Ctrl+D'): 41 | raise KeyboardInterrupt('user canceled') 42 | elif key == '\b': 43 | if text: 44 | text.pop() 45 | self.console.write('\b \b') 46 | elif len(key) == 1: 47 | text.append(key) 48 | self.console.write(key) 49 | self.console.write('\r\n') 50 | return ''.join(text) 51 | -------------------------------------------------------------------------------- /serial_terminal/features/ask_for_port.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Print port settings extension. 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from .api import Feature 11 | from serial.tools.list_ports import comports 12 | 13 | 14 | class AskForPort(Feature): 15 | def ask_for_port(self): 16 | """\ 17 | Show a list of ports and ask the user for a choice. To make selection 18 | easier on systems with long device names, also allow the input of an 19 | index. 20 | """ 21 | self.message('\n--- Available ports:\n') 22 | ports = [] 23 | for n, (port, desc, hwid) in enumerate(sorted(comports()), 1): 24 | self.message('--- {:2}: {:20} {}\n'.format(n, port, desc)) 25 | ports.append(port) 26 | while True: 27 | port = self.ask_string('--- Enter port index or full name: ') 28 | try: 29 | index = int(port) - 1 30 | if not 0 <= index < len(ports): 31 | self.message('--- Invalid index!\n') 32 | continue 33 | except ValueError: 34 | pass 35 | else: 36 | port = ports[index] 37 | return port 38 | -------------------------------------------------------------------------------- /serial_terminal/features/menu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Print port settings extension. 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from .api import Feature 11 | from . import ask_for_port, print_port_settings, send_file 12 | import serial 13 | 14 | 15 | class Menu(Feature): 16 | def __init__(self, *args, hot_key='Ctrl+T'): 17 | super().__init__(*args) 18 | self.hot_key = hot_key 19 | self.register_hotkey(hot_key, self.handle_menu_key) 20 | 21 | def start(self): 22 | self.message('--- Quit: {} | Menu: {} | Help: {} followed by Ctrl+H ---\r\n'.format( 23 | self.miniterm.exit_key, 24 | self.hot_key, 25 | self.hot_key)) 26 | 27 | def handle_menu_key(self, key_name): 28 | """Implement a simple menu / settings""" 29 | c = self.console.getkey() # read action key 30 | if c == self.hot_key or c == self.miniterm.exit_key: 31 | # Menu/exit character again -> send itself 32 | self.miniterm.send_key(c) 33 | elif c == 'Ctrl+U': # upload file 34 | send_file.SendFile(self.miniterm).execute() # XXX 35 | elif c in ['Ctrl+H', 'h', 'H', '?']: # Show help 36 | self.message(self.get_help_text()) 37 | elif c == 'Ctrl+R': # Toggle RTS 38 | self.serial.rts = not self.serial.rts 39 | self.message('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive')) 40 | elif c == 'Ctrl+D': # Toggle DTR 41 | self.serial.dtr = not self.serial.dtr 42 | self.message('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive')) 43 | elif c == 'Ctrl+B': # toggle BREAK condition 44 | self.serial.break_condition = not self.serial.break_condition 45 | self.message('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive')) 46 | elif c == 'Ctrl+E': # toggle local echo 47 | self.echo = not self.echo 48 | self.message('--- local echo {} ---\n'.format('active' if self.echo else 'inactive')) 49 | elif c == 'Ctrl+F': # edit filters 50 | self.message('\n--- Available Filters:\n') 51 | self.message('\n'.join( 52 | '--- {:<10} = {.__doc__}'.format(k, v) 53 | for k, v in sorted(TRANSFORMATIONS.items()))) 54 | self.message('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.miniterm.filters))) 55 | new_filters = self.ask_string().lower().split() 56 | if new_filters: 57 | for f in new_filters: 58 | if f not in TRANSFORMATIONS: 59 | self.message('--- unknown filter: {}'.format(repr(f))) 60 | break 61 | else: 62 | self.miniterm.filters = new_filters 63 | self.miniterm.update_transformations() 64 | self.message('--- filters: {}\n'.format(' '.join(self.filters))) 65 | elif c == 'Ctrl+L': # EOL mode 66 | modes = list(EOL_TRANSFORMATIONS) # keys 67 | eol = modes.index(self.miniterm.eol) + 1 68 | if eol >= len(modes): 69 | eol = 0 70 | self.miniterm.eol = modes[eol] 71 | self.message('--- EOL: {} ---\n'.format(self.eol.upper())) 72 | self.miniterm.update_transformations() 73 | elif c == 'Ctrl+A': # set encoding 74 | self.message('\n--- Enter new encoding name [{}]: '.format(self.input_encoding)) 75 | new_encoding = self.ask_string().strip() 76 | if new_encoding: 77 | try: 78 | codecs.lookup(new_encoding) 79 | except LookupError: 80 | self.message('--- invalid encoding name: {}\n'.format(new_encoding)) 81 | else: 82 | self.set_rx_encoding(new_encoding) 83 | self.set_tx_encoding(new_encoding) 84 | self.message('--- serial input encoding: {}\n'.format(self.input_encoding)) 85 | self.message('--- serial output encoding: {}\n'.format(self.output_encoding)) 86 | elif c == 'Tab': # info 87 | self.dump_port_settings() 88 | elif c in 'pP': # P -> change port 89 | try: 90 | port = ask_for_port.AskForPort(self.miniterm).ask_for_port() 91 | except KeyboardInterrupt: 92 | port = None 93 | if port and port != self.serial.port: 94 | # reader thread needs to be shut down 95 | self.miniterm._stop_reader() 96 | # save settings 97 | settings = self.serial.getSettingsDict() 98 | try: 99 | new_serial = serial.serial_for_url(port, do_not_open=True) 100 | # restore settings and open 101 | new_serial.applySettingsDict(settings) 102 | new_serial.rts = self.serial.rts 103 | new_serial.dtr = self.serial.dtr 104 | new_serial.open() 105 | new_serial.break_condition = self.serial.break_condition 106 | except Exception as e: 107 | self.message('--- ERROR opening new port: {} ---\n'.format(e)) 108 | new_serial.close() 109 | else: 110 | self.serial.close() 111 | self.miniterm.serial = new_serial 112 | self.message('--- Port changed to: {} ---\n'.format(self.serial.port)) 113 | # and restart the reader thread 114 | self.miniterm._start_reader() 115 | elif c in 'bB': # B -> change baudrate 116 | self.message('\n--- Baudrate: ') 117 | backup = self.serial.baudrate 118 | try: 119 | self.serial.baudrate = int(self.ask_string().strip()) 120 | except ValueError as e: 121 | self.message('--- ERROR setting baudrate: {} ---\n'.format(e)) 122 | self.serial.baudrate = backup 123 | else: 124 | self.dump_port_settings() 125 | elif c == '8': # 8 -> change to 8 bits 126 | self.serial.bytesize = serial.EIGHTBITS 127 | self.dump_port_settings() 128 | elif c == '7': # 7 -> change to 8 bits 129 | self.serial.bytesize = serial.SEVENBITS 130 | self.dump_port_settings() 131 | elif c in 'eE': # E -> change to even parity 132 | self.serial.parity = serial.PARITY_EVEN 133 | self.dump_port_settings() 134 | elif c in 'oO': # O -> change to odd parity 135 | self.serial.parity = serial.PARITY_ODD 136 | self.dump_port_settings() 137 | elif c in 'mM': # M -> change to mark parity 138 | self.serial.parity = serial.PARITY_MARK 139 | self.dump_port_settings() 140 | elif c in 'sS': # S -> change to space parity 141 | self.serial.parity = serial.PARITY_SPACE 142 | self.dump_port_settings() 143 | elif c in 'nN': # N -> change to no parity 144 | self.serial.parity = serial.PARITY_NONE 145 | self.dump_port_settings() 146 | elif c == '1': # 1 -> change to 1 stop bits 147 | self.serial.stopbits = serial.STOPBITS_ONE 148 | self.dump_port_settings() 149 | elif c == '2': # 2 -> change to 2 stop bits 150 | self.serial.stopbits = serial.STOPBITS_TWO 151 | self.dump_port_settings() 152 | elif c == '3': # 3 -> change to 1.5 stop bits 153 | self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE 154 | self.dump_port_settings() 155 | elif c in 'xX': # X -> change software flow control 156 | self.serial.xonxoff = (c == 'X') 157 | self.dump_port_settings() 158 | elif c in 'rR': # R -> change hardware flow control 159 | self.serial.rtscts = (c == 'R') 160 | self.dump_port_settings() 161 | elif c in 'qQ': # Q -> quit 162 | self.miniterm.stop() 163 | else: 164 | self.message('--- unknown menu key {} --\n'.format(c)) 165 | 166 | def dump_port_settings(self): 167 | """Write current settings to console""" 168 | print_port_settings.PrintPortSettings(self.miniterm).execute() 169 | self.message('--- serial input encoding: {}\n'.format(self.miniterm.input_encoding)) 170 | self.message('--- serial output encoding: {}\n'.format(self.miniterm.output_encoding)) 171 | self.message('--- EOL: {}\n'.format(self.miniterm.eol.upper())) 172 | self.message('--- filters: {}\n'.format(' '.join(self.miniterm.filters))) 173 | 174 | def get_help_text(self): 175 | """return the help text""" 176 | # help text, starts with blank line! 177 | return """ 178 | --- pySerial-terminal ({version}) - help 179 | --- 180 | --- {exit:8} or {menu} Q Exit program 181 | --- {menu:8} Menu escape key, followed by: 182 | --- Menu keys: 183 | --- {menu:7} Send the menu character itself to remote 184 | --- {exit:7} Send the exit character itself to remote 185 | --- Ctrl+I Show info 186 | --- Ctrl+U Upload file (prompt will be shown) 187 | --- Ctrl+A encoding 188 | --- Ctrl+F edit filters 189 | --- Toggles: 190 | --- Ctrl+R RTS Ctrl+D DTR Ctrl+B BREAK 191 | --- Ctrl+E echo Ctrl+L EOL 192 | --- 193 | --- Port settings ({menu} followed by the following): 194 | --- p change port 195 | --- 7 8 set data bits 196 | --- N E O S M change parity (None, Even, Odd, Space, Mark) 197 | --- 1 2 3 set stop bits (1, 2, 1.5) 198 | --- b change baud rate 199 | --- x X disable/enable software flow control 200 | --- r R disable/enable hardware flow control 201 | """.format(version='XXX' or __version__, exit=self.miniterm.exit_key, menu=self.hot_key) 202 | -------------------------------------------------------------------------------- /serial_terminal/features/print_port_settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Print port settings extension. 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from .api import Feature 11 | import serial 12 | 13 | 14 | class PrintPortSettings(Feature): 15 | 16 | def execute(self): 17 | """Write current settings to sys.stderr""" 18 | self.message("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format( 19 | p=self.serial)) 20 | self.message('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format( 21 | ('active' if self.serial.rts else 'inactive'), 22 | ('active' if self.serial.dtr else 'inactive'), 23 | ('active' if self.serial.break_condition else 'inactive'))) 24 | try: 25 | self.message('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format( 26 | ('active' if self.serial.cts else 'inactive'), 27 | ('active' if self.serial.dsr else 'inactive'), 28 | ('active' if self.serial.ri else 'inactive'), 29 | ('active' if self.serial.cd else 'inactive'))) 30 | except serial.SerialException: 31 | # on RFC 2217 ports, it can happen if no modem state notification was 32 | # yet received. ignore this error. 33 | pass 34 | self.message('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive')) 35 | self.message('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive')) 36 | # self.message('--- serial input encoding: {}\n'.format(self.input_encoding)) 37 | # self.message('--- serial output encoding: {}\n'.format(self.output_encoding)) 38 | # self.message('--- EOL: {}\n'.format(self.eol.upper())) 39 | # self.message('--- filters: {}\n'.format(' '.join(self.filters))) 40 | -------------------------------------------------------------------------------- /serial_terminal/features/send_file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Send file contents extension 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from .api import Feature 11 | 12 | class SendFile(Feature): 13 | # {'menu_key': 'Ctrl+U'} 14 | 15 | def execute(self): 16 | self.message('\n--- File to upload: ') 17 | with self.console: 18 | filename = self.ask_string().rstrip('\r\n') 19 | if filename: 20 | try: 21 | with open(filename, 'rb') as f: 22 | self.message('--- Sending file {} ---\n'.format(filename)) 23 | while True: 24 | block = f.read(1024) 25 | if not block: 26 | break 27 | self.serial.write(block) 28 | # Wait for output buffer to drain. 29 | self.serial.flush() 30 | self.message('.') # Progress indicator. 31 | self.message('\n--- File {} sent ---\n'.format(filename)) 32 | except IOError as e: 33 | self.message('--- ERROR opening file {}: {} ---\n'.format(filename, e)) 34 | -------------------------------------------------------------------------------- /serial_terminal/features/startup_message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Write a info message at startup. 4 | # 5 | # This file is part of pySerial. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | from .api import Feature 11 | 12 | 13 | class StartupMessage(Feature): 14 | def start(self): 15 | self.miniterm.console.write('--- pySerial-terminal on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\r\n'.format( 16 | p=self.serial)) 17 | -------------------------------------------------------------------------------- /serial_terminal/terminal/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Some constants related to ANSI and other terminals 4 | # 5 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 6 | # (C) 2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | 11 | class Foreground: 12 | BLACK = 30 13 | RED = 31 14 | GREEN = 32 15 | YELLOW = 33 16 | BLUE = 34 17 | MAGENTA = 35 18 | CYAN = 36 19 | WHITE = 37 20 | RESET = 39 21 | LIGHTBLACK = 90 22 | LIGHTRED = 91 23 | LIGHTGREEN = 92 24 | LIGHTYELLOW = 93 25 | LIGHTBLUE = 94 26 | LIGHTMAGENTA = 95 27 | LIGHTCYAN = 96 28 | LIGHTWHITE = 97 29 | 30 | 31 | class Background: 32 | BLACK = 40 33 | RED = 41 34 | GREEN = 42 35 | YELLOW = 43 36 | BLUE = 44 37 | MAGENTA = 45 38 | CYAN = 46 39 | WHITE = 47 40 | RESET = 49 41 | LIGHTBLACK = 100 42 | LIGHTRED = 101 43 | LIGHTGREEN = 102 44 | LIGHTYELLOW = 103 45 | LIGHTBLUE = 104 46 | LIGHTMAGENTA = 105 47 | LIGHTCYAN = 106 48 | LIGHTWHITE = 107 49 | 50 | 51 | class Style: 52 | BRIGHT = 1 53 | DIM = 2 54 | NORMAL = 22 55 | -------------------------------------------------------------------------------- /serial_terminal/terminal/escape_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Parse escape sequences and map them to calls to an emulator. 5 | # 6 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 7 | # (C) 2018 Chris Liechti 8 | # 9 | # SPDX-License-Identifier: BSD-3-Clause 10 | 11 | # C0 control codes 12 | NUL = b'\0' 13 | SOH = b'\x01' 14 | STX = b'\x02' 15 | ETX = b'\x03' 16 | EOT = b'\x04' 17 | ENQ = b'\x05' 18 | ACK = b'\x06' 19 | BEL = b'\x07' 20 | BS = b'\x08' 21 | HT = b'\x09' 22 | LF = b'\x0A' 23 | VT = b'\x0B' 24 | FF = b'\x0C' 25 | CR = b'\x0D' 26 | SO = b'\x0E' 27 | SI = b'\x0F' 28 | DLE = b'\x10' 29 | DC1 = b'\x11' 30 | DC2 = b'\x12' 31 | DC3 = b'\x13' 32 | DC4 = b'\x14' 33 | NAK = b'\x15' 34 | SYN = b'\x16' 35 | ETB = b'\x17' 36 | CAN = b'\x18' 37 | EM = b'\x19' 38 | SUB = b'\x1A' 39 | ESC = b'\x1B' 40 | FS = b'\x1C' 41 | GS = b'\x1D' 42 | RS = b'\x1E' 43 | US = b'\x1F' 44 | DEL = b'\x7F' 45 | 46 | # C1 control codes 47 | PAD = b'\x80' 48 | HOP = b'\x81' 49 | BPH = b'\x82' 50 | NBH = b'\x83' 51 | IND = b'\x84' 52 | NEL = b'\x85' 53 | SSA = b'\x86' 54 | ESA = b'\x87' 55 | HTS = b'\x88' 56 | HTJ = b'\x89' 57 | VTS = b'\x8A' 58 | PLD = b'\x8B' 59 | PLU = b'\x8C' 60 | RI = b'\x8D' 61 | SS2 = b'\x8E' 62 | SS3 = b'\x8F' 63 | DCS = b'\x90' 64 | PU1 = b'\x91' 65 | PU2 = b'\x92' 66 | STS = b'\x93' 67 | CCH = b'\x94' 68 | MW = b'\x95' 69 | SPA = b'\x96' 70 | EPA = b'\x97' 71 | SOS = b'\x98' 72 | SGCI = b'\x99' 73 | SCI = b'\x9A' 74 | CSI = b'\x9B' 75 | ST = b'\x9C' 76 | OSC = b'\x9D' 77 | PM = b'\x9E' 78 | APC = b'\x9F' 79 | 80 | 81 | class EscapeDecoder: 82 | def __init__(self, terminal_code_handler): 83 | self.terminal_code_handler = terminal_code_handler 84 | self._d_parameter = [0] * 3 85 | self._d_parameter_index = 0 86 | self._extra_flag = False 87 | self._value = None 88 | self._handler = self.handle_c0 89 | self.eightbit_controls = False 90 | 91 | def _reset_parameters(self): 92 | self._d_parameter[0] = 0 93 | self._d_parameter_index = 0 94 | self._extra_flag = False 95 | 96 | def handle(self, character): 97 | # h = self._handler 98 | self._handler(character) 99 | # print(h.__name__, repr(character), self._handler.__name__) # XXX 100 | 101 | def handle_c0(self, character): 102 | if character == NUL: 103 | pass 104 | elif character == ENQ: 105 | self.terminal_code_handler.enquiry() 106 | elif character == BEL: 107 | self.terminal_code_handler.bell() 108 | elif character == BS: 109 | self.terminal_code_handler.backspace() 110 | elif character == HT: 111 | self.terminal_code_handler.horizontal_tab() 112 | elif character == LF: 113 | self.terminal_code_handler.line_feed() 114 | elif character == VT: 115 | self.terminal_code_handler.vertical_tabulation() 116 | elif character == FF: 117 | self.terminal_code_handler.form_feed() 118 | elif character == CR: 119 | self.terminal_code_handler.carriage_return() 120 | elif character == SO: 121 | self.terminal_code_handler.shift_out() 122 | elif character == SI: 123 | self.terminal_code_handler.shift_in() 124 | elif character == DC1: 125 | self.terminal_code_handler.device_control_1() 126 | elif character == DC2: 127 | self.terminal_code_handler.device_control_2() 128 | elif character == DC3: 129 | self.terminal_code_handler.device_control_3() 130 | elif character == DC4: 131 | self.terminal_code_handler.device_control_4() 132 | elif character == CAN: 133 | self.terminal_code_handler.cancel() 134 | elif character == SUB: 135 | self.terminal_code_handler.substitute() 136 | elif character == ESC: 137 | #~ self.terminal_code_handler.escape() 138 | self._reset_parameters() 139 | self._handler = self.handle_esc 140 | elif character == DEL: 141 | self.terminal_code_handler.delete() 142 | elif self.eightbit_controls: 143 | if character == IND: 144 | self.terminal_code_handler.index() 145 | elif character == NEL: 146 | self.terminal_code_handler.next_line() 147 | elif character == HTS: 148 | self.terminal_code_handler.horizontal_tab_set() 149 | elif character == RI: 150 | self.terminal_code_handler.reverse_index() 151 | elif character == SS2: 152 | self.terminal_code_handler.single_shift_G2() 153 | elif character == SS3: 154 | self.terminal_code_handler.single_shift_G3() 155 | elif character == DCS: 156 | self.terminal_code_handler.device_control_string() 157 | elif character == CSI: 158 | self._reset_parameters() 159 | self._handler = self.handle_csi 160 | elif character == ST: 161 | self.terminal_code_handler.string_terminator() 162 | else: 163 | self.terminal_code_handler.write(character) 164 | else: 165 | self.terminal_code_handler.write(character) 166 | 167 | #~ def self.handle_vt52_line(self, character): 168 | #~ self._value = ord(character) - 32 169 | #~ self._handler = self.handle_vt52_column 170 | #~ 171 | #~ def handle_vt52_column(self, character): 172 | #~ self.terminal_code_handler.cursor_position(self._value, ord(character) - 32) 173 | #~ self._handler = self.handle_c0 174 | 175 | def handle_esc(self, character): 176 | self._handler = self.handle_c0 177 | if character == b'D': # IND 178 | self.terminal_code_handler.index() 179 | elif character == b'E': # NEL 180 | self.terminal_code_handler.next_line() 181 | elif character == b'H': # HTS 182 | self.terminal_code_handler.horizontal_tab_set() 183 | elif character == b'M': # RI 184 | self.terminal_code_handler.reverse_index() 185 | elif character == b'N': # SS2 186 | self.terminal_code_handler.single_shift_G2() 187 | elif character == b'O': # SS3 188 | self.terminal_code_handler.single_shift_G3() 189 | elif character == b'P': # DCS 190 | self.terminal_code_handler.device_control_string() 191 | elif character == b'[': # CSI 192 | self._handler = self.handle_csi 193 | elif character == b'\\': # ST 194 | self.terminal_code_handler.string_terminator() 195 | elif character == b']': # OSC 196 | self.terminal_code_handler.operating_system_command() 197 | elif character == b'_': # APC 198 | self.terminal_code_handler.application_program_command() 199 | elif character == b'^': # PM 200 | self.terminal_code_handler.privacy_message() 201 | #~ elif character == b'~': # LS1R 202 | #~ elif character == b'n': # LS2 203 | #~ elif character == b'}': # LS2R 204 | #~ elif character == b'o': # LS3 205 | #~ elif character == b'|': # LS3R 206 | elif character == b' ': # S7C1T / S8C1T # XXX ignore in VT100/VT52 modes 207 | self._handler = self.handle_select_control_transmission 208 | elif character == b'#': # line attributes 209 | self._handler = self.handle_line_attributes 210 | elif character in b'()*+': 211 | self._value = ord(character) 212 | self._handler = self.handle_character_set_selection 213 | elif character == b'=': 214 | self.terminal_code_handler.keypad_as_application() 215 | elif character == b'>': 216 | self.terminal_code_handler.keypad_as_numeric() 217 | elif character == b'7': 218 | self.terminal_code_handler.save_cursor() 219 | elif character == b'8': 220 | self.terminal_code_handler.restore_cursor() 221 | #~ elif character == b'A': 222 | #~ self.terminal_code_handler.cursor_up() 223 | #~ elif character == b'B': 224 | #~ self.terminal_code_handler.cursor_down() 225 | #~ elif character == b'C': 226 | #~ self.terminal_code_handler.cursor_forward() 227 | #~ elif character == b'D': 228 | #~ self.terminal_code_handler.cursor_backward() 229 | #~ elif character == b'F': 230 | #~ self.terminal_code_handler.graphics_mode(True) 231 | #~ elif character == b'G': 232 | #~ self.terminal_code_handler.graphics_mode(False) 233 | #~ elif character == b'H': 234 | #~ self.terminal_code_handler.cursor_position(0, 0) 235 | #~ elif character == b'I': 236 | #~ self.terminal_code_handler.reverse_line_feed() 237 | #~ elif character == b'J': 238 | #~ self.terminal_code_handler.erase_display(0) 239 | #~ elif character == b'K': 240 | #~ self.terminal_code_handler.erase_in_line(0) 241 | #~ elif character == b'Y': 242 | #~ self._handler = self.handle_vt52_line 243 | #~ elif character == b'Z': 244 | #~ self.terminal_code_handler.identify() 245 | #~ elif character == b'<': 246 | #~ self.terminal_code_handler.ansi_mode() 247 | #~ elif character == b']': 248 | #~ self.terminal_code_handler.print_screen() 249 | #~ elif character == b'V': 250 | #~ self.terminal_code_handler.print_cursor_line() 251 | # left out ' '/'_' autoprint and 'W'/'X' printer control mode 252 | elif character == ESC: 253 | self.terminal_code_handler.write(character) 254 | self._handler = self.handle_c0 255 | else: 256 | self._handler = self.handle_c0 257 | 258 | def handle_select_control_transmission(self, character): 259 | if character == b'F': 260 | self.terminal_code_handler.convert_c1_codes(True) 261 | elif character == b'G': 262 | self.terminal_code_handler.convert_c1_codes(False) 263 | self._handler = self.handle_c0 264 | 265 | def handle_line_attributes(self, character): 266 | # XXX 267 | self._handler = self.handle_c0 268 | 269 | def handle_character_set_selection(self, character): 270 | #~ self._value # select group 271 | # XXX 272 | self._handler = self.handle_c0 273 | 274 | def handle_csi(self, character): 275 | if character == b'h': 276 | self._handler = self.handle_c0 277 | self.terminal_code_handler.handle_flag(self._d_parameter[0], self._extra_flag, True) 278 | elif character == b'l': 279 | self._handler = self.handle_c0 280 | self.terminal_code_handler.handle_flag(self._d_parameter[0], self._extra_flag, False) 281 | elif character == b'A': 282 | self._handler = self.handle_c0 283 | self.terminal_code_handler.cursor_up(self._d_parameter[0]) 284 | elif character == b'B': 285 | self._handler = self.handle_c0 286 | self.terminal_code_handler.cursor_down(self._d_parameter[0]) 287 | elif character == b'C': 288 | self._handler = self.handle_c0 289 | self.terminal_code_handler.cursor_forward(self._d_parameter[0]) 290 | elif character == b'D': 291 | self._handler = self.handle_c0 292 | self.terminal_code_handler.cursor_backward(self._d_parameter[0]) 293 | elif character == b'H': 294 | self._handler = self.handle_c0 295 | self.terminal_code_handler.cursor_position(self._d_parameter[0], self._d_parameter[1]) 296 | elif character == b'L': # IL 297 | self._handler = self.handle_c0 298 | self.terminal_code_handler.insert_line(self._d_parameter[0]) 299 | elif character == b'M': # DL 300 | self._handler = self.handle_c0 301 | self.terminal_code_handler.delete_line(self._d_parameter[0]) 302 | elif character == b'@': # ICH 303 | self._handler = self.handle_c0 304 | self.terminal_code_handler.insert_character(self._d_parameter[0]) 305 | elif character == b'P': # DCH 306 | self._handler = self.handle_c0 307 | self.terminal_code_handler.delete_character(self._d_parameter[0]) 308 | elif character == b'X': # ECH 309 | self._handler = self.handle_c0 310 | self.terminal_code_handler.erase_character(self._d_parameter[0]) 311 | elif character == b'K': # EL 312 | self._handler = self.handle_c0 313 | self.terminal_code_handler.erase_in_line(self._d_parameter[0], selective=self._extra_flag) 314 | elif character == b'J': # ED 315 | self._handler = self.handle_c0 316 | self.terminal_code_handler.erase_display(self._d_parameter[0], selective=self._extra_flag) 317 | elif character == b'f': 318 | self._handler = self.handle_c0 319 | self.terminal_code_handler.cursor_position(self._d_parameter[0], self._d_parameter[1]) 320 | elif character == b'g': 321 | self._handler = self.handle_c0 322 | self.terminal_code_handler.clear_tabulation(self._d_parameter[0]) 323 | elif character == b'm': 324 | self.terminal_code_handler.select_graphic_rendition(self._d_parameter[:self._d_parameter_index + 1]) 325 | self._handler = self.handle_c0 326 | elif character == b'r': # DECSTBM 327 | self.terminal_code_handler.set_scroll_region_margins(self._d_parameter[0], self._d_parameter[1]) 328 | self._handler = self.handle_c0 329 | #~ elif character == b'"': # sel character attribs, followed by b'q' 330 | #~ elif character == b'i': # printing 331 | elif character == b';': # parameter separator 332 | if self._d_parameter_index < len(self._d_parameter): 333 | self._d_parameter_index += 1 334 | self._d_parameter[self._d_parameter_index] = 0 335 | elif character == b'?': # ANSI private extensions marker 336 | self._extra_flag = True 337 | elif character in b'0123456789': # parse individual parameter (numbers) 338 | self._d_parameter[self._d_parameter_index] *= 10 339 | self._d_parameter[self._d_parameter_index] += ord(character) - ord('0') 340 | 341 | # unknown CSI sequences are ignored 342 | else: 343 | self._handler = self.handle_c0 344 | -------------------------------------------------------------------------------- /serial_terminal/terminal/escape_encoder.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python 3 | # encoding: utf-8 4 | # 5 | # Generate escape sequences. 6 | # 7 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 8 | # (C) 2018 Chris Liechti 9 | # 10 | # SPDX-License-Identifier: BSD-3-Clause 11 | 12 | 13 | class EscapeEncoder: 14 | def __init__(self, output_stream=None): 15 | self.output_stream = output_stream 16 | self.keymap = { 17 | 'Ctrl+Space': '\x00', 18 | 'Ctrl+A': '\x01', 19 | 'Ctrl+B': '\x02', 20 | 'Ctrl+C': '\x03', 21 | 'Ctrl+D': '\x04', 22 | 'Ctrl+E': '\x05', 23 | 'Ctrl+F': '\x06', 24 | 'Ctrl+G': '\x07', 25 | 'Ctrl+H': '\x08', 26 | 'Ctrl+I': '\x09', 27 | 'Ctrl+J': '\x0a', 28 | 'Ctrl+K': '\x0b', 29 | 'Ctrl+L': '\x0c', 30 | 'Ctrl+M': '\x0d', 31 | 'Ctrl+N': '\x0e', 32 | 'Ctrl+O': '\x0f', 33 | 'Ctrl+P': '\x10', 34 | 'Ctrl+Q': '\x11', 35 | 'Ctrl+R': '\x12', 36 | 'Ctrl+S': '\x13', 37 | 'Ctrl+T': '\x14', 38 | 'Ctrl+U': '\x15', 39 | 'Ctrl+V': '\x16', 40 | 'Ctrl+W': '\x17', 41 | 'Ctrl+X': '\x18', 42 | 'Ctrl+Y': '\x19', 43 | 'Ctrl+Z': '\x1a', 44 | 'Ctrl+[': '\x1b', 45 | 'Ctrl+\\': '\x1c', 46 | 'Ctrl+]': '\x1d', 47 | 'Ctrl+^': '\x1e', 48 | 'Ctrl+_': '\x1f', 49 | 'Tab': '\x09', 50 | 'Esc': '\x1d', 51 | 'PF1': '\x1bOP', 52 | 'PF2': '\x1bOQ', 53 | 'PF3': '\x1bOR', 54 | 'PF4': '\x1bOS', 55 | 'F1': '\x1b[11~', 56 | 'F2': '\x1b[12~', 57 | 'F3': '\x1b[13~', 58 | 'F4': '\x1b[14~', 59 | 'F5': '\x1b[15~', 60 | 'F6': '\x1b[17~', 61 | 'F7': '\x1b[18~', 62 | 'F8': '\x1b[19~', 63 | 'F9': '\x1b[20~', 64 | 'F10': '\x1b[21~', 65 | 'F11': '\x1b[23~', 66 | 'F12': '\x1b[24~', 67 | 'F13': '\x1b[25~', 68 | 'F14': '\x1b[26~', 69 | 'F15': '\x1b[28~', 70 | 'F16': '\x1b[29~', 71 | 'F17': '\x1b[31~', 72 | 'F18': '\x1b[32~', 73 | 'F19': '\x1b[33~', 74 | 'F20': '\x1b[34~', 75 | 'Up': '\x1b[A', 76 | 'Down': '\x1b[B', 77 | 'Left': '\x1b[D', 78 | 'Right': '\x1b[C', 79 | 'Home': '\x1b[H', 80 | 'End': '\x1b[F', 81 | 'Find': '\x1b[1~', 82 | 'Insert': '\x1b[2~', 83 | 'Delete': '\x1b[3~', 84 | 'Select': '\x1b[4~', 85 | 'Page Up': '\x1b[5~', 86 | 'Page Down': '\x1b[6~', 87 | 'Num Lock': '\x1bOP', 88 | 'KP_Divide': '\x1bOQ', 89 | 'KP_Multiply': '\x1bOR', 90 | 'KP_Minus': '\x1bOS', 91 | 'Caps Lock': '\x1bOm', 92 | 'KP_Plus': '\x1bOl', 93 | 'KP_Dot': '\x1bOn', 94 | 'KP_Enter': '\x1bOM', 95 | 'KP_0': '\x1bOp', 96 | 'KP_1': '\x1bOq', 97 | 'KP_2': '\x1bOr', 98 | 'KP_3': '\x1bOs', 99 | 'KP_4': '\x1bOt', 100 | 'KP_5': '\x1bOu', 101 | 'KP_6': '\x1bOv', 102 | 'KP_7': '\x1bOw', 103 | 'KP_8': '\x1bOx', 104 | 'KP_9': '\x1bOy', 105 | } 106 | 107 | def translate_named_key(self, key_name): 108 | return self.keymap[key_name] 109 | 110 | def send_named_key(self, key_name): 111 | self.output_stream.write(self.translate_named_key(key_name)) 112 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [flake8] 5 | max-line-length = 120 6 | ignore = E265, E126, E241 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py for pySerial-terminal 2 | # 3 | # Direct install (all systems): 4 | # "python setup.py install" 5 | # 6 | # For Python 3.x use the corresponding Python executable, 7 | # e.g. "python3 setup.py ..." 8 | # 9 | # (C) 2018 Chris Liechti 10 | # 11 | # SPDX-License-Identifier: BSD-3-Clause 12 | import io 13 | import os 14 | import re 15 | 16 | try: 17 | from setuptools import setup 18 | except ImportError: 19 | from distutils.core import setup 20 | 21 | 22 | def read(*names, **kwargs): 23 | """Python 2 and Python 3 compatible text file reading. 24 | 25 | Required for single-sourcing the version string. 26 | """ 27 | with io.open( 28 | os.path.join(os.path.dirname(__file__), *names), 29 | encoding=kwargs.get("encoding", "utf8") 30 | ) as fp: 31 | return fp.read() 32 | 33 | 34 | def find_version(*file_paths): 35 | """ 36 | Search the file for a version string. 37 | 38 | file_path contain string path components. 39 | 40 | Reads the supplied Python module as text without importing it. 41 | """ 42 | version_file = read(*file_paths) 43 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 44 | version_file, re.M) 45 | if version_match: 46 | return version_match.group(1) 47 | raise RuntimeError("Unable to find version string.") 48 | 49 | 50 | version = find_version('serial_terminal', '__init__.py') 51 | 52 | 53 | setup( 54 | name="pyserial-terminal", 55 | description="Python Serial Terminal", 56 | version=version, 57 | author="Chris Liechti", 58 | author_email="cliechti@gmx.net", 59 | url="https://github.com/pyserial/pyserial-terminal", 60 | packages=['serial_terminal'], 61 | license="BSD", 62 | #~ long_description="" 63 | classifiers=[ 64 | # 'Development Status :: 5 - Production/Stable', 65 | 'Intended Audience :: Developers', 66 | 'Intended Audience :: End Users/Desktop', 67 | 'License :: OSI Approved :: BSD License', 68 | 'Natural Language :: English', 69 | 'Operating System :: POSIX', 70 | 'Operating System :: Microsoft :: Windows', 71 | 'Operating System :: MacOS :: MacOS X', 72 | 'Programming Language :: Python', 73 | 'Programming Language :: Python :: 2', 74 | 'Programming Language :: Python :: 2.7', 75 | 'Programming Language :: Python :: 3', 76 | 'Programming Language :: Python :: 3.5', 77 | 'Programming Language :: Python :: 3.6', 78 | 'Programming Language :: Python :: 3.7', 79 | 'Topic :: Communications', 80 | 'Topic :: Software Development :: Libraries', 81 | 'Topic :: Software Development :: Libraries :: Python Modules', 82 | 'Topic :: Terminals :: Serial', 83 | ], 84 | platforms='any', 85 | #~ scripts=['serial_terminal/terminal.py'], 86 | ) 87 | -------------------------------------------------------------------------------- /test/terminal_wrapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Test support to run external tools and use our terminal emulation. 4 | # 5 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 6 | # (C)2002-2018 Chris Liechti 7 | # 8 | # SPDX-License-Identifier: BSD-3-Clause 9 | 10 | import subprocess 11 | import sys 12 | import pathlib 13 | 14 | sys.path.append(str(pathlib.Path(__file__).parent.parent)) 15 | from serial_terminal.console import Console 16 | from serial_terminal.terminal.escape_decoder import EscapeDecoder 17 | from serial_terminal.emulation.simple import SimpleTerminal 18 | 19 | import serial 20 | 21 | 22 | def main(): 23 | terminal = SimpleTerminal(Console()) 24 | decoder = EscapeDecoder(terminal) 25 | p = subprocess.Popen([sys.executable] + sys.argv[1:], stdout=subprocess.PIPE) 26 | # p.stdin.close() 27 | while True: 28 | data = p.stdout.read(4096) 29 | if not data: 30 | break 31 | for byte in serial.iterbytes(data): 32 | decoder.handle(byte) 33 | # sys.stdout.buffer.write(b'\n') 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /test/test_colors.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | for fg in range(30, 38): 4 | sys.stdout.write('\x1b[{0}m {0} \x1b[0m'.format(fg)) 5 | for bg in range(40, 48): 6 | sys.stdout.write('\x1b[{0}m {0} \x1b[0m'.format(bg)) 7 | 8 | sys.stdout.write('\n') 9 | 10 | -------------------------------------------------------------------------------- /test/tk_console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Python 3+ only tkinter terminal widget wrapper: show the output of a 4 | # console program in the widget. 5 | # 6 | # This file is part of pySerial-terminal. https://github.com/pyserial/pyserial-terminal 7 | # (C) 2018 Chris Liechti 8 | # 9 | # SPDX-License-Identifier: BSD-3-Clause 10 | import pathlib 11 | import subprocess 12 | import sys 13 | 14 | import tkinter as tk 15 | from tkinter import ttk 16 | 17 | sys.path.append(str(pathlib.Path(__file__).parent.parent)) 18 | from serial_terminal.console import tk_widget 19 | from serial_terminal.terminal.escape_decoder import EscapeDecoder 20 | from serial_terminal.emulation.simple import SimpleTerminal 21 | 22 | import serial 23 | 24 | def main(): 25 | root = tk.Tk() 26 | root.title('pySerial-Terminal tk_widget test') 27 | console = tk_widget.Console(root) 28 | # console.pack(side=tk.TOP, fill=tk.BOTH, expand=True) 29 | console.pack() 30 | terminal = SimpleTerminal(console) 31 | decoder = EscapeDecoder(terminal) 32 | 33 | p = subprocess.Popen([sys.executable] + sys.argv[1:], stdout=subprocess.PIPE) 34 | # p.stdin.close() 35 | while True: 36 | data = p.stdout.read(4096) 37 | if not data: 38 | break 39 | for byte in serial.iterbytes(data): 40 | decoder.handle(byte) 41 | root.update_idletasks() 42 | root.update() 43 | 44 | root.mainloop() 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /test/vt220_colors.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | import sys 3 | 4 | def main(): 5 | sys.stdout.write(' ') 6 | for background in [''] + ['{}'.format(b) for b in range(40, 48)] + ['{}'.format(b) for b in range(100, 108)]: 7 | sys.stdout.write('{:>4}m '.format(background)) 8 | sys.stdout.write('\n') 9 | for foreground in [''] + ['{}'.format(f) for f in range(30, 38)] + ['{}'.format(f) for f in range(90, 98)]: 10 | for intensity in ('', '1'): 11 | sys.stdout.write('{:>4}m '.format(';'.join(c for c in (intensity, foreground) if c))) 12 | for background in [''] + ['{}'.format(b) for b in range(40, 48)] + ['{}'.format(b) for b in range(100, 108)]: 13 | color = ';'.join(c for c in (intensity, foreground, background) if c) 14 | sys.stdout.write('\x1b[{}m ABC \x1b[0m '.format(color)) 15 | sys.stdout.write('\n') 16 | sys.stdout.write('\x1b[0m') 17 | 18 | 19 | if __name__ == '__main__': 20 | main() 21 | --------------------------------------------------------------------------------