Python library for logging, utils and constants for Espressif Systems' Python projects
Project description
esp-pylib
Python library for logging, utils and constants for Espressif Systems' Python projects.
Installation
pip install esp-pylib
Optional IDE WebSocket support (structured messages to VS Code / other IDEs via websockets):
pip install esp-pylib[ide]
[!NOTE] IDE features require Python ≥ 3.8 (because
websockets ≥ 12does). On Python 3.7,pip install esp-pylib[ide]still succeeds but pulls in nowebsockets—esp_pylib.wsbecomes a no-op:send_log_message()returns silently, andsend_event/wait_for_event/ensure_connectedraiseFatalError.
Optional serial port helpers (port discovery, DTR/RTS reset primitives — pulls in pyserial):
pip install esp-pylib[serial]
Optional CLI helpers (Click parameter types — pulls in rich-click and click):
pip install esp-pylib[cli]
Quick start
[!TIP] If you are migrating an existing Espressif Python tool, the recommended path is the migration skill — an AI coding agent applies the changes for you. This manual quick start is here for new code, for migrating by hand, or in case the skill doesn't work in your setup.
Install the base package (pip install esp-pylib) and use the shared logger — the most common entry point. The log singleton is ready to import; no setup required.
from esp_pylib.logger import log
log.print('Plain output to stdout')
log.note('Informational note')
log.hint('Actionable hint on how to proceed')
log.warn('Something looks off') # stderr
log.err('Something failed') # stderr
log.debug('Only shown in verbose mode') # stdout, verbose only
On a color-capable terminal the helpers are styled distinctly (blue NOTE:, teal HINT:, bold yellow WARNING:, red ERROR:):
Control how much is shown (e.g. from a --verbose / --quiet flag):
from esp_pylib.logger import log, Verbosity
log.set_verbosity(Verbosity.VERBOSE) # also accepts 'VERBOSE', 'NORMAL', 'SILENT'
Show in-place progress for a bounded loop:
from esp_pylib.logger import log
with log.progress(total=len(items), description='Processing') as bar:
for item in items:
do_work(item)
bar.update(1)
When the final count isn't known up front, use a live counter (no bar or percent):
from esp_pylib.logger import log
with log.counter(description='Collecting required components') as counter:
for item in discover():
counter.update(1)
Raise FatalError for unrecoverable conditions and handle it once at your CLI entry point:
from esp_pylib.errors import FatalError
from esp_pylib.logger import log
def main():
raise FatalError('Could not connect to the device')
if __name__ == '__main__':
try:
main()
except FatalError as e:
log.die(str(e), exit_code=2)
That's enough for most tools. See Modules below for config files, serial port helpers, IDE integration, and more.
Modules
esp_pylib.constants— Cross-tool values shared by multiple modules: Espressif USB VID/PID, default ROM baud rate, and serial port name / exclude patterns used by port discovery.esp_pylib.errors— A small exception hierarchy (FatalErrorand common subclasses such asNoSerialPortFoundError,ConfigError) for consistent error handling across tools.esp_pylib.logger— Shared logging for Espressif Python tools: verbosity levels (Verbosity), the default Rich-based singleton (log/EspLog), andEspLogBaseso you can plug in a custom implementation viaEspLog.set_logger(). Helpers:log.err/log.warn(stderr, IDE-forwarded),log.note/log.hint(stdout; cyanHINT:distinct from warnings for color-vision deficiency),log.debug(verbose only). Also provides progress output:log.progress(...)/ProgressTask(bounded bar),log.counter(...)/CounterTask(live count, no bar), and the lower-levellog.progress_bar(...)rendering hook.esp_pylib.config—ToolConfigfinds, parses, and caches a per-tool INI config file. Search order: env-var override → cwd → OS user-config dir (~/.config/<tool>/on POSIX,~/AppData/Local/<tool>/on Windows) → home. Files that don't contain the tool's section are silently skipped during search so candidates likesetup.cfg/tox.iniare safe to list.load()returns(ConfigParser, Optional[Path]);get(key, fallback)is a convenience for single-value lookups. Both are cached after the first call; callreload()to re-scan. Pure stdlib — no extras required.esp_pylib.rom— ROM ELF path resolution foresp-idf-monitorandesp-coredump. ReadsIDF_PATHandESP_ROM_ELF_DIRfrom the environment, looks up chip revision entries inroms.json(current and legacy ESP-IDF locations), and returns{target}_rev{chip_rev}_rom.elfunderESP_ROM_ELF_DIR. Pure stdlib — no extras required.esp_pylib.ws— WebSocket client for IDE integration: sends structured JSON when an IDE sets the environment variable below. Requirespip install esp-pylib[ide](pulls inwebsockets; effectively a no-op on Python 3.7 — see Installation note above). The connection is created lazily on first use; if no URL is set, log helpers no-op.esp_pylib.excepthook— Hooks (sys.excepthookandthreading.excepthook, Python 3.8+) to report uncaught exceptions to the IDE over the same WebSocket channel, then chain to the previous hooks so normal stderr behavior is unchanged. Use together withesp-pylib[ide].esp_pylib.serial_ports— Serial port discovery, filtering, and sorting. Wraps pyserial'scomports()with Espressif-aware priority. Exposesget_port_list,get_port_names,detect_port,get_port_vid_pid, andparse_port_filters(forkey=valueCLI flags likevid=0x303A). Requirespip install esp-pylib[serial].esp_pylib.serial_reset— DTR/RTS primitives and named reset sequences shared betweenesptoolandesp-idf-monitor. Primitives:set_dtr,set_rts,set_dtr_rts. Sequences:classic_bootloader_reset,unix_tight_bootloader_reset,usb_jtag_bootloader_reset,hard_reset— each takesflow_control=Truefor adapters with always-on hardware flow control (e.g. the SiLabs CP2102C).uses_hardware_flow_control((vid, pid))decides that flag against the sharedHARDWARE_FLOW_CONTROL_VID_PIDSlist inesp_pylib.constants. Also includes a parser/executor for custom reset sequences in theD0|R1|U1,0|W0.1format. Requirespip install esp-pylib[serial].esp_pylib.cli_types— Reusable ClickParamTypes:SerialPortType,AnyIntType,AutoSizeType,BaudRateType, andarg_auto_int(). Requirespip install esp-pylib[cli].esp_pylib.cli_options— Reusable Click pieces:EspRichGroup(root group for subcommand CLIs usingOptionEatAll),MutuallyExclusiveOption(argparse-style exclusive groups), andOptionEatAll(consume values until the next flag or subcommand). Requirespip install esp-pylib[cli].
IDE integration (WebSocket)
Espressif IDEs can launch tools with a WebSocket URL so errors, warnings, and exceptions carry file, line, and optional suggestion text (e.g. full tracebacks) for click-to-navigate in the UI. CLI usage is unchanged when no URL is set: there is no connection and no overhead beyond reading the environment once.
Environment variables
| Variable | Purpose |
|---|---|
ESP_IDE_WS |
WebSocket URL (e.g. ws://127.0.0.1:12345). Set by the IDE, not by end users. |
esp_pylib.ws API
send_log_message(typ, message, suggestion, file, line)— Fire-and-forget.typis one ofwarning,error, orexception. Failures are ignored so a broken IDE connection cannot crash the tool.send_event(event, **kwargs)— Debug coordination (e.g. GDB stub / coredump). Sends{"type": "event", "event": "<name>", ...}. RaisesFatalErrorif the URL is unset,websocketsis missing, the connection cannot be established, or the send fails (each case carries a distinct message). The reservedtypeenvelope key always wins over an identically-named**kwargsentry.wait_for_event(event, retries=3)— Blocks until a JSON message with matchingeventis received (e.g.debug_finishedfrom the IDE). RaisesFatalErrorif the URL is unset,websocketsis missing, the connection cannot be established, or no matching message arrives withinretriesreconnects.set_ws_url(url)— Programmatic override for tools that expose their own flag (e.g. esp-idf-monitor's--ws). PassNoneto clear the override and fall back toESP_IDE_WSon next use. Closes any existing connection.close()— Closes the shared connection (call on clean tool exit if you opened one).
Log message JSON shape (tool → IDE):
{
"type": "warning | error | exception",
"file": "/absolute/path/to/source.py",
"line": 42,
"message": "Human-readable description",
"suggestion": "Optional fix text or full traceback, or null"
}
Uncaught exceptions
Call once at startup (after configuring logging if needed):
from esp_pylib.excepthook import install_exception_reporting
install_exception_reporting()
This reports uncaught exceptions to the IDE when ESP_IDE_WS is set (or set_ws_url() has been called). SystemExit and KeyboardInterrupt are not sent. The previous sys.excepthook / threading.excepthook handlers are always invoked afterward.
Progress bars
Use the log.progress(...) context manager for in-place progress output. It yields a ProgressTask; call update(advance, description=...) to advance it. The bar overwrites itself on a TTY, falls back to one full line per update on non-TTYs / verbose mode, and is suppressed entirely under Verbosity.SILENT.
from esp_pylib.logger import log
with log.progress(total=len(packages), description='Resolving') as bar:
for pkg in packages:
do_work(pkg)
bar.update(1, description=f'Resolving {pkg.name}')
Optional keyword arguments:
file=sys.stderr— render the bar on stderr (e.g. when stdout must stay clean for machine-readable output like SPDX).disable=True— turn the bar into a no-op (e.g. when a tool's--no-progressflag is set).unit='B'— humanise the M/N suffix for byte totals using 1024-basedkB/MB/GBprefixes (e.g.1.20MB/5.00MBinstead of raw integers).
# Byte upload with human-readable totals
with log.progress(total=file_size, description='Uploading', unit='B') as bar:
bar.update(bytes_sent)
For discovery with an unknown final count (no bar or percent), use log.counter(...):
with log.counter(description='Collecting required components') as counter:
counter.update(1) # once per item found
If the body of the with block raises, neither the progress bar nor the counter is finalized — you see the last real update before the traceback (no jump to 100% and no trailing newline flush).
The bar is always rendered at a fixed bar_length so the suffix (percent, M/N, elapsed time, …) stays in the same column on every redraw. When the active console can render colors, the unfilled portion uses a dim background bar; when colors are unavailable (NO_COLOR, piped output, no detected color system) the unfilled portion is rendered as plain spaces and gets replaced with ━ as progress advances.
For full control over rendering, override EspLogBase.progress_bar(cur_iter, total_iters, prefix, suffix, bar_length) in a custom logger. progress_bar is abstract: every EspLogBase subclass must implement it (a no-op body is fine if the logger doesn't render bars). Live counters use counter_line(prefix, suffix, final=False) — optional on EspLogBase (default no-op); EspLog provides TTY-aware rendering.
Collapsible stages
On an interactive terminal at normal verbosity, wrap noisy steps with log.stage() / log.stage(finish=True) (ported from esptool). Ordinary log.print() output inside the stage is erased on successful finish; log.note() and log.warn() are buffered and shown afterward. Verbose mode and non-TTY stdout disable collapsing (output is kept as printed).
from esp_pylib.logger import log
log.stage()
log.print('Connecting...')
# ...
log.stage(finish=True)
Custom logger
Subclass EspLogBase, implement its methods, then register your instance so all code using the shared logger goes through your implementation:
import sys
from esp_pylib.logger import EspLog, EspLogBase, Verbosity
class MyLogger(EspLogBase):
def __init__(self):
self._verbosity = Verbosity.NORMAL
def print(self, *args, **kwargs):
print(*args, **kwargs)
def err(self, *args, suggestion=None):
print("ERROR:", *args, file=sys.stderr)
def warn(self, *args, suggestion=None):
print("WARNING:", *args, file=sys.stderr)
def note(self, *args):
print("NOTE:", *args)
def hint(self, *args):
print("HINT:", *args)
def debug(self, *args):
if self._verbosity == Verbosity.VERBOSE:
print(*args)
def set_verbosity(self, mode):
if isinstance(mode, str):
mode = Verbosity[mode.upper()]
self._verbosity = mode
def progress_bar(self, cur_iter, total_iters, prefix='', suffix='', bar_length=30):
# Render however you like (file, GUI, ...); a no-op is fine if you
# don't render progress.
pass
EspLog.set_logger(MyLogger())
Migration Skill for AI Coding Agents
This repository ships a tool-agnostic Agent Skill under ./migrate-to-esp-pylib/ that walks an AI coding agent through replacing duplicated code in any Espressif Python tool (constants, FatalError classes, raw ANSI logging, Python logging calls, IDE WebSocket clients, exception hooks, INI config loaders, ROM ELF resolution, port discovery, reset sequences, argparse CLIs) with the matching esp-pylib module. Each step is tagged [Available] (perform now) or [Planned] (skip until the upstream module ships), so the same skill stays useful as esp-pylib grows.
The skill is split for progressive disclosure:
migrate-to-esp-pylib/SKILL.md— concise entry point: module status table, task checklist, critical rules, and links into the references.migrate-to-esp-pylib/references/workflow.md— full per-step instructions, code examples, and backward-compatibility patterns.
Use it from Cursor
The skill is meant for consumer-tool repos, not for day-to-day work in this repository. Two ways to apply it:
-
Reference on demand (simplest): in the tool repo you are migrating,
@-mention or attachmigrate-to-esp-pylib/SKILL.mdfrom a local clone of esp-pylib, a submodule, or a copiedmigrate-to-esp-pylib/directory. Plain prompts that point at the file path work too. -
Install once for global auto-invocation: symlink the skill directory from your esp-pylib checkout into your personal skills folder so Cursor/Claude picks it up across repos and stays in sync when you pull esp-pylib (preferred over copying). Replace
<path-to-esp-pylib>with the absolute path to your clone (relative targets break easily):ln -sfn <path-to-esp-pylib>/migrate-to-esp-pylib ~/.cursor/skills/migrate-to-esp-pylib ln -sfn <path-to-esp-pylib>/migrate-to-esp-pylib ~/.claude/skills/migrate-to-esp-pylib
Example:
ln -sfn ~/Documents/esp-pylib/migrate-to-esp-pylib ~/.cursor/skills/migrate-to-esp-pylibIf the symlink is not picked up by Cursor,
@-mentioning the skill file is the reliable fallback.
Use it from Other AI Coding Agents
The skill is plain Markdown with YAML frontmatter in SKILL.md, so any coding agent that can read repository files works:
- GitHub Copilot Chat / VS Code: open
migrate-to-esp-pylib/SKILL.md(or attach it to the chat) and ask the agent to follow it for the migration; the agent will pull inreferences/workflow.mdas it works through the steps. - Claude Code and other agents: point at
migrate-to-esp-pylib/SKILL.md(global symlink, submodule, or copy). The rootAGENTS.mdhere is only for esp-pylib contributors keeping the skill in sync — not for running migrations in other repos. - Plain prompt: paste the contents of
migrate-to-esp-pylib/SKILL.mdinto the system prompt or initial message, and providereferences/workflow.mdwhen the agent reaches the per-step work.
Keeping the Skill in Sync with New Features
Required for any public-API change in
esp-pylib. Failing to update the skill in lockstep with code is a review blocker, because stale[Planned]/[Available]markers cause agents to either skip shipped features or invent imports for unshipped ones.
When you change anything user-facing in esp_pylib/, update these in order — in the same PR as the code change:
- Code — implement and test the change.
README.md— update the module summary and any code examples affected by the change.migrate-to-esp-pylib/SKILL.md:- Flip the affected row in the Module status table from
PlannedtoAvailable(or vice versa for a removal / deprecation). - Update the matching
[Planned]/[Available]marker on the per-step checklist. - Add new exported names to the frontmatter
descriptiontrigger keywords so the skill router still picks the file up.
- Flip the affected row in the Module status table from
migrate-to-esp-pylib/references/workflow.md:- Replace the placeholder example in the matching workflow step with a concrete, working example. Mirror the layout of the already-
Availablesteps (short prose, before/after code, gotchas). - Bump the install pin in Step 2 if a new minimum is required.
- Add backward-compatibility wrapper guidance under "Backward-compatibility patterns" if the new API returns a type that differs from common consumer expectations.
- Replace the placeholder example in the matching workflow step with a concrete, working example. Mirror the layout of the already-
CHANGELOG.md— do not hand-edit;cz bumphandles it via the conventional commit message.
What counts as a "public-API change":
| Change | Triggers skill update? |
|---|---|
New module, function, class, or constant exported via __all__ or top-level |
Yes |
| Signature change of any exported callable | Yes |
| Behaviour change visible to consumers (default values, error type, output stream) | Yes |
| New optional dependency or extras group | Yes |
| New environment variable read by the library | Yes |
| Internal refactor with identical public surface | No |
| Test-only change | No |
| Typo fix in docstring | No |
When in doubt, update the skill — the cost is small and the cost of stale agent guidance is large.
How to Contribute
First, set up the development environment:
git clone https://github.com/espressif/esp-pylib.git
cd esp-pylib
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
pre-commit install
When adding a new feature, also update the skill at ./migrate-to-esp-pylib/ following the checklist in Keeping the skill in sync with new features above. AI coding agents should pick this up from AGENTS.md in the repo root.
How to Release (For Maintainers Only)
python -m venv venv
source venv/bin/activate
pip install commitizen czespressif
git fetch
git checkout -b update/release_v1.1.0
git reset --hard origin/master
cz bump
git push -u
git push --tags
Create a pull request and edit the automatically created draft release notes.
License
This document and the attached source code are released under Apache License Version 2. See the accompanying LICENSE file for a copy.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file esp_pylib-1.0.0.tar.gz.
File metadata
- Download URL: esp_pylib-1.0.0.tar.gz
- Upload date:
- Size: 92.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
52696a1f5faa807375feb1a3ea9eaf5529a19e08d864254ff388de42101b052b
|
|
| MD5 |
5880bbafa4eec0fe1550f857874fb2dc
|
|
| BLAKE2b-256 |
dff0bbf49acefa2c3f9d80e60b3845ddd9fd8612d9160468be0cf3ecd3c71152
|
Provenance
The following attestation bundles were made for esp_pylib-1.0.0.tar.gz:
Publisher:
release_pypi.yml on espressif/esp-pylib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
esp_pylib-1.0.0.tar.gz -
Subject digest:
52696a1f5faa807375feb1a3ea9eaf5529a19e08d864254ff388de42101b052b - Sigstore transparency entry: 1717312680
- Sigstore integration time:
-
Permalink:
espressif/esp-pylib@c12525d48ad906b5a2b9ebe3f47f60ee2eea89dc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/espressif
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release_pypi.yml@c12525d48ad906b5a2b9ebe3f47f60ee2eea89dc -
Trigger Event:
release
-
Statement type:
File details
Details for the file esp_pylib-1.0.0-py3-none-any.whl.
File metadata
- Download URL: esp_pylib-1.0.0-py3-none-any.whl
- Upload date:
- Size: 55.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c8470f948dc5d56bd8d0a168b0c76277b2cbbbb4993e7703631a34655fd00a6
|
|
| MD5 |
5b4af0ba3c293f7ebbfd7031882f8839
|
|
| BLAKE2b-256 |
6a1f6eaf93fb328f330e551e18da5b8942efc8a9c99a6fba62c9503ba3e1c3ef
|
Provenance
The following attestation bundles were made for esp_pylib-1.0.0-py3-none-any.whl:
Publisher:
release_pypi.yml on espressif/esp-pylib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
esp_pylib-1.0.0-py3-none-any.whl -
Subject digest:
0c8470f948dc5d56bd8d0a168b0c76277b2cbbbb4993e7703631a34655fd00a6 - Sigstore transparency entry: 1717312791
- Sigstore integration time:
-
Permalink:
espressif/esp-pylib@c12525d48ad906b5a2b9ebe3f47f60ee2eea89dc -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/espressif
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release_pypi.yml@c12525d48ad906b5a2b9ebe3f47f60ee2eea89dc -
Trigger Event:
release
-
Statement type: