cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
cliechti | a128a70 | 2004-07-21 22:13:31 +0000 | [diff] [blame] | 3 | # Very simple serial terminal |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 4 | # (C)2002-2006 Chris Liechti <cliechti@gmx.net> |
cliechti | fc9eb38 | 2002-03-05 01:12:29 +0000 | [diff] [blame] | 5 | |
cliechti | a128a70 | 2004-07-21 22:13:31 +0000 | [diff] [blame] | 6 | # Input characters are sent directly (only LF -> CR/LF/CRLF translation is |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 7 | # done), received characters are displayed as is (or escaped trough pythons |
cliechti | a128a70 | 2004-07-21 22:13:31 +0000 | [diff] [blame] | 8 | # repr, useful for debug purposes) |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 9 | |
| 10 | |
cliechti | 8802802 | 2007-11-13 16:06:46 +0000 | [diff] [blame^] | 11 | import sys, os, serial, threading |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 12 | |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 13 | EXITCHARCTER = '\x1d' #GS/ctrl+] |
cliechti | a128a70 | 2004-07-21 22:13:31 +0000 | [diff] [blame] | 14 | |
| 15 | #first choose a platform dependant way to read single characters from the console |
cliechti | fc9eb38 | 2002-03-05 01:12:29 +0000 | [diff] [blame] | 16 | if os.name == 'nt': |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 17 | import msvcrt |
| 18 | def getkey(): |
| 19 | while 1: |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 20 | z = msvcrt.getch() |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 21 | if z == '\0' or z == '\xe0': #functions keys |
| 22 | msvcrt.getch() |
| 23 | else: |
cliechti | 7672d61 | 2004-07-21 19:52:33 +0000 | [diff] [blame] | 24 | if z == '\r': |
| 25 | return '\n' |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 26 | return z |
| 27 | |
| 28 | elif os.name == 'posix': |
cliechti | b7466da | 2004-07-11 20:04:41 +0000 | [diff] [blame] | 29 | import termios, sys, os |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 30 | fd = sys.stdin.fileno() |
| 31 | old = termios.tcgetattr(fd) |
| 32 | new = termios.tcgetattr(fd) |
cliechti | b7466da | 2004-07-11 20:04:41 +0000 | [diff] [blame] | 33 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO |
| 34 | new[6][termios.VMIN] = 1 |
| 35 | new[6][termios.VTIME] = 0 |
| 36 | termios.tcsetattr(fd, termios.TCSANOW, new) |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 37 | def getkey(): |
cliechti | b7466da | 2004-07-11 20:04:41 +0000 | [diff] [blame] | 38 | c = os.read(fd, 1) |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 39 | return c |
| 40 | def clenaup_console(): |
cliechti | b7466da | 2004-07-11 20:04:41 +0000 | [diff] [blame] | 41 | termios.tcsetattr(fd, termios.TCSAFLUSH, old) |
| 42 | sys.exitfunc = clenaup_console #terminal modes have to be restored on exit... |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 43 | |
| 44 | else: |
| 45 | raise "Sorry no implementation for your platform (%s) available." % sys.platform |
| 46 | |
cliechti | a128a70 | 2004-07-21 22:13:31 +0000 | [diff] [blame] | 47 | CONVERT_CRLF = 2 |
| 48 | CONVERT_CR = 1 |
| 49 | CONVERT_LF = 0 |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 50 | NEWLINE_CONVERISON_MAP = ('\n', '\r', '\r\n') |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 51 | |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 52 | class Miniterm: |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 53 | def __init__(self, port, baudrate, parity, rtscts, xonxoff, echo=False, convert_outgoing=CONVERT_CRLF, repr_mode=0): |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 54 | self.serial = serial.Serial(port, baudrate, parity=parity, rtscts=rtscts, xonxoff=xonxoff, timeout=0.7) |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 55 | self.echo = echo |
| 56 | self.repr_mode = repr_mode |
| 57 | self.convert_outgoing = convert_outgoing |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 58 | self.newline = NEWLINE_CONVERISON_MAP[self.convert_outgoing] |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 59 | |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 60 | def start(self): |
| 61 | self.alive = True |
| 62 | #start serial->console thread |
| 63 | self.receiver_thread = threading.Thread(target=self.reader) |
| 64 | self.receiver_thread.setDaemon(1) |
| 65 | self.receiver_thread.start() |
| 66 | #enter console->serial loop |
| 67 | self.transmitter_thread = threading.Thread(target=self.writer) |
| 68 | self.transmitter_thread.setDaemon(1) |
| 69 | self.transmitter_thread.start() |
| 70 | |
| 71 | def stop(self): |
| 72 | self.alive = False |
| 73 | |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 74 | def join(self, transmit_only=False): |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 75 | self.transmitter_thread.join() |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 76 | if not transmit_only: |
| 77 | self.receiver_thread.join() |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 78 | |
| 79 | def reader(self): |
| 80 | """loop and copy serial->console""" |
| 81 | while self.alive: |
| 82 | data = self.serial.read(1) |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 83 | |
| 84 | if self.repr_mode == 0: |
| 85 | # direct output, just have to care about newline setting |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 86 | if data == '\r' and self.convert_outgoing == CONVERT_CR: |
| 87 | sys.stdout.write('\n') |
| 88 | else: |
| 89 | sys.stdout.write(data) |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 90 | elif self.repr_mode == 1: |
| 91 | # escape non-printable, let pass newlines |
| 92 | if self.convert_outgoing == CONVERT_CRLF and data in '\r\n': |
| 93 | if data == '\n': |
| 94 | sys.stdout.write('\n') |
| 95 | elif data == '\r': |
| 96 | pass |
| 97 | elif data == '\n' and self.convert_outgoing == CONVERT_LF: |
| 98 | sys.stdout.write('\n') |
| 99 | elif data == '\r' and self.convert_outgoing == CONVERT_CR: |
| 100 | sys.stdout.write('\n') |
| 101 | else: |
| 102 | sys.stdout.write(repr(data)[1:-1]) |
| 103 | elif self.repr_mode == 2: |
| 104 | # escape all non-printable, including newline |
| 105 | sys.stdout.write(repr(data)[1:-1]) |
| 106 | elif self.repr_mode == 3: |
| 107 | # escape everything (hexdump) |
| 108 | for character in data: |
| 109 | sys.stdout.write("%s " % character.encode('hex')) |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 110 | sys.stdout.flush() |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 111 | |
cliechti | 576de25 | 2002-02-28 23:54:44 +0000 | [diff] [blame] | 112 | |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 113 | def writer(self): |
| 114 | """loop and copy console->serial until EXITCHARCTER character is found""" |
| 115 | while self.alive: |
| 116 | try: |
| 117 | c = getkey() |
| 118 | except KeyboardInterrupt: |
| 119 | c = '\x03' |
| 120 | if c == EXITCHARCTER: |
| 121 | self.stop() |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 122 | break # exit app |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 123 | elif c == '\n': |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 124 | self.serial.write(self.newline) # send newline character(s) |
| 125 | if self.echo: |
| 126 | sys.stdout.write(c) #local echo is a real newline in any case |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 127 | else: |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 128 | self.serial.write(c) # send character |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 129 | if self.echo: |
| 130 | sys.stdout.write(c) |
cliechti | 8b3ad39 | 2002-03-03 20:12:21 +0000 | [diff] [blame] | 131 | |
cliechti | 8b3ad39 | 2002-03-03 20:12:21 +0000 | [diff] [blame] | 132 | |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 133 | |
| 134 | def main(): |
| 135 | import optparse |
| 136 | |
| 137 | parser = optparse.OptionParser(usage="""\ |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 138 | %prog [options] [port [baudrate]] |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 139 | |
| 140 | Miniterm - A simple terminal program for the serial port.""") |
| 141 | |
| 142 | parser.add_option("-p", "--port", dest="port", |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 143 | help="port, a number (default 0) or a device name (deprecated option)", |
| 144 | default=None) |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 145 | |
| 146 | parser.add_option("-b", "--baud", dest="baudrate", action="store", type='int', |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 147 | help="set baudrate, default 9600", default=9600) |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 148 | |
| 149 | parser.add_option("", "--parity", dest="parity", action="store", |
| 150 | help="set parity, one of [N, E, O], default=N", default='N') |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 151 | |
| 152 | parser.add_option("-e", "--echo", dest="echo", action="store_true", |
| 153 | help="enable local echo (default off)", default=False) |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 154 | |
| 155 | parser.add_option("", "--rtscts", dest="rtscts", action="store_true", |
| 156 | help="enable RTS/CTS flow control (default off)", default=False) |
| 157 | |
| 158 | parser.add_option("", "--xonxoff", dest="xonxoff", action="store_true", |
| 159 | help="enable software flow control (default off)", default=False) |
| 160 | |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 161 | parser.add_option("", "--cr", dest="cr", action="store_true", |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 162 | help="do not send CR+LF, send CR only", default=False) |
| 163 | |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 164 | parser.add_option("", "--lf", dest="lf", action="store_true", |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 165 | help="do not send CR+LF, send LF only", default=False) |
| 166 | |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 167 | parser.add_option("-D", "--debug", dest="repr_mode", action="count", |
| 168 | help="""debug received data (escape non-printable chars) |
| 169 | --debug can be given multiple times: |
| 170 | 0: just print what is received |
| 171 | 1: escape non-printable characters, do newlines as ususal |
| 172 | 2: escape non-printable characters, newlines too |
| 173 | 3: hex dump everything""", default=0) |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 174 | |
cliechti | b7d746d | 2006-03-28 22:44:30 +0000 | [diff] [blame] | 175 | parser.add_option("", "--rts", dest="rts_state", action="store", type='int', |
| 176 | help="set initial RTS line state (possible values: 0, 1)", default=None) |
| 177 | |
| 178 | parser.add_option("", "--dtr", dest="dtr_state", action="store", type='int', |
| 179 | help="set initial DTR line state (possible values: 0, 1)", default=None) |
| 180 | |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 181 | parser.add_option("-q", "--quiet", dest="quiet", action="store_true", |
| 182 | help="suppress non error messages", default=False) |
| 183 | |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 184 | |
| 185 | (options, args) = parser.parse_args() |
| 186 | |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 187 | if options.cr and options.lf: |
| 188 | parser.error("ony one of --cr or --lf can be specified") |
| 189 | |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 190 | port = options.port |
| 191 | baudrate = options.baudrate |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 192 | if args: |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 193 | if options.port is not None: |
| 194 | parser.error("no arguments are allowed, options only when --port is given") |
| 195 | port = args.pop(0) |
| 196 | if args: |
| 197 | try: |
| 198 | baudrate = int(args[0]) |
| 199 | except ValueError: |
| 200 | parser.error("baudrate must be a number, not %r" % args[0]) |
| 201 | args.pop(0) |
| 202 | if args: |
| 203 | parser.error("too many arguments") |
| 204 | else: |
| 205 | if port is None: port = 0 |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 206 | |
| 207 | convert_outgoing = CONVERT_CRLF |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 208 | if options.cr: |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 209 | convert_outgoing = CONVERT_CR |
cliechti | 9cf2622 | 2006-03-24 20:09:21 +0000 | [diff] [blame] | 210 | elif options.lf: |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 211 | convert_outgoing = CONVERT_LF |
| 212 | |
| 213 | try: |
| 214 | miniterm = Miniterm( |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 215 | port, |
| 216 | baudrate, |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 217 | options.parity, |
| 218 | rtscts=options.rtscts, |
| 219 | xonxoff=options.xonxoff, |
| 220 | echo=options.echo, |
| 221 | convert_outgoing=convert_outgoing, |
| 222 | repr_mode=options.repr_mode, |
| 223 | ) |
| 224 | except serial.SerialException: |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 225 | sys.stderr.write("could not open port %r" % port) |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 226 | sys.exit(1) |
| 227 | |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 228 | if not options.quiet: |
cliechti | 9743e22 | 2006-04-04 21:48:56 +0000 | [diff] [blame] | 229 | sys.stderr.write('--- Miniterm on %s: %d,%s,%s,%s. Type Ctrl-] to quit. ---\n' % ( |
| 230 | miniterm.serial.portstr, |
| 231 | miniterm.serial.baudrate, |
| 232 | miniterm.serial.bytesize, |
| 233 | miniterm.serial.parity, |
| 234 | miniterm.serial.stopbits, |
| 235 | )) |
cliechti | b7d746d | 2006-03-28 22:44:30 +0000 | [diff] [blame] | 236 | if options.dtr_state is not None: |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 237 | if not options.quiet: |
| 238 | sys.stderr.write('--- forcing DTR %s\n' % (options.dtr_state and 'active' or 'inactive')) |
cliechti | b7d746d | 2006-03-28 22:44:30 +0000 | [diff] [blame] | 239 | miniterm.serial.setDTR(options.dtr_state) |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 240 | if options.rts_state is not None: |
| 241 | if not options.quiet: |
| 242 | sys.stderr.write('--- forcing RTS %s\n' % (options.rts_state and 'active' or 'inactive')) |
| 243 | miniterm.serial.setRTS(options.rts_state) |
cliechti | b7d746d | 2006-03-28 22:44:30 +0000 | [diff] [blame] | 244 | |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 245 | miniterm.start() |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 246 | miniterm.join(True) |
| 247 | if not options.quiet: |
| 248 | sys.stderr.write("\n--- exit ---\n") |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 249 | miniterm.join() |
cliechti | bf6bb7d | 2006-03-30 00:28:18 +0000 | [diff] [blame] | 250 | |
cliechti | 8b3ad39 | 2002-03-03 20:12:21 +0000 | [diff] [blame] | 251 | |
| 252 | if __name__ == '__main__': |
cliechti | 6385f2c | 2005-09-21 19:51:19 +0000 | [diff] [blame] | 253 | main() |