Last active
January 14, 2026 17:24
-
-
Save markusand/d5a7b9806dba059a565dfdd947d1747b to your computer and use it in GitHub Desktop.
Lightweight version of pyboard. Includes replacement of module constants declaration for execfile()
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # PEP 561 stub marker |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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