pyboard.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. #!/usr/bin/env python
  2. #
  3. # This file is part of the MicroPython project, http://micropython.org/
  4. #
  5. # The MIT License (MIT)
  6. #
  7. # Copyright (c) 2014-2019 Damien P. George
  8. # Copyright (c) 2017 Paul Sokolovsky
  9. #
  10. # Permission is hereby granted, free of charge, to any person obtaining a copy
  11. # of this software and associated documentation files (the "Software"), to deal
  12. # in the Software without restriction, including without limitation the rights
  13. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  14. # copies of the Software, and to permit persons to whom the Software is
  15. # furnished to do so, subject to the following conditions:
  16. #
  17. # The above copyright notice and this permission notice shall be included in
  18. # all copies or substantial portions of the Software.
  19. #
  20. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  21. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  22. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  23. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  24. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  25. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  26. # THE SOFTWARE.
  27. """
  28. pyboard interface
  29. This module provides the Pyboard class, used to communicate with and
  30. control a MicroPython device over a communication channel. Both real
  31. boards and emulated devices (e.g. running in QEMU) are supported.
  32. Various communication channels are supported, including a serial
  33. connection, telnet-style network connection, external process
  34. connection.
  35. Example usage:
  36. import pyboard
  37. pyb = pyboard.Pyboard('/dev/ttyACM0')
  38. Or:
  39. pyb = pyboard.Pyboard('192.168.1.1')
  40. Then:
  41. pyb.enter_raw_repl()
  42. pyb.exec('import pyb')
  43. pyb.exec('pyb.LED(1).on()')
  44. pyb.exit_raw_repl()
  45. Note: if using Python2 then pyb.exec must be written as pyb.exec_.
  46. To run a script from the local machine on the board and print out the results:
  47. import pyboard
  48. pyboard.execfile('test.py', device='/dev/ttyACM0')
  49. This script can also be run directly. To execute a local script, use:
  50. ./pyboard.py test.py
  51. Or:
  52. python pyboard.py test.py
  53. """
  54. import sys
  55. import time
  56. import os
  57. try:
  58. stdout = sys.stdout.buffer
  59. except AttributeError:
  60. # Python2 doesn't have buffer attr
  61. stdout = sys.stdout
  62. def stdout_write_bytes(b):
  63. b = b.replace(b"\x04", b"")
  64. stdout.write(b)
  65. stdout.flush()
  66. class PyboardError(Exception):
  67. pass
  68. class TelnetToSerial:
  69. def __init__(self, ip, user, password, read_timeout=None):
  70. self.tn = None
  71. import telnetlib
  72. self.tn = telnetlib.Telnet(ip, timeout=15)
  73. self.read_timeout = read_timeout
  74. if b'Login as:' in self.tn.read_until(b'Login as:', timeout=read_timeout):
  75. self.tn.write(bytes(user, 'ascii') + b"\r\n")
  76. if b'Password:' in self.tn.read_until(b'Password:', timeout=read_timeout):
  77. # needed because of internal implementation details of the telnet server
  78. time.sleep(0.2)
  79. self.tn.write(bytes(password, 'ascii') + b"\r\n")
  80. if b'for more information.' in self.tn.read_until(b'Type "help()" for more information.', timeout=read_timeout):
  81. # login successful
  82. from collections import deque
  83. self.fifo = deque()
  84. return
  85. raise PyboardError('Failed to establish a telnet connection with the board')
  86. def __del__(self):
  87. self.close()
  88. def close(self):
  89. if self.tn:
  90. self.tn.close()
  91. def read(self, size=1):
  92. while len(self.fifo) < size:
  93. timeout_count = 0
  94. data = self.tn.read_eager()
  95. if len(data):
  96. self.fifo.extend(data)
  97. timeout_count = 0
  98. else:
  99. time.sleep(0.25)
  100. if self.read_timeout is not None and timeout_count > 4 * self.read_timeout:
  101. break
  102. timeout_count += 1
  103. data = b''
  104. while len(data) < size and len(self.fifo) > 0:
  105. data += bytes([self.fifo.popleft()])
  106. return data
  107. def write(self, data):
  108. self.tn.write(data)
  109. return len(data)
  110. def inWaiting(self):
  111. n_waiting = len(self.fifo)
  112. if not n_waiting:
  113. data = self.tn.read_eager()
  114. self.fifo.extend(data)
  115. return len(data)
  116. else:
  117. return n_waiting
  118. class ProcessToSerial:
  119. "Execute a process and emulate serial connection using its stdin/stdout."
  120. def __init__(self, cmd):
  121. import subprocess
  122. self.subp = subprocess.Popen(cmd, bufsize=0, shell=True, preexec_fn=os.setsid,
  123. stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  124. # Initially was implemented with selectors, but that adds Python3
  125. # dependency. However, there can be race conditions communicating
  126. # with a particular child process (like QEMU), and selectors may
  127. # still work better in that case, so left inplace for now.
  128. #
  129. #import selectors
  130. #self.sel = selectors.DefaultSelector()
  131. #self.sel.register(self.subp.stdout, selectors.EVENT_READ)
  132. import select
  133. self.poll = select.poll()
  134. self.poll.register(self.subp.stdout.fileno())
  135. def close(self):
  136. import signal
  137. os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)
  138. def read(self, size=1):
  139. data = b""
  140. while len(data) < size:
  141. data += self.subp.stdout.read(size - len(data))
  142. return data
  143. def write(self, data):
  144. self.subp.stdin.write(data)
  145. return len(data)
  146. def inWaiting(self):
  147. #res = self.sel.select(0)
  148. res = self.poll.poll(0)
  149. if res:
  150. return 1
  151. return 0
  152. class ProcessPtyToTerminal:
  153. """Execute a process which creates a PTY and prints slave PTY as
  154. first line of its output, and emulate serial connection using
  155. this PTY."""
  156. def __init__(self, cmd):
  157. import subprocess
  158. import re
  159. import serial
  160. self.subp = subprocess.Popen(cmd.split(), bufsize=0, shell=False, preexec_fn=os.setsid,
  161. stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  162. pty_line = self.subp.stderr.readline().decode("utf-8")
  163. m = re.search(r"/dev/pts/[0-9]+", pty_line)
  164. if not m:
  165. print("Error: unable to find PTY device in startup line:", pty_line)
  166. self.close()
  167. sys.exit(1)
  168. pty = m.group()
  169. # rtscts, dsrdtr params are to workaround pyserial bug:
  170. # http://stackoverflow.com/questions/34831131/pyserial-does-not-play-well-with-virtual-port
  171. self.ser = serial.Serial(pty, interCharTimeout=1, rtscts=True, dsrdtr=True)
  172. def close(self):
  173. import signal
  174. os.killpg(os.getpgid(self.subp.pid), signal.SIGTERM)
  175. def read(self, size=1):
  176. return self.ser.read(size)
  177. def write(self, data):
  178. return self.ser.write(data)
  179. def inWaiting(self):
  180. return self.ser.inWaiting()
  181. class Pyboard:
  182. def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0):
  183. if device.startswith("exec:"):
  184. self.serial = ProcessToSerial(device[len("exec:"):])
  185. elif device.startswith("execpty:"):
  186. self.serial = ProcessPtyToTerminal(device[len("qemupty:"):])
  187. elif device and device[0].isdigit() and device[-1].isdigit() and device.count('.') == 3:
  188. # device looks like an IP address
  189. self.serial = TelnetToSerial(device, user, password, read_timeout=10)
  190. else:
  191. import serial
  192. delayed = False
  193. for attempt in range(wait + 1):
  194. try:
  195. self.serial = serial.Serial(device, baudrate=baudrate, interCharTimeout=1)
  196. break
  197. except (OSError, IOError): # Py2 and Py3 have different errors
  198. if wait == 0:
  199. continue
  200. if attempt == 0:
  201. sys.stdout.write('Waiting {} seconds for pyboard '.format(wait))
  202. delayed = True
  203. time.sleep(1)
  204. sys.stdout.write('.')
  205. sys.stdout.flush()
  206. else:
  207. if delayed:
  208. print('')
  209. raise PyboardError('failed to access ' + device)
  210. if delayed:
  211. print('')
  212. def close(self):
  213. self.serial.close()
  214. def read_until(self, min_num_bytes, ending, timeout=10, data_consumer=None):
  215. # if data_consumer is used then data is not accumulated and the ending must be 1 byte long
  216. assert data_consumer is None or len(ending) == 1
  217. data = self.serial.read(min_num_bytes)
  218. if data_consumer:
  219. data_consumer(data)
  220. timeout_count = 0
  221. while True:
  222. if data.endswith(ending):
  223. break
  224. elif self.serial.inWaiting() > 0:
  225. new_data = self.serial.read(1)
  226. if data_consumer:
  227. data_consumer(new_data)
  228. data = new_data
  229. else:
  230. data = data + new_data
  231. timeout_count = 0
  232. else:
  233. timeout_count += 1
  234. if timeout is not None and timeout_count >= 100 * timeout:
  235. break
  236. time.sleep(0.01)
  237. return data
  238. def enter_raw_repl(self):
  239. self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program
  240. # flush input (without relying on serial.flushInput())
  241. n = self.serial.inWaiting()
  242. while n > 0:
  243. self.serial.read(n)
  244. n = self.serial.inWaiting()
  245. self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL
  246. data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>')
  247. if not data.endswith(b'raw REPL; CTRL-B to exit\r\n>'):
  248. print(data)
  249. raise PyboardError('could not enter raw repl')
  250. self.serial.write(b'\x04') # ctrl-D: soft reset
  251. data = self.read_until(1, b'soft reboot\r\n')
  252. if not data.endswith(b'soft reboot\r\n'):
  253. print(data)
  254. raise PyboardError('could not enter raw repl')
  255. # By splitting this into 2 reads, it allows boot.py to print stuff,
  256. # which will show up after the soft reboot and before the raw REPL.
  257. data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n')
  258. if not data.endswith(b'raw REPL; CTRL-B to exit\r\n'):
  259. print(data)
  260. raise PyboardError('could not enter raw repl')
  261. def exit_raw_repl(self):
  262. self.serial.write(b'\r\x02') # ctrl-B: enter friendly REPL
  263. def follow(self, timeout, data_consumer=None):
  264. # wait for normal output
  265. data = self.read_until(1, b'\x04', timeout=timeout, data_consumer=data_consumer)
  266. if not data.endswith(b'\x04'):
  267. raise PyboardError('timeout waiting for first EOF reception')
  268. data = data[:-1]
  269. # wait for error output
  270. data_err = self.read_until(1, b'\x04', timeout=timeout)
  271. if not data_err.endswith(b'\x04'):
  272. raise PyboardError('timeout waiting for second EOF reception')
  273. data_err = data_err[:-1]
  274. # return normal and error output
  275. return data, data_err
  276. def exec_raw_no_follow(self, command):
  277. if isinstance(command, bytes):
  278. command_bytes = command
  279. else:
  280. command_bytes = bytes(command, encoding='utf8')
  281. # check we have a prompt
  282. data = self.read_until(1, b'>')
  283. if not data.endswith(b'>'):
  284. raise PyboardError('could not enter raw repl')
  285. # write command
  286. for i in range(0, len(command_bytes), 256):
  287. self.serial.write(command_bytes[i:min(i + 256, len(command_bytes))])
  288. time.sleep(0.01)
  289. self.serial.write(b'\x04')
  290. # check if we could exec command
  291. data = self.serial.read(2)
  292. if data != b'OK':
  293. raise PyboardError('could not exec command (response: %r)' % data)
  294. def exec_raw(self, command, timeout=10, data_consumer=None):
  295. self.exec_raw_no_follow(command);
  296. return self.follow(timeout, data_consumer)
  297. def eval(self, expression):
  298. ret = self.exec_('print({})'.format(expression))
  299. ret = ret.strip()
  300. return ret
  301. def exec_(self, command, data_consumer=None):
  302. ret, ret_err = self.exec_raw(command, data_consumer=data_consumer)
  303. if ret_err:
  304. raise PyboardError('exception', ret, ret_err)
  305. return ret
  306. def execfile(self, filename):
  307. with open(filename, 'rb') as f:
  308. pyfile = f.read()
  309. return self.exec_(pyfile)
  310. def get_time(self):
  311. t = str(self.eval('pyb.RTC().datetime()'), encoding='utf8')[1:-1].split(', ')
  312. return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])
  313. def fs_ls(self, src):
  314. cmd = "import uos\nfor f in uos.ilistdir(%s):\n" \
  315. " print('{:12} {}{}'.format(f[3]if len(f)>3 else 0,f[0],'/'if f[1]&0x4000 else ''))" % \
  316. (("'%s'" % src) if src else '')
  317. self.exec_(cmd, data_consumer=stdout_write_bytes)
  318. def fs_cat(self, src, chunk_size=256):
  319. cmd = "with open('%s') as f:\n while 1:\n" \
  320. " b=f.read(%u)\n if not b:break\n print(b,end='')" % (src, chunk_size)
  321. self.exec_(cmd, data_consumer=stdout_write_bytes)
  322. def fs_get(self, src, dest, chunk_size=256):
  323. self.exec_("f=open('%s','rb')\nr=f.read" % src)
  324. with open(dest, 'wb') as f:
  325. while True:
  326. data = bytearray()
  327. self.exec_("print(r(%u))" % chunk_size, data_consumer=lambda d:data.extend(d))
  328. assert data.endswith(b'\r\n\x04')
  329. data = eval(str(data[:-3], 'ascii'))
  330. if not data:
  331. break
  332. f.write(data)
  333. self.exec_("f.close()")
  334. def fs_put(self, src, dest, chunk_size=256):
  335. self.exec_("f=open('%s','wb')\nw=f.write" % dest)
  336. with open(src, 'rb') as f:
  337. while True:
  338. data = f.read(chunk_size)
  339. if not data:
  340. break
  341. if sys.version_info < (3,):
  342. self.exec_('w(b' + repr(data) + ')')
  343. else:
  344. self.exec_('w(' + repr(data) + ')')
  345. self.exec_("f.close()")
  346. def fs_mkdir(self, dir):
  347. self.exec_("import uos\nuos.mkdir('%s')" % dir)
  348. def fs_rmdir(self, dir):
  349. self.exec_("import uos\nuos.rmdir('%s')" % dir)
  350. def fs_rm(self, src):
  351. self.exec_("import uos\nuos.remove('%s')" % src)
  352. # in Python2 exec is a keyword so one must use "exec_"
  353. # but for Python3 we want to provide the nicer version "exec"
  354. setattr(Pyboard, "exec", Pyboard.exec_)
  355. def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', password='python'):
  356. pyb = Pyboard(device, baudrate, user, password)
  357. pyb.enter_raw_repl()
  358. output = pyb.execfile(filename)
  359. stdout_write_bytes(output)
  360. pyb.exit_raw_repl()
  361. pyb.close()
  362. def filesystem_command(pyb, args):
  363. def fname_remote(src):
  364. if src.startswith(':'):
  365. src = src[1:]
  366. return src
  367. def fname_cp_dest(src, dest):
  368. src = src.rsplit('/', 1)[-1]
  369. if dest is None or dest == '':
  370. dest = src
  371. elif dest == '.':
  372. dest = './' + src
  373. elif dest.endswith('/'):
  374. dest += src
  375. return dest
  376. cmd = args[0]
  377. args = args[1:]
  378. try:
  379. if cmd == 'cp':
  380. srcs = args[:-1]
  381. dest = args[-1]
  382. if srcs[0].startswith('./') or dest.startswith(':'):
  383. op = pyb.fs_put
  384. fmt = 'cp %s :%s'
  385. dest = fname_remote(dest)
  386. else:
  387. op = pyb.fs_get
  388. fmt = 'cp :%s %s'
  389. for src in srcs:
  390. src = fname_remote(src)
  391. dest2 = fname_cp_dest(src, dest)
  392. print(fmt % (src, dest2))
  393. op(src, dest2)
  394. else:
  395. op = {'ls': pyb.fs_ls, 'cat': pyb.fs_cat, 'mkdir': pyb.fs_mkdir,
  396. 'rmdir': pyb.fs_rmdir, 'rm': pyb.fs_rm}[cmd]
  397. if cmd == 'ls' and not args:
  398. args = ['']
  399. for src in args:
  400. src = fname_remote(src)
  401. print('%s :%s' % (cmd, src))
  402. op(src)
  403. except PyboardError as er:
  404. print(str(er.args[2], 'ascii'))
  405. pyb.exit_raw_repl()
  406. pyb.close()
  407. sys.exit(1)
  408. def main():
  409. import argparse
  410. cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.')
  411. cmd_parser.add_argument('--device', default='/dev/ttyACM0', help='the serial device or the IP address of the pyboard')
  412. cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device')
  413. cmd_parser.add_argument('-u', '--user', default='micro', help='the telnet login username')
  414. cmd_parser.add_argument('-p', '--password', default='python', help='the telnet login password')
  415. cmd_parser.add_argument('-c', '--command', help='program passed in as string')
  416. cmd_parser.add_argument('-w', '--wait', default=0, type=int, help='seconds to wait for USB connected board to become available')
  417. cmd_parser.add_argument('--follow', action='store_true', help='follow the output after running the scripts [default if no scripts given]')
  418. cmd_parser.add_argument('-f', '--filesystem', action='store_true', help='perform a filesystem action')
  419. cmd_parser.add_argument('files', nargs='*', help='input files')
  420. args = cmd_parser.parse_args()
  421. # open the connection to the pyboard
  422. try:
  423. pyb = Pyboard(args.device, args.baudrate, args.user, args.password, args.wait)
  424. except PyboardError as er:
  425. print(er)
  426. sys.exit(1)
  427. # run any command or file(s)
  428. if args.command is not None or args.filesystem or len(args.files):
  429. # we must enter raw-REPL mode to execute commands
  430. # this will do a soft-reset of the board
  431. try:
  432. pyb.enter_raw_repl()
  433. except PyboardError as er:
  434. print(er)
  435. pyb.close()
  436. sys.exit(1)
  437. def execbuffer(buf):
  438. try:
  439. ret, ret_err = pyb.exec_raw(buf, timeout=None, data_consumer=stdout_write_bytes)
  440. except PyboardError as er:
  441. print(er)
  442. pyb.close()
  443. sys.exit(1)
  444. except KeyboardInterrupt:
  445. sys.exit(1)
  446. if ret_err:
  447. pyb.exit_raw_repl()
  448. pyb.close()
  449. stdout_write_bytes(ret_err)
  450. sys.exit(1)
  451. # do filesystem commands, if given
  452. if args.filesystem:
  453. filesystem_command(pyb, args.files)
  454. args.files.clear()
  455. # run the command, if given
  456. if args.command is not None:
  457. execbuffer(args.command.encode('utf-8'))
  458. # run any files
  459. for filename in args.files:
  460. with open(filename, 'rb') as f:
  461. pyfile = f.read()
  462. execbuffer(pyfile)
  463. # exiting raw-REPL just drops to friendly-REPL mode
  464. pyb.exit_raw_repl()
  465. # if asked explicitly, or no files given, then follow the output
  466. if args.follow or (args.command is None and not args.filesystem and len(args.files) == 0):
  467. try:
  468. ret, ret_err = pyb.follow(timeout=None, data_consumer=stdout_write_bytes)
  469. except PyboardError as er:
  470. print(er)
  471. sys.exit(1)
  472. except KeyboardInterrupt:
  473. sys.exit(1)
  474. if ret_err:
  475. pyb.close()
  476. stdout_write_bytes(ret_err)
  477. sys.exit(1)
  478. # close the connection to the pyboard
  479. pyb.close()
  480. if __name__ == "__main__":
  481. main()