Skip to content

Commit

Permalink
Merge pull request #11 from BrianPugh/webrepl
Browse files Browse the repository at this point in the history
WebREPL Support
  • Loading branch information
BrianPugh authored Aug 15, 2022
2 parents 539fbd0 + c2ea86d commit a0d69e1
Show file tree
Hide file tree
Showing 15 changed files with 609 additions and 78 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,4 @@ Temporary Items
.apdisk

/poetry.lock
profile.json
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Belay is a library that enables the rapid development of projects that interact

Belay works by interacting with the REPL interface of a micropython board from Python code running on PC.

Belay supports wired serial connections (USB) and wireless connections via WebREPL over WiFi.

`Quick Video of Belay in 22 seconds.`_

See `the documentation`_ for usage and other details.
Expand Down
73 changes: 41 additions & 32 deletions belay/device.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ast
import binascii
import hashlib
import importlib.resources as pkg_resources
import linecache
Expand All @@ -13,6 +12,7 @@
from ._minify import minify as minify_code
from .inspect import getsource
from .pyboard import Pyboard, PyboardException
from .webrepl import WebreplToSerial

# Typing
PythonLiteral = Union[None, bool, bytes, int, float, str, List, Dict, Set]
Expand Down Expand Up @@ -42,7 +42,7 @@ def _local_hash_file(fn):
if not data:
break
hasher.update(data)
return binascii.hexlify(hasher.digest()).decode()
return hasher.digest()


class _Executer(ABC):
Expand Down Expand Up @@ -217,32 +217,30 @@ def __init__(
Code to run on startup. Defaults to a few common imports.
"""
self._board = Pyboard(*args, **kwargs)
self._board.enter_raw_repl()
if isinstance(self._board.serial, WebreplToSerial):
soft_reset = False
else:
soft_reset = True
self._board.enter_raw_repl(soft_reset=soft_reset)

self.task = _TaskExecuter(self)
self.thread = _ThreadExecuter(self)

self._exec_snippet("startup")

if startup is None:
self._exec_snippet("convenience_imports")
self._exec_snippet("startup", "convenience_imports")
elif startup:
self(startup)
self(_read_snippet("startup") + "\n" + startup)

def _exec_snippet(self, name: str, *args):
def _exec_snippet(self, *names: str):
"""Load and execute a snippet from the snippets sub-package.
Parameters
----------
name : str
Snippet to load.
args
If provided, substitutes into loaded snippet.
names : str
Snippet(s) to load and execute.
"""
snippet = _read_snippet(name)
if args:
snippet = snippet % args
return self(snippet)
snippets = [_read_snippet(name) for name in names]
return self("\n".join(snippets))

def __call__(
self,
Expand Down Expand Up @@ -321,34 +319,45 @@ def sync(
keep = ["boot.py", "webrepl_cfg.py"]
elif isinstance(keep, str):
keep = [keep]
for dst in keep:
if dst[0] != "/":
dst = "/" + dst
self(f'all_files.discard("{dst}")')
keep = [x if x[0] == "/" else "/" + x for x in keep]

# Sort so that folder creation comes before file sending.
local_files = sorted(folder.rglob("*"))
for src in local_files:
dst = f"/{src.relative_to(folder)}"
src_objects = sorted(folder.rglob("*"))
src_files, src_dirs = [], []
for src_object in src_objects:
if src_object.is_dir():
src_dirs.append(src_object)
else:
src_files.append(src_object)
dst_files = [f"/{src.relative_to(folder)}" for src in src_files]
dst_dirs = [f"/{src.relative_to(folder)}" for src in src_dirs]
keep = [x for x in keep if x not in dst_files]
if dst_files + keep:
self(f"for x in {repr(dst_files + keep)}:\n all_files.discard(x)")

with tempfile.TemporaryDirectory() as tmp_dir:
tmp_dir = Path(tmp_dir) # Used if we need to perform a conversion
# Try and make all remote dirs
if dst_dirs:
self(f"__belay_mkdirs({repr(dst_dirs)})")

if src.is_dir():
self._exec_snippet("try_mkdir", dst)
continue
# Get all remote hashes
dst_hashes = self(f"__belay_hfs({repr(dst_files)})")

if len(dst_hashes) != len(dst_files):
raise Exception

for src, dst, dst_hash in zip(src_files, dst_files, dst_hashes):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_dir = Path(tmp_dir)

if minify and src.suffix == ".py":
minified = minify_code(src.read_text())
src = tmp_dir / src.name
src.write_text(minified)

# All other files, just sync over.
local_hash = _local_hash_file(src)
remote_hash = self(f"__belay_hash_file({repr(dst)})")
if local_hash != remote_hash:
src_hash = _local_hash_file(src)
if src_hash != dst_hash:
self._board.fs_put(src, dst)
self(f'all_files.discard("{dst}")')

# Remove all the files and directories that did not exist in local filesystem.
self._exec_snippet("sync_end")
Expand Down
4 changes: 4 additions & 0 deletions belay/pyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
import sys
import time

from .webrepl import WebreplToSerial

try:
stdout = sys.stdout.buffer
except AttributeError:
Expand Down Expand Up @@ -286,6 +288,8 @@ def __init__(
):
# device looks like an IP address
self.serial = TelnetToSerial(device, user, password, read_timeout=10)
elif device and device.startswith("ws://"):
self.serial = WebreplToSerial(device, password, read_timeout=10)
else:
import serial

Expand Down
29 changes: 19 additions & 10 deletions belay/snippets/sync_begin.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
# Creates and populates two set[str]: all_files, all_dirs
import os, hashlib, binascii
def __belay_hash_file(fn):
hasher = hashlib.sha256()
import os, hashlib, errno
def __belay_hf(fn):
h = hashlib.sha256()
try:
with open(fn, "rb") as f:
while True:
data = f.read(4096)
if not data:
break
hasher.update(data)
h.update(data)
except OSError:
return "0" * 64
return str(binascii.hexlify(hasher.digest()))
return b""
return h.digest()
def __belay_hfs(fns):
print(repr([__belay_hf(fn) for fn in fns]))
def __belay_mkdirs(fns):
for fn in fns:
try:
os.mkdir('%s')
except OSError as e:
if e.errno != errno.EEXIST:
raise
all_files, all_dirs = set(), []
def enumerate_fs(path=""):
def __belay_fs(path=""):
for elem in os.ilistdir(path):
full_name = path + "/" + elem[0]
if elem[1] & 0x4000: # is_dir
all_dirs.append(full_name)
enumerate_fs(full_name)
__belay_fs(full_name)
else:
all_files.add(full_name)
enumerate_fs()
__belay_fs()
all_dirs.sort()
del enumerate_fs
del __belay_fs
2 changes: 1 addition & 1 deletion belay/snippets/sync_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
os.rmdir(folder)
except OSError:
pass
del all_files, all_dirs, __belay_hash_file
del all_files, all_dirs, __belay_hf, __belay_hfs, __belay_mkdirs
6 changes: 0 additions & 6 deletions belay/snippets/try_mkdir.py

This file was deleted.

Loading

0 comments on commit a0d69e1

Please sign in to comment.