Skip to content

Instantly share code, notes, and snippets.

@markusand
Last active January 14, 2026 17:24
Show Gist options
  • Select an option

  • Save markusand/d5a7b9806dba059a565dfdd947d1747b to your computer and use it in GitHub Desktop.

Select an option

Save markusand/d5a7b9806dba059a565dfdd947d1747b to your computer and use it in GitHub Desktop.
Lightweight version of pyboard. Includes replacement of module constants declaration for execfile()
# PEP 561 stub marker
"""Minimal pyboard.py implementation for communicating with MicroPython boards"""
import os
import time
import re
from types import TracebackType
import serial
class PyboardError(Exception):
"""Exception raised for errors in the pyboard."""
def parse_error(error: PyboardError) -> str:
"""Parse a PyboardError into a human-readable message"""
message = str(error).split(r"\r\n")[-2:][0] # Get the second-to-last or last line
return message.split(":")[-1].strip()
class Pyboard:
"""Class for communicating with MicroPython boards"""
def __init__(self, device: str, baudrate: int = 115200, wait: int = 0):
"""Initialize connection to the board"""
try:
self._serial = serial.Serial(device, baudrate, timeout=1)
except serial.SerialException as e:
raise PyboardError(f"Failed to open serial port {device}: {str(e)}") from e
if wait:
time.sleep(wait)
self.enter_repl()
def __enter__(self) -> "Pyboard":
"""Context manager entry"""
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool:
"""Context manager exit - ensures connection is closed"""
self.close()
return False
def close(self) -> None:
"""Close the serial connection"""
if self._serial:
self.exit_repl()
self._serial.close()
def read_until(self, min_bytes: int, ending: bytes, timeout: int = 10) -> bytes:
"""Read from serial until ending sequence is found"""
if not self._serial:
raise PyboardError("Board not connected")
data = self._serial.read(min_bytes)
start_time = time.time()
while True:
if time.time() - start_time > timeout:
raise PyboardError("Timeout waiting for board response")
if data.endswith(ending):
break
if self._serial.in_waiting > 0:
data += self._serial.read(1)
start_time = time.time()
return data
def enter_repl(self) -> None:
"""Enter raw REPL mode"""
if not self._serial:
raise PyboardError("Board not connected")
self._serial.write(b"\x03\x03") # CTRL-C twice
time.sleep(0.1)
self._serial.write(b"\x01") # CTRL-A
self.read_until(1, b"raw REPL; CTRL-B to exit\r\n>")
def exit_repl(self) -> None:
"""Exit raw REPL mode"""
if not self._serial:
raise PyboardError("Board not connected")
self._serial.write(b"\x02") # CTRL-B
time.sleep(0.1)
def exec(self, command: str, timeout: int = 10) -> str:
"""Execute command in raw REPL mode"""
if not self._serial:
raise PyboardError("Board not connected")
self._serial.reset_input_buffer() # Flush any data that might be in the buffer
self._serial.write(command.encode("utf8") + b"\x04")
if self._serial.read(2) != b"OK":
raise PyboardError(f"Could not execute the command {command}")
data = self.read_until(1, b"\04")
if not data.endswith(b"\x04"):
raise PyboardError("Timeout waiting for first EOF")
data = data[:-1]
error = self.read_until(1, b"\x04", timeout=timeout)
if not error.endswith(b"\x04"):
raise PyboardError("Timeout waiting for second EOF")
if error[:-1]:
raise PyboardError(f"Error with command {command} because {error.decode('utf-8')[:-1]}")
return data.decode("utf8").strip()
def execfile(self, filepath: str, **kwargs: bool | str | int | float) -> str:
"""Execute a local MicroPython script file on the board with optional arguments"""
if not os.path.exists(filepath):
raise PyboardError(f"File not found: {filepath}")
try:
with open(filepath, "r", encoding="utf8") as file:
data = file.read()
# Remove comments and docstrings
data = re.sub(r'""".*?"""|#.*$', "", data, flags=re.MULTILINE)
# Replace module constant assignments
if kwargs:
for k, v in kwargs.items():
pattern = rf"^{k}\s*=\s*.*"
# Properly quote string values, keep other types as-is
if isinstance(v, str):
replacement = f"{k} = '{v}'"
elif isinstance(v, bool):
replacement = f"{k} = {str(v)}"
else:
replacement = f"{k} = {v}"
data = re.sub(pattern, replacement, data, flags=re.MULTILINE)
return self.exec(data)
except IOError as e:
raise PyboardError(f"Failed to read file {filepath}: {e}") from e
except PyboardError as e:
raise PyboardError(f"Error executing {filepath}: {e}") from e
except Exception as e:
raise PyboardError(f"Unexpected error executing {filepath}: {e}") from e
def reset(self) -> None:
"""Reset the board"""
if not self._serial:
raise PyboardError("Board not connected")
self._serial.write(b"\x04") # CTRL-D
time.sleep(0.5)
self.enter_repl()
"""Minimal pyboard.py implementation for communicating with MicroPython boards"""
from types import TracebackType
from typing import Literal
class PyboardError(Exception):
"""Exception raised for errors in the pyboard."""
def parse_error(error: PyboardError) -> str:
"""Parse a PyboardError into a human-readable message"""
class Pyboard:
"""Class for communicating with MicroPython boards"""
def __init__(self, device: str, baudrate: int = 115200, wait: int = 0) -> None: ...
def __enter__(self) -> Pyboard: ...
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> Literal[False]: ...
def close(self) -> None: ...
def read_until(self, min_bytes: int, ending: bytes, timeout: int = 10) -> bytes: ...
def enter_repl(self) -> None: ...
def exit_repl(self) -> None: ...
def exec(self, command: str, timeout: int = 10) -> str: ...
def execfile(self, filepath: str, **kwargs: bool | str | int | float) -> str: ...
def reset(self) -> None: ...
__all__: list[str]
from setuptools import setup
VERSION = "0.2.0"
setup(
name="pyboard",
description="Minimal pyboard.py implementation for communicating with MicroPython boards",
author="Marc Vilella",
license="MIT",
version=VERSION,
py_modules=["pyboard"],
package_data={"": ["*.pyi", "py.typed"]},
python_requires=">=3.10",
install_requires=[
"pyserial>=3.5",
],
zip_safe=False,
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment