Upgrade to FastMCP 3.x, add Starlight docs site
Dependency changes: - fastmcp >=2.12.4 → >=3.0.2,<4 - Drop asyncio-mqtt (dead dependency, never imported) - Align pydantic, click, rich with FastMCP 3.x - Bump version to 2026.02.23 Docs site (docs-site/): - Astro Starlight with Tailwind v4 teal theme - 28 pages following Diataxis: 3 tutorials, 7 how-to guides, 13 reference pages, 4 explanation pages - Docker infrastructure (Caddy prod, Node HMR dev) - Configured for mcesptool.warehack.ing
This commit is contained in:
parent
6623cfa016
commit
b2ba0cd67a
3
docs-site/.env.example
Normal file
3
docs-site/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
COMPOSE_PROJECT=mcesptool-docs
|
||||||
|
DOMAIN=mcesptool.warehack.ing
|
||||||
|
MODE=prod
|
||||||
4
docs-site/.gitignore
vendored
Normal file
4
docs-site/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
*.tgz
|
||||||
49
docs-site/Dockerfile
Normal file
49
docs-site/Dockerfile
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
FROM node:22-slim AS base
|
||||||
|
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
FROM base AS deps
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Build static site
|
||||||
|
FROM deps AS build
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production: serve from Caddy
|
||||||
|
FROM caddy:2-alpine AS prod
|
||||||
|
COPY --from=build /app/dist /srv
|
||||||
|
COPY <<'CADDYFILE' /etc/caddy/Caddyfile
|
||||||
|
:80 {
|
||||||
|
root * /srv
|
||||||
|
file_server
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
@hashed path /_astro/*
|
||||||
|
header @hashed Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
@unhashed not path /_astro/*
|
||||||
|
header @unhashed Cache-Control "public, max-age=3600"
|
||||||
|
|
||||||
|
handle_errors {
|
||||||
|
@404 expression {err.status_code} == 404
|
||||||
|
handle @404 {
|
||||||
|
rewrite * /404.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try_files {path} {path}/ /index.html
|
||||||
|
}
|
||||||
|
CADDYFILE
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Development: Node with HMR
|
||||||
|
FROM deps AS dev
|
||||||
|
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 4321
|
||||||
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
11
docs-site/Makefile
Normal file
11
docs-site/Makefile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
prod:
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
dev:
|
||||||
|
docker compose --profile dev up --build
|
||||||
|
|
||||||
|
down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
logs:
|
||||||
|
docker compose logs -f
|
||||||
112
docs-site/astro.config.mjs
Normal file
112
docs-site/astro.config.mjs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import starlight from "@astrojs/starlight";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: "https://mcesptool.warehack.ing",
|
||||||
|
telemetry: false,
|
||||||
|
devToolbar: { enabled: false },
|
||||||
|
integrations: [
|
||||||
|
starlight({
|
||||||
|
title: "mcesptool",
|
||||||
|
description:
|
||||||
|
"ESP32 and ESP8266 development through Model Context Protocol",
|
||||||
|
favicon: "/favicon.svg",
|
||||||
|
social: [
|
||||||
|
{
|
||||||
|
icon: "github",
|
||||||
|
label: "Source",
|
||||||
|
href: "https://git.supported.systems/MCP/mcesptool",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customCss: ["./src/styles/global.css"],
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
label: "Tutorials",
|
||||||
|
items: [
|
||||||
|
{ label: "Getting Started", slug: "tutorials/getting-started" },
|
||||||
|
{ label: "First Flash", slug: "tutorials/first-flash" },
|
||||||
|
{
|
||||||
|
label: "First QEMU Session",
|
||||||
|
slug: "tutorials/first-qemu-session",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "How-To Guides",
|
||||||
|
autogenerate: { directory: "guides" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reference",
|
||||||
|
items: [
|
||||||
|
{ label: "Overview", slug: "reference" },
|
||||||
|
{
|
||||||
|
label: "Tool Reference",
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Chip Control",
|
||||||
|
slug: "reference/chip-control",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Flash Operations",
|
||||||
|
slug: "reference/flash-operations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Partition Management",
|
||||||
|
slug: "reference/partition-management",
|
||||||
|
},
|
||||||
|
{ label: "Security", slug: "reference/security" },
|
||||||
|
{
|
||||||
|
label: "Firmware Builder",
|
||||||
|
slug: "reference/firmware-builder",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "OTA Management",
|
||||||
|
slug: "reference/ota-management",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Production Tools",
|
||||||
|
slug: "reference/production-tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Diagnostics",
|
||||||
|
slug: "reference/diagnostics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "QEMU Manager",
|
||||||
|
slug: "reference/qemu-manager",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Server Tools",
|
||||||
|
slug: "reference/server-tools",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ label: "Resources", slug: "reference/resources" },
|
||||||
|
{ label: "Configuration", slug: "reference/configuration" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Explanation",
|
||||||
|
autogenerate: { directory: "explanation" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
sitemap(),
|
||||||
|
],
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
...(process.env.VITE_HMR_HOST && {
|
||||||
|
hmr: {
|
||||||
|
host: process.env.VITE_HMR_HOST,
|
||||||
|
protocol: "wss",
|
||||||
|
clientPort: 443,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
41
docs-site/docker-compose.yml
Normal file
41
docs-site/docker-compose.yml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
docs:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: prod
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
labels:
|
||||||
|
caddy: ${DOMAIN}
|
||||||
|
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||||
|
|
||||||
|
docs-dev:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: dev
|
||||||
|
profiles:
|
||||||
|
- dev
|
||||||
|
volumes:
|
||||||
|
- ./src:/app/src
|
||||||
|
- ./public:/app/public
|
||||||
|
- ./astro.config.mjs:/app/astro.config.mjs
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
environment:
|
||||||
|
- VITE_HMR_HOST=${DOMAIN}
|
||||||
|
labels:
|
||||||
|
caddy: ${DOMAIN}
|
||||||
|
caddy.reverse_proxy: "{{upstreams 4321}}"
|
||||||
|
caddy.reverse_proxy.flush_interval: "-1"
|
||||||
|
caddy.reverse_proxy.transport: "http"
|
||||||
|
caddy.reverse_proxy.transport.read_timeout: "0"
|
||||||
|
caddy.reverse_proxy.transport.write_timeout: "0"
|
||||||
|
caddy.reverse_proxy.transport.keepalive: "5m"
|
||||||
|
caddy.reverse_proxy.transport.keepalive_idle_conns: "10"
|
||||||
|
caddy.reverse_proxy.stream_timeout: "24h"
|
||||||
|
caddy.reverse_proxy.stream_close_delay: "5s"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
external: true
|
||||||
7979
docs-site/package-lock.json
generated
Normal file
7979
docs-site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
docs-site/package.json
Normal file
19
docs-site/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "mcesptool-docs",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/sitemap": "^3.3.1",
|
||||||
|
"@astrojs/starlight": "^0.37.6",
|
||||||
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
|
"astro": "^5.7.10",
|
||||||
|
"sharp": "^0.33.0",
|
||||||
|
"tailwindcss": "^4.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
docs-site/public/favicon.svg
Normal file
24
docs-site/public/favicon.svg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<!-- Chip body -->
|
||||||
|
<rect x="8" y="8" width="16" height="16" rx="2" fill="#0d9488"/>
|
||||||
|
<!-- Chip die -->
|
||||||
|
<rect x="12" y="12" width="8" height="8" rx="1" fill="#042f2e"/>
|
||||||
|
<!-- Pins left -->
|
||||||
|
<rect x="4" y="10" width="4" height="2" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="4" y="15" width="4" height="2" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="4" y="20" width="4" height="2" rx="0.5" fill="#5eead4"/>
|
||||||
|
<!-- Pins right -->
|
||||||
|
<rect x="24" y="10" width="4" height="2" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="24" y="15" width="4" height="2" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="24" y="20" width="4" height="2" rx="0.5" fill="#5eead4"/>
|
||||||
|
<!-- Pins top -->
|
||||||
|
<rect x="10" y="4" width="2" height="4" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="15" y="4" width="2" height="4" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="20" y="4" width="2" height="4" rx="0.5" fill="#5eead4"/>
|
||||||
|
<!-- Pins bottom -->
|
||||||
|
<rect x="10" y="24" width="2" height="4" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="15" y="24" width="2" height="4" rx="0.5" fill="#5eead4"/>
|
||||||
|
<rect x="20" y="24" width="2" height="4" rx="0.5" fill="#5eead4"/>
|
||||||
|
<!-- Corner dot -->
|
||||||
|
<circle cx="13.5" cy="13.5" r="1" fill="#5eead4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
7
docs-site/src/content.config.ts
Normal file
7
docs-site/src/content.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineCollection } from "astro:content";
|
||||||
|
import { docsLoader } from "@astrojs/starlight/loaders";
|
||||||
|
import { docsSchema } from "@astrojs/starlight/schema";
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||||
|
};
|
||||||
169
docs-site/src/content/docs/explanation/architecture.mdx
Normal file
169
docs-site/src/content/docs/explanation/architecture.mdx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
title: Architecture
|
||||||
|
description: Component architecture and system design
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, FileTree, Card, CardGrid } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
mcesptool is a FastMCP server that gives LLMs the ability to work with ESP32 and ESP8266 microcontrollers. Understanding how it is put together helps you reason about what it can do, where to extend it, and why certain boundaries exist.
|
||||||
|
|
||||||
|
## The big picture
|
||||||
|
|
||||||
|
At the highest level, three things happen when mcesptool starts:
|
||||||
|
|
||||||
|
1. The `ESPToolServer` class creates a FastMCP application.
|
||||||
|
2. It instantiates a series of **component classes**, each of which registers its own tools with that application.
|
||||||
|
3. It sets up MCP **resources** that expose server state for read-only inspection.
|
||||||
|
|
||||||
|
After initialization, the FastMCP runtime takes over. It listens for JSON-RPC messages on stdin, dispatches tool calls to the registered handlers, and sends responses back on stdout. The server itself is stateless between tool calls -- there is no persistent connection to any ESP device. Each tool invocation is a self-contained operation.
|
||||||
|
|
||||||
|
## Source layout
|
||||||
|
|
||||||
|
<FileTree>
|
||||||
|
- src/mcesptool/
|
||||||
|
- \_\_init\_\_.py
|
||||||
|
- server.py Server class + CLI entry point
|
||||||
|
- config.py ESPToolServerConfig dataclass
|
||||||
|
- components/
|
||||||
|
- \_\_init\_\_.py Component registry + exports
|
||||||
|
- chip_control.py 7 tools -- detection, connection, scanning, RAM loading, serial monitor
|
||||||
|
- flash_manager.py 7 tools -- write, read, erase, backup, multi-flash, verify
|
||||||
|
- partition_manager.py 3 tools -- OTA tables, custom layouts, analysis
|
||||||
|
- security_manager.py 4 tools -- eFuse, encryption, security audit
|
||||||
|
- firmware_builder.py 3 tools -- ELF conversion, image analysis
|
||||||
|
- ota_manager.py 3 tools -- package creation, deploy, rollback
|
||||||
|
- production_tools.py 3 tools -- factory programming, batch ops, QC
|
||||||
|
- diagnostics.py 3 tools -- memory dump, performance profiling, diagnostic report
|
||||||
|
- qemu_manager.py 5 tools -- virtual device lifecycle
|
||||||
|
</FileTree>
|
||||||
|
|
||||||
|
This is a flat component structure. There is no deep inheritance hierarchy and no framework-level abstraction layer between components. Each component file is a self-contained class that knows how to register tools and execute operations.
|
||||||
|
|
||||||
|
## Server initialization
|
||||||
|
|
||||||
|
`ESPToolServer.__init__` performs three phases of setup, in order:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self._initialize_components()
|
||||||
|
self._setup_server_info()
|
||||||
|
self._setup_resources()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Component initialization** creates instances of each component class, passing them the FastMCP `app` and the `ESPToolServerConfig`. The components are stored in a `self.components` dictionary keyed by name. This dictionary is used later for health checks and server info reporting.
|
||||||
|
|
||||||
|
**Server info tools** register a few meta-tools (`esp_server_info`, `esp_list_tools`, `esp_health_check`) that report on the server's own state. These are registered directly on the server rather than through a component because they need access to the full component list.
|
||||||
|
|
||||||
|
**Resources** register three MCP resources (`esp://server/status`, `esp://config`, `esp://capabilities`) that provide read-only snapshots of server state. Unlike tools, resources are designed for passive consumption -- an LLM can read them without triggering any side effects.
|
||||||
|
|
||||||
|
## Component registration pattern
|
||||||
|
|
||||||
|
Every component follows the same structure:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ChipControl:
|
||||||
|
def __init__(self, app: FastMCP, config: ESPToolServerConfig):
|
||||||
|
self.app = app
|
||||||
|
self.config = config
|
||||||
|
self._register_tools()
|
||||||
|
|
||||||
|
def _register_tools(self) -> None:
|
||||||
|
@self.app.tool("esp_detect_chip")
|
||||||
|
async def detect_chip(context: Context, port: str | None = None, ...) -> dict:
|
||||||
|
return await self._detect_chip_impl(context, port, ...)
|
||||||
|
|
||||||
|
async def _detect_chip_impl(self, context, port, ...) -> dict:
|
||||||
|
# actual work happens here
|
||||||
|
```
|
||||||
|
|
||||||
|
The constructor receives the FastMCP app and config, stores them, and immediately calls `_register_tools()`. Inside that method, inner functions decorated with `@self.app.tool()` serve as the MCP-visible entry points. Those inner functions delegate to `_*_impl()` methods on the class.
|
||||||
|
|
||||||
|
This two-layer pattern is not accidental. The inner functions exist because FastMCP needs the decorated function to have a specific signature with type annotations that match the MCP tool schema. The implementation methods can have a more flexible signature and are easier to call from other code paths, including health checks and cross-component integrations.
|
||||||
|
|
||||||
|
## Conditional loading
|
||||||
|
|
||||||
|
Not every component is always available. Two components load conditionally:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# QEMU emulation (if available)
|
||||||
|
if self.config.get_qemu_available():
|
||||||
|
self.components["qemu_manager"] = QemuManager(self.app, self.config)
|
||||||
|
|
||||||
|
# ESP-IDF integration (if available)
|
||||||
|
if self.config.get_idf_available():
|
||||||
|
from .components.idf_integration import IDFIntegration
|
||||||
|
self.components["idf_integration"] = IDFIntegration(self.app, self.config)
|
||||||
|
```
|
||||||
|
|
||||||
|
`QemuManager` only loads if the Espressif QEMU fork binaries are detected on the system (either via environment variable or auto-detection in `~/.espressif/tools/`). `IDFIntegration` only loads if an ESP-IDF installation is found. When these components are absent, their tools simply do not appear in the MCP tool list -- there are no stubs or error-returning placeholders.
|
||||||
|
|
||||||
|
This conditional approach means the server adapts to its host environment. A developer who only has esptool installed gets the core flashing and diagnostic tools. A developer with the full ESP-IDF toolchain and QEMU gets additional capabilities automatically.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
The IDFIntegration component uses a lazy import (`from .components.idf_integration import IDFIntegration` inside the conditional block) to avoid import-time failures when ESP-IDF Python packages are not installed.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Cross-wiring
|
||||||
|
|
||||||
|
One piece of initialization happens after all components are created:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if "qemu_manager" in self.components:
|
||||||
|
self.components["chip_control"].qemu_manager = self.components["qemu_manager"]
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives `ChipControl` a reference to `QemuManager` so that port scanning (`esp_scan_ports`) can include running QEMU virtual devices alongside physical serial ports. The cross-wire happens as a post-init step rather than a constructor argument because `ChipControl` is instantiated before `QemuManager`, and the QEMU manager might not load at all.
|
||||||
|
|
||||||
|
This is the only cross-component dependency in the system. Every other component operates in isolation.
|
||||||
|
|
||||||
|
## Configuration flow
|
||||||
|
|
||||||
|
Configuration follows a single path: **environment variables** feed into the `ESPToolServerConfig` dataclass, which is then passed to every component.
|
||||||
|
|
||||||
|
```
|
||||||
|
Environment variables
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ESPToolServerConfig.__post_init__()
|
||||||
|
-> _load_environment_variables()
|
||||||
|
-> _setup_esp_idf_path() # auto-detect common locations
|
||||||
|
-> _setup_qemu_paths() # auto-detect ~/.espressif/tools/
|
||||||
|
-> _setup_project_roots() # auto-detect project directories
|
||||||
|
-> _validate_configuration()
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Passed to every component constructor
|
||||||
|
```
|
||||||
|
|
||||||
|
The config object validates itself during construction. If esptool is not found on PATH, validation fails and the server will not start. Other checks, like unusual baud rates or out-of-range timeouts, produce warnings or errors depending on severity.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The server can also pick up project roots from MCP client roots at runtime via `initialize_with_context()`. This is a secondary configuration path that augments the environment-based config after the MCP connection is established.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## The component registry
|
||||||
|
|
||||||
|
The `components/__init__.py` file exports a `COMPONENT_REGISTRY` dictionary:
|
||||||
|
|
||||||
|
```python
|
||||||
|
COMPONENT_REGISTRY = {
|
||||||
|
"chip_control": ChipControl,
|
||||||
|
"flash_manager": FlashManager,
|
||||||
|
"partition_manager": PartitionManager,
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Today the server instantiates components directly by name. The registry exists to support future dynamic loading scenarios -- for example, enabling or disabling specific component groups via configuration, or allowing third-party components to register themselves.
|
||||||
|
|
||||||
|
## What the server does not do
|
||||||
|
|
||||||
|
Some architectural choices are defined by what is absent:
|
||||||
|
|
||||||
|
- **No persistent device connections.** The server does not hold open serial ports between tool calls. Every operation opens a connection, does its work, and closes it. This avoids stale connection state and port lock contention.
|
||||||
|
- **No internal state machine.** There is no concept of "connected to device X" or "currently flashing." Each tool call is independent.
|
||||||
|
- **No middleware layer.** Earlier versions had a middleware abstraction between components and esptool. This was removed in favor of direct subprocess calls, which reduced complexity without losing capability.
|
||||||
|
- **No tool dependency graph.** Tools do not declare dependencies on other tools. An LLM that wants to flash firmware and then verify it will call `esp_flash_firmware` followed by `esp_verify_flash` as two separate tool invocations. The server does not enforce ordering.
|
||||||
|
|
||||||
|
These omissions are deliberate. A stateless, subprocess-based architecture is more robust for a long-running server that interacts with unreliable hardware over serial connections. The trade-off is that some workflows require multiple tool calls where a stateful system might combine them, but this gives the LLM -- and the human -- full control over the sequence of operations.
|
||||||
195
docs-site/src/content/docs/explanation/component-design.mdx
Normal file
195
docs-site/src/content/docs/explanation/component-design.mdx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
---
|
||||||
|
title: Component Design
|
||||||
|
description: How tools are organized and registered
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Card, CardGrid } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
mcesptool organizes its 40+ tools into nine component classes. This page explains why the component boundary exists, how tool registration works internally, and the patterns that keep each component manageable.
|
||||||
|
|
||||||
|
## Why components at all
|
||||||
|
|
||||||
|
A single flat file with 40 tool functions would work. FastMCP does not require any particular organization -- you can register tools however you like. The component structure exists for human reasons, not framework reasons.
|
||||||
|
|
||||||
|
Each component groups tools that share a conceptual domain and tend to share helper code. Flash operations need output parsing for bytes-written counts. Security operations need to invoke `espefuse` in addition to `esptool`. QEMU management needs to track running processes. Grouping these concerns into classes creates a natural home for shared state and utility methods.
|
||||||
|
|
||||||
|
The alternative -- a utils module with shared helpers and a flat list of tool functions -- would produce the same runtime behavior. But the component classes provide a clear answer to "where does new code go?" When you need to add a tool for reading eFuse summary data, the answer is `SecurityManager`. When you need to add flash compression settings, the answer is `FlashManager`. This kind of obvious placement reduces coordination overhead.
|
||||||
|
|
||||||
|
## The registration mechanism
|
||||||
|
|
||||||
|
Tool registration uses FastMCP's `@app.tool()` decorator on inner functions defined inside each component's `_register_tools()` method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class FlashManager:
|
||||||
|
def __init__(self, app: FastMCP, config: ESPToolServerConfig) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.config = config
|
||||||
|
self._register_tools()
|
||||||
|
|
||||||
|
def _register_tools(self) -> None:
|
||||||
|
@self.app.tool("esp_flash_firmware")
|
||||||
|
async def flash_firmware(
|
||||||
|
context: Context,
|
||||||
|
firmware_path: str,
|
||||||
|
port: str | None = None,
|
||||||
|
address: str = "0x0",
|
||||||
|
verify: bool = True,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Flash firmware to ESP device."""
|
||||||
|
return await self._flash_firmware_impl(
|
||||||
|
context, firmware_path, port, address, verify
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Three things happen during `__init__`:
|
||||||
|
|
||||||
|
1. The component stores references to the FastMCP app and the server config.
|
||||||
|
2. `_register_tools()` is called, which defines and decorates inner functions.
|
||||||
|
3. FastMCP records each decorated function as a tool, including its name, docstring, and parameter schema.
|
||||||
|
|
||||||
|
After this, the component's job is done in terms of registration. FastMCP owns the routing -- when a tool call arrives over JSON-RPC, FastMCP dispatches it to the registered function based on the tool name.
|
||||||
|
|
||||||
|
## Why inner functions
|
||||||
|
|
||||||
|
You might wonder why the tools are defined as inner functions inside `_register_tools()` rather than as regular methods on the class. The reason is FastMCP's introspection.
|
||||||
|
|
||||||
|
FastMCP inspects the decorated function's signature to build the tool's parameter schema. It looks at the function's type annotations, default values, and docstring to generate the JSON Schema that gets advertised to MCP clients. For this to work correctly, the function needs to have exactly the right signature -- the parameters that the LLM will provide, plus a `Context` parameter.
|
||||||
|
|
||||||
|
If the tool were a regular class method, its first parameter would be `self`, which FastMCP would try to include in the schema. The inner function pattern avoids this by defining a standalone function that closes over `self`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# This is what the LLM sees in the tool schema:
|
||||||
|
# name: "esp_flash_firmware"
|
||||||
|
# parameters: firmware_path (str), port (str|null), address (str), verify (bool)
|
||||||
|
#
|
||||||
|
# The inner function's context parameter is handled specially by FastMCP.
|
||||||
|
# self is captured via closure, invisible to the schema.
|
||||||
|
```
|
||||||
|
|
||||||
|
## The impl delegation pattern
|
||||||
|
|
||||||
|
Every inner function immediately delegates to a `_*_impl()` method:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@self.app.tool("esp_flash_firmware")
|
||||||
|
async def flash_firmware(context, firmware_path, port, address, verify):
|
||||||
|
return await self._flash_firmware_impl(context, firmware_path, port, address, verify)
|
||||||
|
|
||||||
|
async def _flash_firmware_impl(self, context, firmware_path, port, address, verify):
|
||||||
|
# 50+ lines of actual logic
|
||||||
|
```
|
||||||
|
|
||||||
|
This looks like unnecessary indirection, and in a narrow sense it is. But it serves a few purposes:
|
||||||
|
|
||||||
|
**Testability.** The impl methods are regular instance methods that can be called directly in tests without going through FastMCP's dispatch layer. You can construct a `FlashManager` with a mock config, call `_flash_firmware_impl()` directly, and inspect the result.
|
||||||
|
|
||||||
|
**Readability.** The `_register_tools()` method reads as a clean list of tool definitions -- name, parameters, docstring. The implementation details live elsewhere. When you open a component file, you can quickly scan the tool list without getting lost in implementation code.
|
||||||
|
|
||||||
|
**Flexibility.** Impl methods can call each other. For example, `FlashManager._flash_backup_impl()` delegates to `_flash_read_impl()` to avoid duplicating the flash-read logic. This kind of internal reuse would be awkward if the tools were only accessible through their FastMCP-decorated entry points.
|
||||||
|
|
||||||
|
## The `_run_esptool` helper
|
||||||
|
|
||||||
|
Every component that interacts with hardware has its own `_run_esptool` (or `_run_cmd`) async method. These helpers build the esptool command, run it as a subprocess, handle timeouts, and return a standardized result dict.
|
||||||
|
|
||||||
|
The helpers are not shared across components. Each one has a slightly different signature tuned to its component's needs:
|
||||||
|
|
||||||
|
- **ChipControl's version** accepts a `command` string and `connect_attempts` parameter, because chip detection operations benefit from limited retries and quick timeouts.
|
||||||
|
- **FlashManager's version** takes a flat `args` list and defaults to a 120-second timeout, because flash operations are slow.
|
||||||
|
- **SecurityManager's version** is called `_run_cmd` and accepts an arbitrary command list, because it invokes both `esptool` and `espefuse`.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The duplication across component helpers is a conscious trade-off. A shared base class would reduce line count, but it would also create a coupling point where changes to one component's subprocess needs could affect others. Given that each helper is roughly 20 lines and rarely changes, the duplication cost is low.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Health checks
|
||||||
|
|
||||||
|
Each component implements an `async def health_check()` method that returns a status dictionary:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def health_check(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"status": "healthy",
|
||||||
|
"qemu_xtensa_available": bool(
|
||||||
|
self.config.qemu_xtensa_path
|
||||||
|
and Path(self.config.qemu_xtensa_path).exists()
|
||||||
|
),
|
||||||
|
"running_instances": sum(
|
||||||
|
1 for i in self.instances.values() if i.is_running
|
||||||
|
),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Health checks are called by the server's `esp_health_check` tool when the `detailed` flag is set. They report on component-specific concerns: whether binary dependencies exist, how many QEMU instances are running, whether serial ports are accessible.
|
||||||
|
|
||||||
|
The health check interface is informal -- there is no base class enforcing it. The server checks `hasattr(component, "health_check")` before calling it. This keeps the component contract light. Components that have nothing interesting to report beyond "I exist" return a simple `{"status": "healthy"}` dict.
|
||||||
|
|
||||||
|
## Naming convention
|
||||||
|
|
||||||
|
All tool names use the `esp_` prefix:
|
||||||
|
|
||||||
|
- `esp_detect_chip`
|
||||||
|
- `esp_flash_firmware`
|
||||||
|
- `esp_qemu_start`
|
||||||
|
- `esp_security_audit`
|
||||||
|
|
||||||
|
This namespacing prevents collisions when the MCP client has multiple servers connected. An LLM working with both mcesptool and a database MCP server will see `esp_flash_firmware` and `db_query` as clearly belonging to different domains.
|
||||||
|
|
||||||
|
The prefix also groups all mcesptool tools together when they are listed alphabetically, which is how most MCP clients present their tool catalogs.
|
||||||
|
|
||||||
|
## The component registry
|
||||||
|
|
||||||
|
The `components/__init__.py` module exports a `COMPONENT_REGISTRY` dictionary that maps names to classes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
COMPONENT_REGISTRY = {
|
||||||
|
"chip_control": ChipControl,
|
||||||
|
"flash_manager": FlashManager,
|
||||||
|
"partition_manager": PartitionManager,
|
||||||
|
"security_manager": SecurityManager,
|
||||||
|
"firmware_builder": FirmwareBuilder,
|
||||||
|
"ota_manager": OTAManager,
|
||||||
|
"production_tools": ProductionTools,
|
||||||
|
"diagnostics": Diagnostics,
|
||||||
|
"qemu_manager": QemuManager,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Today, the server does not use this registry for instantiation -- it creates components by name in `_initialize_components()`. The registry is forward-looking infrastructure. It enables patterns like configuration-driven component selection ("only load flash and diagnostics components") or plugin-style extension ("register a custom component from an external package").
|
||||||
|
|
||||||
|
## Cross-component references
|
||||||
|
|
||||||
|
The system has exactly one cross-component reference: `ChipControl` holds an optional reference to `QemuManager`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set by server after QemuManager initialization
|
||||||
|
self.qemu_manager = None
|
||||||
|
```
|
||||||
|
|
||||||
|
This reference exists so that `esp_scan_ports` can include running QEMU virtual devices in its results alongside physical serial ports. Without it, a port scan would only find hardware devices, and the LLM would need to call `esp_qemu_list` separately to discover virtual ones.
|
||||||
|
|
||||||
|
The cross-wire is set by the server after all components are initialized:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if "qemu_manager" in self.components:
|
||||||
|
self.components["chip_control"].qemu_manager = self.components["qemu_manager"]
|
||||||
|
```
|
||||||
|
|
||||||
|
This post-init wiring avoids both circular constructor dependencies and import-time coupling. `ChipControl` can be instantiated and tested without `QemuManager` -- the attribute simply remains `None`, and the scan results exclude QEMU devices.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
If you add new cross-component references, be careful about the initialization order in `ESPToolServer._initialize_components()`. Components are created sequentially, and post-init wiring only works when both components already exist.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Adding a new component
|
||||||
|
|
||||||
|
The pattern for adding a new component is straightforward:
|
||||||
|
|
||||||
|
1. Create a new file in `components/` with a class that accepts `app: FastMCP` and `config: ESPToolServerConfig`.
|
||||||
|
2. Implement `_register_tools()` with your tool definitions.
|
||||||
|
3. Implement `health_check()` if your component has meaningful status to report.
|
||||||
|
4. Add the class to `COMPONENT_REGISTRY` in `components/__init__.py`.
|
||||||
|
5. Instantiate it in `ESPToolServer._initialize_components()`.
|
||||||
|
|
||||||
|
The bar for adding a new component (vs. adding tools to an existing one) should be: does this group of tools share a distinct conceptual domain and unique helper code? If the answer is yes, a new component keeps things organized. If you are adding one or two tools that fit naturally alongside existing ones, extending the existing component is the simpler choice.
|
||||||
223
docs-site/src/content/docs/explanation/mcp-integration.mdx
Normal file
223
docs-site/src/content/docs/explanation/mcp-integration.mdx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
---
|
||||||
|
title: MCP Integration
|
||||||
|
description: How mcesptool uses Model Context Protocol features
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, LinkCard, Card, CardGrid } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
mcesptool exists because of MCP. Without the Model Context Protocol, it would be just another esptool wrapper. MCP is what makes it possible for an LLM to detect a chip, flash firmware, and diagnose problems through natural conversation. This page explains how mcesptool uses the protocol and the framework it is built on.
|
||||||
|
|
||||||
|
## What MCP provides
|
||||||
|
|
||||||
|
The Model Context Protocol is an open specification that defines how LLM applications communicate with external tools and data sources. It establishes a JSON-RPC message format, a set of capability types (tools, resources, prompts), and transport mechanisms for exchanging those messages.
|
||||||
|
|
||||||
|
For mcesptool, MCP provides three essential things:
|
||||||
|
|
||||||
|
1. **A standard way to describe tools.** Each tool has a name, a description, and a typed parameter schema. The LLM reads these descriptions to understand what operations are available.
|
||||||
|
2. **A structured invocation mechanism.** The LLM calls tools by name with JSON parameters. The server executes the operation and returns a JSON result. No prompt engineering required to parse output.
|
||||||
|
3. **A transport layer.** The MCP client (like Claude Code) spawns mcesptool as a subprocess and communicates over stdin/stdout using JSON-RPC. The server does not need to implement HTTP, manage authentication, or handle network concerns.
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Model Context Protocol Specification"
|
||||||
|
description="The official MCP specification covering transports, capabilities, and message formats."
|
||||||
|
href="https://modelcontextprotocol.io/specification"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## FastMCP as the runtime
|
||||||
|
|
||||||
|
mcesptool is built on FastMCP, a Python framework that handles MCP protocol details so the server code can focus on tool implementations.
|
||||||
|
|
||||||
|
FastMCP provides:
|
||||||
|
|
||||||
|
- **Tool registration** via the `@app.tool()` decorator, which inspects function signatures to auto-generate JSON Schema parameter descriptions.
|
||||||
|
- **Resource registration** via `@app.resource()` for read-only data endpoints.
|
||||||
|
- **Transport management** for stdio (the default), SSE, and HTTP transports.
|
||||||
|
- **Context objects** injected into tool handlers for accessing MCP capabilities like progress reporting and client roots.
|
||||||
|
|
||||||
|
The server creates a FastMCP application instance and registers everything against it:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.app = FastMCP("ESP Development Server")
|
||||||
|
```
|
||||||
|
|
||||||
|
That single object is the container for all tools, resources, and runtime configuration. Each component receives a reference to it and registers its own tools.
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="FastMCP Documentation"
|
||||||
|
description="Framework documentation covering tool registration, resources, context, and transports."
|
||||||
|
href="https://gofastmcp.com"
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Transport: stdio by default
|
||||||
|
|
||||||
|
When you add mcesptool to Claude Code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude mcp add mcesptool -- uvx mcesptool
|
||||||
|
```
|
||||||
|
|
||||||
|
Claude Code spawns mcesptool as a child process. Communication happens over the process's stdin and stdout using newline-delimited JSON-RPC messages. This is the **stdio transport**, and it is the default for MCP servers.
|
||||||
|
|
||||||
|
Stdio transport has several properties that matter for a hardware interaction tool:
|
||||||
|
|
||||||
|
- **No port conflicts.** There is no HTTP server to bind, no port to configure, no firewall to open.
|
||||||
|
- **Process lifecycle management.** The MCP client owns the server process. When Claude Code exits, it terminates the subprocess. No orphaned servers.
|
||||||
|
- **Implicit security.** The server only communicates with its parent process. There is no network surface to attack.
|
||||||
|
|
||||||
|
The trade-off is that stdio is point-to-point. Only one client can connect to a given server instance. For mcesptool, this is actually desirable -- you do not want two LLM sessions fighting over the same serial port.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Because MCP uses stdout for protocol messages, mcesptool must never `print()` to stdout. All diagnostic output goes to stderr via Python's logging module. FastMCP handles its own protocol output on stdout -- the server code should never write to it directly.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Tools: the primary interface
|
||||||
|
|
||||||
|
mcesptool registers 40+ tools with FastMCP. Each tool is a function with typed parameters and a structured JSON return value. Here is what one looks like from the LLM's perspective:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "esp_detect_chip",
|
||||||
|
"description": "Detect ESP chip type and gather comprehensive information",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"port": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"description": "Serial port (auto-detect if not specified)"
|
||||||
|
},
|
||||||
|
"baud_rate": {
|
||||||
|
"type": ["integer", "null"],
|
||||||
|
"description": "Connection baud rate"
|
||||||
|
},
|
||||||
|
"detailed": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Include detailed chip information and eFuse data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The LLM sees the tool name, reads the description, understands the parameter types, and can call it:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "esp_detect_chip",
|
||||||
|
"arguments": {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"detailed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The server executes the operation and returns structured data:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 460800,
|
||||||
|
"connection_time_seconds": 1.23,
|
||||||
|
"chip_info": {
|
||||||
|
"chip_type": "ESP32-S3",
|
||||||
|
"mac_address": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"flash_size": "8MB",
|
||||||
|
"features": ["WiFi", "BLE", "Embedded PSRAM 8MB"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This structured round-trip is what distinguishes MCP from ad-hoc tool use. The LLM does not need to parse CLI output or guess at formats. The JSON response has a consistent shape that the LLM can reason about directly.
|
||||||
|
|
||||||
|
## Resources: passive data
|
||||||
|
|
||||||
|
In addition to tools (which perform operations), mcesptool registers three MCP resources that provide read-only server state:
|
||||||
|
|
||||||
|
| Resource URI | Content |
|
||||||
|
|---|---|
|
||||||
|
| `esp://server/status` | Uptime, component count, production mode flag |
|
||||||
|
| `esp://config` | Current server configuration (paths, baud rate, timeouts) |
|
||||||
|
| `esp://capabilities` | Feature availability matrix (which chips, which operations, QEMU/IDF status) |
|
||||||
|
|
||||||
|
Resources differ from tools in two ways. First, they do not take parameters -- they are simple data endpoints. Second, they are designed for passive consumption. An MCP client can subscribe to resources and re-read them periodically without triggering side effects.
|
||||||
|
|
||||||
|
In practice, the LLM uses resources to understand the server's current state before deciding which tools to call. For example, reading `esp://capabilities` lets the LLM know whether QEMU emulation is available before it tries to start a virtual device.
|
||||||
|
|
||||||
|
## Context: accessing MCP capabilities
|
||||||
|
|
||||||
|
Most tool handlers accept a FastMCP `Context` parameter:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def detect_chip(context: Context, port: str | None = None, ...) -> dict:
|
||||||
|
```
|
||||||
|
|
||||||
|
The Context object provides access to MCP capabilities that go beyond simple request/response:
|
||||||
|
|
||||||
|
- **`context.list_roots()`** -- Discover directories that the MCP client has granted access to. mcesptool uses this to find project roots.
|
||||||
|
- **Progress reporting** -- Tools can report progress during long operations. (Currently, mcesptool's subprocess pattern makes real-time progress difficult, but the mechanism is available.)
|
||||||
|
- **Logging** -- Tools can emit log messages back to the MCP client for display.
|
||||||
|
|
||||||
|
The `initialize_with_context()` method on the config object shows how roots integration works:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def initialize_with_context(self, context: Context) -> bool:
|
||||||
|
mcp_roots = await context.list_roots()
|
||||||
|
if mcp_roots:
|
||||||
|
for root in mcp_roots:
|
||||||
|
root_path = Path(root.get("uri", "").replace("file://", ""))
|
||||||
|
if root_path.exists():
|
||||||
|
self.project_roots.append(root_path)
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
When the MCP client provides roots (directories it considers part of the current workspace), mcesptool adds them to its list of known project directories. This means the server can find ESP project files relative to the user's workspace without requiring explicit path configuration.
|
||||||
|
|
||||||
|
## Configuration philosophy
|
||||||
|
|
||||||
|
mcesptool follows a "detect and adapt" configuration approach rather than requiring explicit setup:
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<Card title="Auto-detection">
|
||||||
|
esptool is found via PATH. ESP-IDF is discovered at common install locations (`~/esp/esp-idf`, `/opt/esp-idf`). QEMU binaries are found in `~/.espressif/tools/`. Serial ports are probed at OS-appropriate paths.
|
||||||
|
</Card>
|
||||||
|
<Card title="Environment overrides">
|
||||||
|
Every auto-detected value can be overridden with an environment variable (`ESPTOOL_PATH`, `ESP_IDF_PATH`, `QEMU_XTENSA_PATH`, etc.). This handles non-standard installations without requiring a configuration file.
|
||||||
|
</Card>
|
||||||
|
<Card title="MCP root augmentation">
|
||||||
|
Project roots from the MCP client's workspace supplement the auto-detected and environment-configured paths. This creates a layered configuration model: defaults, then environment, then runtime context.
|
||||||
|
</Card>
|
||||||
|
<Card title="Graceful degradation">
|
||||||
|
If QEMU binaries are absent, QEMU tools do not register. If ESP-IDF is not found, IDF integration tools are skipped. The server starts with whatever capabilities are available rather than failing because an optional dependency is missing.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
This philosophy reflects the reality that mcesptool's environment varies widely. A developer working on a laptop with a single ESP32 on a USB port has different needs than a CI pipeline programming boards in batch. The configuration system accommodates both without requiring either to write a config file.
|
||||||
|
|
||||||
|
## How LLMs use it
|
||||||
|
|
||||||
|
The interaction pattern between an LLM and mcesptool typically follows a discovery-then-action sequence:
|
||||||
|
|
||||||
|
1. The LLM reads the tool list to understand available capabilities.
|
||||||
|
2. It calls `esp_scan_ports` or `esp_detect_chip` to discover what hardware is present.
|
||||||
|
3. Based on the discovery results, it selects appropriate tools for the user's request.
|
||||||
|
4. It calls tools in sequence, using the output of each call to inform the next.
|
||||||
|
|
||||||
|
For example, a user who says "flash my firmware" triggers the LLM to:
|
||||||
|
1. Scan for connected devices.
|
||||||
|
2. Detect the chip type to confirm compatibility.
|
||||||
|
3. Flash the firmware with the appropriate address and settings.
|
||||||
|
4. Verify the flash contents.
|
||||||
|
|
||||||
|
Each step is a separate tool call. The LLM decides the sequence, handles errors, and communicates results to the user. mcesptool provides the capabilities; the LLM provides the orchestration.
|
||||||
|
|
||||||
|
This division of responsibility is central to the MCP model. The server does not try to be a workflow engine. It offers atomic operations with clear inputs and outputs, and trusts the LLM to compose them into coherent workflows based on the user's intent.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The tool descriptions (docstrings) are written with this LLM-as-orchestrator pattern in mind. They explain not just what a tool does, but when to use it and how it relates to other tools. For example, `esp_qemu_flash`'s description explicitly suggests preferring `esp_flash_firmware` with a socket URI for most use cases.
|
||||||
|
</Aside>
|
||||||
143
docs-site/src/content/docs/explanation/subprocess-pattern.mdx
Normal file
143
docs-site/src/content/docs/explanation/subprocess-pattern.mdx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
---
|
||||||
|
title: The Subprocess Pattern
|
||||||
|
description: Why mcesptool shells out to esptool instead of using it as a library
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Card, CardGrid } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The most consequential design decision in mcesptool is that it never imports esptool as a Python library. Every esptool operation -- chip detection, flash reading, firmware writing, eFuse queries -- runs as an async subprocess. Understanding why requires knowing a bit about esptool's internals and the constraints of a long-running MCP server.
|
||||||
|
|
||||||
|
## The tempting alternative
|
||||||
|
|
||||||
|
esptool is a Python package. You can `pip install esptool` and call its functions directly. At first glance, using it as a library seems like the obvious choice: lower overhead, direct access to data structures, no output parsing. A library call that returns a Python object is cleaner than spawning a process and scraping its stdout.
|
||||||
|
|
||||||
|
So why not do that?
|
||||||
|
|
||||||
|
## Serial port lifecycle
|
||||||
|
|
||||||
|
The core problem is serial port ownership. esptool manages its own serial port lifecycle internally. When you call an esptool function, it opens the serial port, performs the ROM bootloader handshake, optionally uploads the stub flasher, runs the requested operation, and closes the port. This is fine for a CLI tool that runs one command and exits, but it creates friction in a long-running server.
|
||||||
|
|
||||||
|
If mcesptool used esptool as a library, it would face a choice: hold the serial port open between tool calls (risking stale connections when devices are unplugged or reset) or open and close it for every operation (paying the handshake cost each time, just like the subprocess approach, but with less isolation).
|
||||||
|
|
||||||
|
Neither option is great. The subprocess pattern sidesteps the question entirely -- each invocation gets a fresh serial connection, managed by esptool's own well-tested lifecycle code, and the MCP server never holds a file descriptor to a serial device.
|
||||||
|
|
||||||
|
## API stability
|
||||||
|
|
||||||
|
esptool's Python API is internal. The public interface is the command line. Internal functions change between releases, argument signatures shift, and class hierarchies get refactored. The esptool maintainers at Espressif explicitly do not promise backward compatibility for library usage.
|
||||||
|
|
||||||
|
The CLI, by contrast, is the supported interface. Flag names are documented, output format is reasonably stable, and breaking changes are announced in release notes. By targeting the CLI, mcesptool depends on the part of esptool that Espressif actively maintains for external consumption.
|
||||||
|
|
||||||
|
## Process isolation
|
||||||
|
|
||||||
|
A subprocess boundary is a fault boundary. If an esptool operation corrupts memory, segfaults (some serial drivers do), or enters an infinite loop, the subprocess dies and the MCP server continues running. The server catches the timeout or non-zero exit code and returns a clean error to the LLM.
|
||||||
|
|
||||||
|
With library usage, a crash in esptool would crash the entire MCP server process. For a server that might be managing QEMU instances, tracking configuration state, and handling concurrent tool calls, that kind of failure mode is unacceptable.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Process isolation is especially valuable during flash operations, which interact with the USB subsystem through serial drivers. USB-serial adapters occasionally produce kernel-level errors that can destabilize the host process. Subprocess isolation ensures these errors are contained.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## How it works in practice
|
||||||
|
|
||||||
|
Every component has a `_run_esptool` helper method. The implementations vary slightly -- ChipControl's version accepts `connect_attempts` and a `command` string; FlashManager's version takes a flat `args` list -- but they all follow the same core pattern:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _run_esptool(
|
||||||
|
self,
|
||||||
|
port: str,
|
||||||
|
args: list[str],
|
||||||
|
timeout: float = 30.0,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
cmd = [self.config.esptool_path, "--port", port, *args]
|
||||||
|
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await asyncio.wait_for(
|
||||||
|
proc.communicate(), timeout=timeout
|
||||||
|
)
|
||||||
|
output = (stdout or b"").decode() + (stderr or b"").decode()
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
return {"success": False, "error": output.strip()}
|
||||||
|
return {"success": True, "output": output}
|
||||||
|
```
|
||||||
|
|
||||||
|
A few things to note:
|
||||||
|
|
||||||
|
**Fully async.** The subprocess is created with `asyncio.create_subprocess_exec`, and its output is awaited with `asyncio.wait_for`. This means a flash operation that takes 30 seconds does not block the event loop -- other tool calls (like checking QEMU status) can proceed concurrently.
|
||||||
|
|
||||||
|
**Timeout handling.** If the subprocess exceeds the timeout, it gets killed. Flash operations get generous timeouts (120-300 seconds); quick operations like chip detection get 10-15 seconds. The timeout is the server's protection against a hung serial port or unresponsive device.
|
||||||
|
|
||||||
|
**Combined stdout/stderr.** esptool writes some information to stdout and some to stderr. The helper merges them into a single output string for parsing. This is pragmatic -- esptool's output routing is not always consistent across versions.
|
||||||
|
|
||||||
|
**Structured return value.** Every helper returns a dict with at minimum `success` (bool) and either `output` or `error`. This convention makes the callers straightforward: check `success`, then either parse `output` or propagate `error`.
|
||||||
|
|
||||||
|
## Output parsing
|
||||||
|
|
||||||
|
The price of the subprocess approach is that structured data comes back as human-readable text. mcesptool extracts information from esptool's output using regular expressions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@staticmethod
|
||||||
|
def _parse_chip_output(output: str) -> dict[str, Any]:
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
chip_match = re.search(r"Chip is\s+(.+?)(?:\n|$)", output)
|
||||||
|
if chip_match:
|
||||||
|
result["chip_type"] = chip_match.group(1).strip()
|
||||||
|
|
||||||
|
mac_match = re.search(r"MAC:\s*([0-9a-f:]+)", output, re.IGNORECASE)
|
||||||
|
if mac_match:
|
||||||
|
result["mac_address"] = mac_match.group(1)
|
||||||
|
|
||||||
|
flash_size_match = re.search(r"Detected flash size:\s*(\S+)", output)
|
||||||
|
if flash_size_match:
|
||||||
|
result["flash_size"] = flash_size_match.group(1)
|
||||||
|
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
This parsing is intentionally tolerant. Each regex match is independent -- if one field is missing from the output (as happens with different chip types or esptool versions), the others still get extracted. The result dict only includes fields that were actually found.
|
||||||
|
|
||||||
|
Is this fragile? Somewhat. An esptool release that changes its output format could break the regexes. In practice, the key output lines (`Chip is`, `MAC:`, `Detected flash size:`) have been stable across many esptool versions because they are part of the user-facing output that humans and scripts depend on.
|
||||||
|
|
||||||
|
## The socket:// connection
|
||||||
|
|
||||||
|
An elegant side effect of the subprocess pattern is transparent QEMU support. When the Espressif QEMU fork exposes a virtual serial port over TCP, esptool can connect to it using a `socket://localhost:PORT` URI in place of a serial port path.
|
||||||
|
|
||||||
|
Because mcesptool passes the port string directly to esptool's `--port` argument, QEMU virtual devices work identically to physical hardware. The same flash, read, and detect operations work against `socket://localhost:5555` as against `/dev/ttyUSB0`. The server does not need separate code paths for physical vs. virtual devices.
|
||||||
|
|
||||||
|
This would be harder to achieve with library usage, where the serial port abstraction layer would need explicit socket transport support.
|
||||||
|
|
||||||
|
## Trade-offs
|
||||||
|
|
||||||
|
Every design choice has costs. The subprocess pattern is no exception.
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<Card title="Latency overhead">
|
||||||
|
Each tool call pays subprocess spawn cost (a few milliseconds on Linux) plus esptool's own startup time (importing Python modules, initializing the serial driver). For operations like chip detection, this overhead can be a significant fraction of the total time. A library call would skip most of this.
|
||||||
|
</Card>
|
||||||
|
<Card title="No streaming progress">
|
||||||
|
esptool prints progress bars during flash operations. The subprocess pattern captures all output after the process completes, so the MCP server cannot relay real-time progress to the LLM during a long flash. A library integration could potentially feed progress updates to MCP's progress reporting mechanism.
|
||||||
|
</Card>
|
||||||
|
<Card title="Duplicated helpers">
|
||||||
|
Each component has its own `_run_esptool` method with slightly different signatures. This is mild code duplication. A shared base class or utility function could consolidate these, but the current approach keeps each component fully self-contained, which is useful for understanding and testing individual components in isolation.
|
||||||
|
</Card>
|
||||||
|
<Card title="Output parsing brittleness">
|
||||||
|
Regex-based parsing of human-readable output is inherently less reliable than working with typed data structures. The regexes are tested against current esptool versions, but a major output format change would require updates.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
## Why the trade-offs are worth it
|
||||||
|
|
||||||
|
For a long-running MCP server that interacts with hardware, the subprocess pattern's benefits outweigh its costs:
|
||||||
|
|
||||||
|
- **Robustness** over performance. A crashed subprocess is a returned error, not a dead server. This matters when you are connected to a production device and the USB cable gets bumped.
|
||||||
|
- **Compatibility** over elegance. Targeting the CLI means mcesptool works with any esptool version that supports the required commands. No version-pinning of internal APIs.
|
||||||
|
- **Simplicity** over sophistication. Each tool call is a single function: build a command, run it, parse the output. There is no connection pool, no state machine, no retry-at-the-library-level complexity.
|
||||||
|
|
||||||
|
The 10-millisecond subprocess overhead is invisible compared to the 500ms+ that a serial bootloader handshake takes. The real bottleneck is always the hardware.
|
||||||
157
docs-site/src/content/docs/guides/diagnostics.mdx
Normal file
157
docs-site/src/content/docs/guides/diagnostics.mdx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
title: Diagnostics and Profiling
|
||||||
|
description: Memory dumps, performance profiling, and diagnostic reports
|
||||||
|
sidebar:
|
||||||
|
order: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
When a device is not behaving as expected, you need data -- chip identity, flash status, memory contents, and transport performance. mcesptool provides three diagnostic tools that gather this information without modifying anything on the device.
|
||||||
|
|
||||||
|
## Quick device overview
|
||||||
|
|
||||||
|
The `esp_diagnostic_report` tool collects chip identity, MAC address, and flash information in a single call.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_diagnostic_report({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The response includes:
|
||||||
|
|
||||||
|
- `chip`: the detected chip type (e.g., "ESP32-D0WDQ6")
|
||||||
|
- `chip_id`: the hardware chip ID
|
||||||
|
- `mac_address`: the factory-programmed MAC
|
||||||
|
- `flash.manufacturer`: the SPI flash manufacturer ID
|
||||||
|
- `flash.device`: the flash device ID
|
||||||
|
- `flash.size`: detected flash size (e.g., "4MB")
|
||||||
|
|
||||||
|
### Include a memory snapshot
|
||||||
|
|
||||||
|
Pass `include_memory: true` to add a 256-byte hex dump from address `0x0`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_diagnostic_report({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"include_memory": true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `memory_dump_0x0` field contains the formatted hex output. This is useful for confirming whether the bootloader region has been written or is still erased (`0xFF`).
|
||||||
|
|
||||||
|
## Inspect memory regions
|
||||||
|
|
||||||
|
The `esp_memory_dump` tool reads arbitrary memory addresses and returns a formatted hex dump with ASCII interpretation.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_memory_dump({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x0",
|
||||||
|
"size": "256B"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Useful memory regions
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Bootloader">
|
||||||
|
```json
|
||||||
|
esp_memory_dump({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x1000",
|
||||||
|
"size": "256B"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
On ESP32, the second-stage bootloader starts at `0x1000`. The first bytes should contain the image header if firmware has been flashed.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Peripheral registers">
|
||||||
|
```json
|
||||||
|
esp_memory_dump({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x3FF00000",
|
||||||
|
"size": "64B"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
The `0x3FF00000` region contains peripheral register space on ESP32. Reading here can help verify hardware configuration state.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Partition table">
|
||||||
|
```json
|
||||||
|
esp_memory_dump({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x8000",
|
||||||
|
"size": "1KB"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
The partition table lives at `0x8000`. Valid entries start with the magic bytes `AA 50`. An all-`FF` dump means no partition table has been written.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Reading hex dump output
|
||||||
|
|
||||||
|
The hex dump format shows 16 bytes per line with three columns:
|
||||||
|
|
||||||
|
```
|
||||||
|
0x00001000: e9 09 02 20 08 10 40 00 ee 00 00 00 05 00 04 00 ... ..@.........
|
||||||
|
0x00001010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Left column**: memory address
|
||||||
|
- **Middle column**: hex bytes (16 per line)
|
||||||
|
- **Right column**: ASCII interpretation (`.` for non-printable bytes)
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
The maximum dump size is 1MB. For larger flash reads, use `esp_flash_read` instead -- it writes directly to a file and handles larger regions more efficiently.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Performance profiling
|
||||||
|
|
||||||
|
The `esp_performance_profile` tool measures serial transport speed by timing a series of esptool operations. This is useful for comparing physical serial performance against QEMU, or for diagnosing slow connections.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_performance_profile({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The profiler runs four operations and reports individual timings:
|
||||||
|
|
||||||
|
| Operation | What it measures |
|
||||||
|
|-------------------|-------------------------------------------------|
|
||||||
|
| `chip-id` | Lightweight command round-trip latency |
|
||||||
|
| `flash-id` | SPI flash register read latency |
|
||||||
|
| `read-mac` | eFuse read latency |
|
||||||
|
| `read-flash (4KB)`| Flash read throughput (bytes/second calculated) |
|
||||||
|
|
||||||
|
The `summary` section reports average latency across all successful operations and the total operations tested.
|
||||||
|
|
||||||
|
### Compare physical vs. QEMU performance
|
||||||
|
|
||||||
|
Run the profiler against both a physical device and a QEMU instance to quantify the performance difference:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Profile the physical device:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_performance_profile({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Profile a QEMU instance:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_performance_profile({
|
||||||
|
"port": "socket://localhost:5555"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Compare the `average_latency_seconds` and `throughput_bytes_per_sec` values from each run.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
QEMU typically shows lower latency for metadata operations (no real serial transport) but similar or slower throughput for bulk reads due to emulation overhead.
|
||||||
|
|
||||||
|
See the [Diagnostics reference](/reference/diagnostics/) for full parameter details.
|
||||||
126
docs-site/src/content/docs/guides/flash-multi-file.mdx
Normal file
126
docs-site/src/content/docs/guides/flash-multi-file.mdx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
title: Flash Multiple Binaries
|
||||||
|
description: Flash bootloader, partition table, and application in one operation
|
||||||
|
sidebar:
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
A typical ESP-IDF build produces three separate binaries that must land at specific flash addresses: the bootloader, the partition table, and your application. Using `esp_flash_multi`, you can write all three in a single esptool invocation -- one connection, one flash cycle, faster than flashing each file individually.
|
||||||
|
|
||||||
|
## Standard address layout
|
||||||
|
|
||||||
|
Every ESP chip family uses a slightly different bootloader offset. The partition table and application offsets remain consistent across most configurations.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="ESP32">
|
||||||
|
| Binary | Address |
|
||||||
|
|-----------------|------------|
|
||||||
|
| Bootloader | `0x1000` |
|
||||||
|
| Partition table | `0x8000` |
|
||||||
|
| Application | `0x10000` |
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="ESP32-S3">
|
||||||
|
| Binary | Address |
|
||||||
|
|-----------------|------------|
|
||||||
|
| Bootloader | `0x0` |
|
||||||
|
| Partition table | `0x8000` |
|
||||||
|
| Application | `0x10000` |
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="ESP32-C3">
|
||||||
|
| Binary | Address |
|
||||||
|
|-----------------|------------|
|
||||||
|
| Bootloader | `0x0` |
|
||||||
|
| Partition table | `0x8000` |
|
||||||
|
| Application | `0x10000` |
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
ESP32 (original) uses `0x1000` for its bootloader. The ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, and ESP32-H2 all use `0x0`. Check your `sdkconfig` or build output if you have a non-default configuration.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Flash all three binaries at once
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Locate the build artifacts from your ESP-IDF or PlatformIO build. For ESP-IDF, they are typically in `build/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
build/bootloader/bootloader.bin
|
||||||
|
build/partition_table/partition-table.bin
|
||||||
|
build/my_app.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Call `esp_flash_multi` with the files array. Each entry needs an `address` and a `path`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_flash_multi({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"files": [
|
||||||
|
{ "address": "0x1000", "path": "build/bootloader/bootloader.bin" },
|
||||||
|
{ "address": "0x8000", "path": "build/partition_table/partition-table.bin" },
|
||||||
|
{ "address": "0x10000", "path": "build/my_app.bin" }
|
||||||
|
],
|
||||||
|
"verify": true,
|
||||||
|
"compress": true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. The tool connects once, writes all three binaries with compression enabled, then verifies each region. The response includes the total bytes written and per-file sizes.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Addresses in the `files` array must not overlap. If two binaries are written to overlapping regions, the later write silently overwrites the earlier one.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Add a filesystem image
|
||||||
|
|
||||||
|
If your project includes a SPIFFS or LittleFS partition, add it to the same flash operation:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_flash_multi({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"files": [
|
||||||
|
{ "address": "0x1000", "path": "build/bootloader/bootloader.bin" },
|
||||||
|
{ "address": "0x8000", "path": "build/partition_table/partition-table.bin" },
|
||||||
|
{ "address": "0x10000", "path": "build/my_app.bin" },
|
||||||
|
{ "address": "0x290000", "path": "build/storage.bin" }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The filesystem address must match the offset defined in your partition table. Use `esp_partition_analyze` to read the current table from a device, or check your `partitions.csv`.
|
||||||
|
|
||||||
|
## Verify after flashing
|
||||||
|
|
||||||
|
If you need to confirm the flash contents without re-flashing, use `esp_verify_flash` for each binary:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_verify_flash({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_path": "build/my_app.bin",
|
||||||
|
"address": "0x10000"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool reads flash at the specified address and compares byte-for-byte against the file. A `verified: true` response confirms the contents match.
|
||||||
|
|
||||||
|
## Use with QEMU
|
||||||
|
|
||||||
|
The same `esp_flash_multi` call works against a QEMU virtual device. Pass the socket URI instead of a serial port:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_flash_multi({
|
||||||
|
"port": "socket://localhost:5555",
|
||||||
|
"files": [
|
||||||
|
{ "address": "0x0", "path": "build/bootloader/bootloader.bin" },
|
||||||
|
{ "address": "0x8000", "path": "build/partition_table/partition-table.bin" },
|
||||||
|
{ "address": "0x10000", "path": "build/my_app.bin" }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Flash Operations reference](/reference/flash-operations/) for full parameter details.
|
||||||
195
docs-site/src/content/docs/guides/partition-layouts.mdx
Normal file
195
docs-site/src/content/docs/guides/partition-layouts.mdx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
---
|
||||||
|
title: Custom Partition Layouts
|
||||||
|
description: Design and flash custom partition tables
|
||||||
|
sidebar:
|
||||||
|
order: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, Tabs, TabItem, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The partition table defines how the ESP's flash memory is divided. The default layout works for simple applications, but OTA, large NVS storage, or filesystem partitions require a custom table. mcesptool provides tools to generate standard OTA layouts, define fully custom layouts, and analyze what is already on a device.
|
||||||
|
|
||||||
|
## Default OTA layout
|
||||||
|
|
||||||
|
The fastest way to get an OTA-capable partition table is `esp_partition_create_ota`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_partition_create_ota({
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"app_size": "1MB"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This generates:
|
||||||
|
|
||||||
|
| Partition | Type | Subtype | Offset | Size |
|
||||||
|
|-----------|------|---------|----------|--------|
|
||||||
|
| nvs | data | nvs | 0x9000 | 24KB |
|
||||||
|
| otadata | data | ota | 0xf000 | 8KB |
|
||||||
|
| phy_init | data | phy | 0x11000 | 4KB |
|
||||||
|
| ota_0 | app | ota_0 | 0x12000 | 1MB |
|
||||||
|
| ota_1 | app | ota_1 | 0x112000 | 1MB |
|
||||||
|
| storage | data | spiffs | 0x212000 | (remaining) |
|
||||||
|
|
||||||
|
Any space left after the two OTA slots is allocated to a SPIFFS storage partition.
|
||||||
|
|
||||||
|
## Custom layouts
|
||||||
|
|
||||||
|
For layouts that do not fit the OTA template, use `esp_partition_custom` with a partition configuration dict.
|
||||||
|
|
||||||
|
### Factory-only with large NVS
|
||||||
|
|
||||||
|
No OTA, but a large NVS partition for configuration storage:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_partition_custom({
|
||||||
|
"partition_config": {
|
||||||
|
"partitions": [
|
||||||
|
{ "name": "nvs", "type": "data", "subtype": "nvs", "size": "64K" },
|
||||||
|
{ "name": "phy_init","type": "data", "subtype": "phy", "size": "4K" },
|
||||||
|
{ "name": "factory", "type": "app", "subtype": "factory", "size": "2MB" },
|
||||||
|
{ "name": "storage", "type": "data", "subtype": "spiffs", "size": "1MB" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### OTA with LittleFS storage
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_partition_custom({
|
||||||
|
"partition_config": {
|
||||||
|
"partitions": [
|
||||||
|
{ "name": "nvs", "type": "data", "subtype": "nvs", "size": "24K" },
|
||||||
|
{ "name": "otadata", "type": "data", "subtype": "ota", "size": "8K" },
|
||||||
|
{ "name": "phy_init", "type": "data", "subtype": "phy", "size": "4K" },
|
||||||
|
{ "name": "ota_0", "type": "app", "subtype": "ota_0", "size": "1536K" },
|
||||||
|
{ "name": "ota_1", "type": "app", "subtype": "ota_1", "size": "1536K" },
|
||||||
|
{ "name": "littlefs", "type": "data", "subtype": "littlefs", "size": "512K" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dual-app with coredump
|
||||||
|
|
||||||
|
A factory app plus one OTA slot, with a coredump partition for crash analysis:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_partition_custom({
|
||||||
|
"partition_config": {
|
||||||
|
"partitions": [
|
||||||
|
{ "name": "nvs", "type": "data", "subtype": "nvs", "size": "16K" },
|
||||||
|
{ "name": "otadata", "type": "data", "subtype": "ota", "size": "8K" },
|
||||||
|
{ "name": "phy_init", "type": "data", "subtype": "phy", "size": "4K" },
|
||||||
|
{ "name": "factory", "type": "app", "subtype": "factory", "size": "1MB" },
|
||||||
|
{ "name": "ota_0", "type": "app", "subtype": "ota_0", "size": "1MB" },
|
||||||
|
{ "name": "coredump", "type": "data", "subtype": "coredump", "size": "64K" },
|
||||||
|
{ "name": "storage", "type": "data", "subtype": "fat", "size": "512K" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Valid partition types and subtypes
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="App subtypes">
|
||||||
|
| Subtype | Value | Description |
|
||||||
|
|------------|--------|---------------------------------|
|
||||||
|
| `factory` | `0x00` | Factory application (default) |
|
||||||
|
| `ota_0` | `0x10` | OTA slot 0 |
|
||||||
|
| `ota_1` | `0x11` | OTA slot 1 |
|
||||||
|
| `ota_2` | `0x12` | OTA slot 2 |
|
||||||
|
| `ota_3` | `0x13` | OTA slot 3 |
|
||||||
|
| `test` | `0x20` | Test application |
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Data subtypes">
|
||||||
|
| Subtype | Value | Description |
|
||||||
|
|------------|--------|---------------------------------|
|
||||||
|
| `ota` | `0x00` | OTA selection data |
|
||||||
|
| `phy` | `0x01` | PHY calibration data |
|
||||||
|
| `nvs` | `0x02` | Non-volatile storage |
|
||||||
|
| `coredump` | `0x03` | Core dump storage |
|
||||||
|
| `nvs_keys` | `0x04` | NVS encryption keys |
|
||||||
|
| `efuse` | `0x05` | eFuse emulation data |
|
||||||
|
| `fat` | `0x81` | FAT filesystem |
|
||||||
|
| `spiffs` | `0x82` | SPIFFS filesystem |
|
||||||
|
| `littlefs` | `0x83` | LittleFS filesystem |
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Alignment requirements
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
App partitions (type `app`) must be aligned to 64KB (0x10000) boundaries. The `esp_partition_custom` tool auto-aligns app partitions by rounding up the offset. Data partitions have no alignment requirement beyond 4KB sector alignment.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
If you provide explicit offsets in your partition entries, make sure app partitions fall on 64KB boundaries. The tool warns when auto-alignment shifts a partition's offset.
|
||||||
|
|
||||||
|
## Auto-calculated offsets
|
||||||
|
|
||||||
|
When you omit the `offset` field from a partition entry, the tool calculates it automatically. Partitions are laid out sequentially starting after the partition table region (offset `0x9000`). Each partition starts immediately after the previous one ends, with app partitions rounded up to the next 64KB boundary.
|
||||||
|
|
||||||
|
To override auto-calculation for a specific entry, include an `"offset"` field:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "storage", "type": "data", "subtype": "spiffs", "size": "1MB", "offset": "0x300000" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convert CSV to binary
|
||||||
|
|
||||||
|
Both `esp_partition_create_ota` and `esp_partition_custom` return a `partition_csv` field containing the generated CSV. To flash it to a device, you first need to convert it to the binary partition table format.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Save the CSV content to a file (e.g., `partitions.csv`).
|
||||||
|
|
||||||
|
2. Convert using ESP-IDF's partition generator:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python $IDF_PATH/components/partition_table/gen_esp32part.py \
|
||||||
|
partitions.csv partitions.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Flash the binary to the partition table offset:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_flash_firmware({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_path": "partitions.bin",
|
||||||
|
"address": "0x8000"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Analyze existing partition tables
|
||||||
|
|
||||||
|
To read and parse the partition table from a connected device:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_partition_analyze({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The response lists every partition with its name, type, subtype, offset, size, and whether the encrypted flag is set. If the flash is blank (all `0xFF` at the partition table offset), the response returns an empty partitions array.
|
||||||
|
|
||||||
|
This is useful for:
|
||||||
|
|
||||||
|
- Verifying a partition table was flashed correctly
|
||||||
|
- Inspecting an unknown device to understand its layout
|
||||||
|
- Finding the exact offset of a specific partition (e.g., otadata for manual rollback)
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Partition Management Reference"
|
||||||
|
description="Full parameter details for esp_partition_create_ota, esp_partition_custom, and esp_partition_analyze."
|
||||||
|
href="/reference/partition-management/"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Set Up OTA Updates"
|
||||||
|
description="Use the OTA partition layout to enable over-the-air firmware updates."
|
||||||
|
href="/guides/setup-ota/"
|
||||||
|
/>
|
||||||
156
docs-site/src/content/docs/guides/production-programming.mdx
Normal file
156
docs-site/src/content/docs/guides/production-programming.mdx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
title: Production Programming
|
||||||
|
description: Factory programming and batch operations for production lines
|
||||||
|
sidebar:
|
||||||
|
order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Production programming differs from development flashing in one key way: repeatability. Every device must receive identical firmware, be verified, and pass quality checks before shipping. mcesptool provides tools for single-device factory programming, parallel batch operations across multiple ports, and automated quality control.
|
||||||
|
|
||||||
|
## Single device factory programming
|
||||||
|
|
||||||
|
The `esp_factory_program` tool runs a complete erase-flash-verify cycle in sequence. It accepts a configuration dict that describes the full firmware stack.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_factory_program({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"program_config": {
|
||||||
|
"firmware_path": "/firmware/v2.1/app.bin",
|
||||||
|
"address": "0x10000",
|
||||||
|
"bootloader": "/firmware/v2.1/bootloader.bin",
|
||||||
|
"bootloader_address": "0x1000",
|
||||||
|
"partition_table": "/firmware/v2.1/partitions.bin",
|
||||||
|
"partition_table_address": "0x8000",
|
||||||
|
"erase_before": true,
|
||||||
|
"verify": true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool executes these steps in order:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Erase** the entire flash (when `erase_before` is true).
|
||||||
|
|
||||||
|
2. **Flash the bootloader** at the specified address.
|
||||||
|
|
||||||
|
3. **Flash the partition table** at the specified address.
|
||||||
|
|
||||||
|
4. **Flash the application** with verification enabled.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
The response includes a `steps` array showing the result of each stage. If any step fails, the operation stops and reports which step failed and why.
|
||||||
|
|
||||||
|
### Minimal configuration
|
||||||
|
|
||||||
|
If your build produces a single merged binary (bootloader + partitions + app already combined), the config is simpler:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_factory_program({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"program_config": {
|
||||||
|
"firmware_path": "/firmware/v2.1/merged.bin",
|
||||||
|
"address": "0x0",
|
||||||
|
"erase_before": true,
|
||||||
|
"verify": true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch programming
|
||||||
|
|
||||||
|
When programming multiple devices simultaneously, `esp_batch_program` runs factory programming in parallel across a list of serial ports.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_batch_program({
|
||||||
|
"device_list": [
|
||||||
|
"/dev/ttyUSB0",
|
||||||
|
"/dev/ttyUSB1",
|
||||||
|
"/dev/ttyUSB2",
|
||||||
|
"/dev/ttyUSB3"
|
||||||
|
],
|
||||||
|
"firmware_path": "/firmware/v2.1/merged.bin"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Each device gets the same treatment: full erase, flash at `0x0`, and verification. The operations run concurrently using asyncio, so a four-device batch completes in roughly the same time as a single device.
|
||||||
|
|
||||||
|
The response reports:
|
||||||
|
|
||||||
|
- `total_devices`: How many ports were attempted
|
||||||
|
- `succeeded` / `failed`: Pass/fail counts
|
||||||
|
- `devices`: Per-device results with individual timing and error details
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Batch programming assumes all devices are the same chip type and should receive the same firmware. If a port fails (bad connection, wrong chip, etc.), the other devices continue -- the failed device is reported in the results without blocking the batch.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Quality control checks
|
||||||
|
|
||||||
|
After programming, run `esp_quality_control` to verify each device is functioning correctly.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Basic suite">
|
||||||
|
The basic test suite runs three fast checks:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_quality_control({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"test_suite": "basic"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests performed:
|
||||||
|
- **Chip identification** -- confirms the chip responds and reports its type
|
||||||
|
- **Flash identification** -- reads flash manufacturer ID and detected size
|
||||||
|
- **MAC address** -- reads the factory-programmed MAC
|
||||||
|
|
||||||
|
Typical execution time: under 5 seconds.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Extended suite">
|
||||||
|
The extended suite adds flash read verification:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_quality_control({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"test_suite": "extended"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional tests:
|
||||||
|
- **Flash read (4KB)** -- reads the first 4KB of flash to verify read connectivity
|
||||||
|
- **Data check** -- confirms flash contains data (not all `0xFF` / erased)
|
||||||
|
|
||||||
|
Typical execution time: 10-15 seconds.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
The response includes a `verdict` of `"PASS"` or `"FAIL"`, with individual test results in the `tests` array. Each test entry includes its own `success` flag and any error details.
|
||||||
|
|
||||||
|
## Production workflow
|
||||||
|
|
||||||
|
A typical production line sequence:
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Connect the device to the programming jig.
|
||||||
|
|
||||||
|
2. Run `esp_factory_program` with the full firmware stack.
|
||||||
|
|
||||||
|
3. Run `esp_quality_control` with the `"extended"` test suite.
|
||||||
|
|
||||||
|
4. If the verdict is `"PASS"`, mark the device as good and record its MAC address.
|
||||||
|
|
||||||
|
5. If `"FAIL"`, flag the device for inspection. The test results indicate which check failed.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
For high-volume lines, use `esp_batch_program` with a multi-port USB hub to program 4-8 devices simultaneously, then run quality control on each device individually to get per-device MAC addresses and test results.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
See the [Production Tools reference](/reference/production-tools/) for full parameter details.
|
||||||
121
docs-site/src/content/docs/guides/ram-loading.mdx
Normal file
121
docs-site/src/content/docs/guides/ram-loading.mdx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: RAM Loading for Development
|
||||||
|
description: Rapid development iteration without flash wear
|
||||||
|
sidebar:
|
||||||
|
order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Flash memory on ESP devices has a limited write endurance -- typically 100,000 cycles. During active development, flashing dozens of times per day adds up. RAM loading bypasses flash entirely: you convert your ELF to a RAM binary, load it directly into the device's SRAM, and it executes immediately. No flash wear, no erase cycles, faster iteration.
|
||||||
|
|
||||||
|
## When to use RAM loading
|
||||||
|
|
||||||
|
RAM loading is a good fit when:
|
||||||
|
|
||||||
|
- You are iterating rapidly on application logic and want sub-second deploy cycles.
|
||||||
|
- You want to preserve the existing flash contents (e.g., NVS data, calibration values).
|
||||||
|
- You are testing on hardware that has already been through many flash cycles.
|
||||||
|
|
||||||
|
It is **not** a good fit when:
|
||||||
|
|
||||||
|
- Your application depends on flash-resident features: NVS, SPIFFS, LittleFS, or OTA partitions.
|
||||||
|
- Secure boot is enabled (RAM loading requires `CONFIG_SECURE_BOOT=n`).
|
||||||
|
- Your binary is too large to fit in available SRAM.
|
||||||
|
|
||||||
|
## The edit-build-load-test cycle
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Build your project** with ESP-IDF as usual. The build produces an ELF file (typically `build/<project>.elf`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idf.py build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Convert the ELF to a RAM binary** using `esp_elf_to_ram_binary`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_elf_to_ram_binary({
|
||||||
|
"elf_path": "build/my_project.elf",
|
||||||
|
"chip": "esp32"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces a binary with RAM segments (IRAM/DRAM) placed first, suitable for direct loading. The output defaults to `build/my_project-ram.bin`.
|
||||||
|
|
||||||
|
3. **Load to the device** with `esp_load_ram`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_load_ram({
|
||||||
|
"binary_path": "build/my_project-ram.bin",
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary transfers to RAM and begins executing immediately.
|
||||||
|
|
||||||
|
4. **Capture output** with `esp_serial_monitor`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_serial_monitor({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"duration_seconds": 10,
|
||||||
|
"reset_on_connect": false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `reset_on_connect` to `false` -- resetting the device would stop the RAM-loaded code.
|
||||||
|
|
||||||
|
5. **Iterate**: edit your source, rebuild, convert, load again. Steps 2-4 typically complete in a few seconds.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## sdkconfig requirements
|
||||||
|
|
||||||
|
Your project must be configured to produce binaries compatible with RAM execution.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The ELF must NOT have an embedded SHA256 digest at the reserved offset. Disable the following in your `sdkconfig`:
|
||||||
|
|
||||||
|
```
|
||||||
|
CONFIG_SECURE_BOOT=n
|
||||||
|
CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT=n
|
||||||
|
```
|
||||||
|
|
||||||
|
If your build uses PlatformIO, check equivalent settings in `platformio.ini`. Some default board configurations enable signing that is incompatible with RAM loading.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Specify an output path
|
||||||
|
|
||||||
|
By default, `esp_elf_to_ram_binary` writes the output next to the ELF file with a `-ram.bin` suffix. To control the output location:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_elf_to_ram_binary({
|
||||||
|
"elf_path": "build/my_project.elf",
|
||||||
|
"output_path": "/tmp/test-ram.bin",
|
||||||
|
"chip": "auto"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Setting `chip` to `"auto"` lets esptool detect the target from the ELF metadata. Specify `"esp32"`, `"esp32s3"`, or `"esp32c3"` explicitly if auto-detection fails.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- **No flash-dependent features**: NVS, SPIFFS, LittleFS, and OTA all read from flash. Code that calls these APIs will fail or return errors when running from RAM.
|
||||||
|
- **No persistence**: the loaded code runs until the device is reset. A hardware reset or power cycle returns the device to whatever firmware is in flash (or the ROM bootloader if flash is blank).
|
||||||
|
- **SRAM size limits**: ESP32 has roughly 520KB of SRAM. Large applications or those with heavy static allocations may not fit. The `esp_elf_to_ram_binary` tool reports the output file size -- compare this against your chip's available RAM.
|
||||||
|
- **Cannot stop remotely**: once loaded, execution continues until a physical reset. There is no software-only stop mechanism through mcesptool.
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Firmware Builder Reference"
|
||||||
|
description="Full parameter details for esp_elf_to_ram_binary and esp_elf_to_binary."
|
||||||
|
href="/reference/firmware-builder/"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Chip Control Reference"
|
||||||
|
description="Details for esp_load_ram and esp_serial_monitor."
|
||||||
|
href="/reference/chip-control/"
|
||||||
|
/>
|
||||||
172
docs-site/src/content/docs/guides/secure-boot.mdx
Normal file
172
docs-site/src/content/docs/guides/secure-boot.mdx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
title: Secure Boot and Flash Encryption
|
||||||
|
description: Enable hardware security features on ESP devices
|
||||||
|
sidebar:
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
ESP32 chips have hardware-level security features controlled by eFuses -- one-time-programmable bits burned into silicon. Once burned, they cannot be reversed. This guide covers assessing a device's current security posture, enabling flash encryption, and understanding the secure boot workflow.
|
||||||
|
|
||||||
|
<Aside type="danger">
|
||||||
|
eFuse operations are **permanent and irreversible** on real hardware. A misconfigured eFuse burn can brick a device with no recovery path. Always validate your workflow on QEMU first, where eFuses reset when you recreate the instance.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Assess current security posture
|
||||||
|
|
||||||
|
Before changing anything, audit the device to understand what is already configured.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_security_audit({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The audit returns a structured report including:
|
||||||
|
|
||||||
|
- **Chip identity** and chip ID
|
||||||
|
- **Security posture** summary: flash encryption (enabled/disabled), secure boot (enabled/disabled), JTAG (enabled/disabled)
|
||||||
|
- **Security-relevant eFuses**: `FLASH_CRYPT_CNT`, `ABS_DONE_0`, `JTAG_DISABLE`, and others
|
||||||
|
|
||||||
|
If the `posture` section shows everything as "disabled", the device is in its factory default state with no security features active.
|
||||||
|
|
||||||
|
## Read individual eFuses
|
||||||
|
|
||||||
|
To inspect a specific eFuse value:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_efuse_read({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"efuse_name": "FLASH_CRYPT_CNT"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Omit `efuse_name` to get the full eFuse summary.
|
||||||
|
|
||||||
|
## Enable flash encryption
|
||||||
|
|
||||||
|
Flash encryption prevents reading firmware from the flash chip. The process involves checking the current state, then burning the appropriate eFuses.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Check current flash encryption status:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_enable_flash_encryption({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If encryption is already enabled, the response reports the current state and no further action is needed. If disabled, it returns guidance on the required eFuse burns.
|
||||||
|
|
||||||
|
2. On a QEMU instance (for testing), burn the encryption eFuses:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_efuse_burn({
|
||||||
|
"port": "socket://localhost:5555",
|
||||||
|
"efuse_name": "FLASH_CRYPT_CNT",
|
||||||
|
"value": "0x1"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Verify the change took effect:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_efuse_read({
|
||||||
|
"port": "socket://localhost:5555",
|
||||||
|
"efuse_name": "FLASH_CRYPT_CNT"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The response shows `value_before` and `value_after` to confirm the burn.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="danger">
|
||||||
|
On real hardware, `FLASH_CRYPT_CNT` is a one-time field. Once all bits are burned, flash encryption is permanently enabled and the device can only boot encrypted firmware. There is no way to disable it.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Secure boot workflow
|
||||||
|
|
||||||
|
Secure boot ensures only signed firmware can execute on the device. It is controlled by the `ABS_DONE_0` eFuse.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Run a security audit to confirm secure boot is not already enabled:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_security_audit({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for `secure_boot: "disabled"` in the posture section.
|
||||||
|
|
||||||
|
2. Build your firmware with secure boot enabled in the ESP-IDF menuconfig. This generates signing keys and embeds the public key in the bootloader.
|
||||||
|
|
||||||
|
3. Flash the signed bootloader, partition table, and application using `esp_flash_multi`.
|
||||||
|
|
||||||
|
4. Burn the secure boot eFuse to lock it in:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_efuse_burn({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"efuse_name": "ABS_DONE_0",
|
||||||
|
"value": "0x1"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Optionally disable JTAG to prevent debug-port attacks:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_efuse_burn({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"efuse_name": "JTAG_DISABLE",
|
||||||
|
"value": "0x1"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Common security eFuses
|
||||||
|
|
||||||
|
| eFuse Name | Purpose | Reversible |
|
||||||
|
|-----------------------|----------------------------------------------|------------|
|
||||||
|
| `FLASH_CRYPT_CNT` | Controls flash encryption enable/disable | No |
|
||||||
|
| `FLASH_CRYPT_CONFIG` | Encryption configuration bits | No |
|
||||||
|
| `ABS_DONE_0` | Enables secure boot v1 | No |
|
||||||
|
| `ABS_DONE_1` | Enables secure boot v2 (ESP32-V3+) | No |
|
||||||
|
| `JTAG_DISABLE` | Disables JTAG debug interface | No |
|
||||||
|
| `DISABLE_DL_ENCRYPT` | Disables flash encryption in download mode | No |
|
||||||
|
| `DISABLE_DL_DECRYPT` | Disables flash decryption in download mode | No |
|
||||||
|
| `DISABLE_DL_CACHE` | Disables flash cache in download mode | No |
|
||||||
|
|
||||||
|
## Test on QEMU first
|
||||||
|
|
||||||
|
QEMU emulates eFuses in a file on disk. When you destroy and recreate a QEMU instance, the eFuse file is regenerated from defaults -- effectively giving you a fresh, unburnished chip.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Start a QEMU instance:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_qemu_start({
|
||||||
|
"chip_type": "esp32",
|
||||||
|
"boot_mode": "download"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run your entire security configuration workflow against the socket URI.
|
||||||
|
|
||||||
|
3. Verify with `esp_security_audit` that the posture matches expectations.
|
||||||
|
|
||||||
|
4. Stop and delete the instance. Start a fresh one to repeat the test or try a different configuration.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
If you burn an eFuse on QEMU and want to start over, just stop the instance with `esp_qemu_stop` and start a new one. The new instance gets fresh eFuse defaults.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
See the [Security reference](/reference/security/) for full parameter details on `esp_security_audit`, `esp_efuse_read`, `esp_efuse_burn`, and `esp_enable_flash_encryption`.
|
||||||
132
docs-site/src/content/docs/guides/setup-ota.mdx
Normal file
132
docs-site/src/content/docs/guides/setup-ota.mdx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
---
|
||||||
|
title: Set Up OTA Updates
|
||||||
|
description: Configure over-the-air update pipeline for ESP devices
|
||||||
|
sidebar:
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Over-the-air updates let you deploy new firmware to ESP devices over WiFi without physical access to the serial port. This guide walks through creating the partition layout, building an OTA package, deploying it, and recovering with rollback if something goes wrong.
|
||||||
|
|
||||||
|
## Create an OTA partition layout
|
||||||
|
|
||||||
|
OTA requires at least two application slots (ota_0 and ota_1) plus an otadata partition that tracks which slot is active. The `esp_partition_create_ota` tool generates this layout automatically.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Generate the partition table CSV:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_partition_create_ota({
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"app_size": "1MB"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces a table with `nvs`, `otadata`, `phy_init`, `ota_0`, `ota_1`, and a `storage` partition using whatever space remains.
|
||||||
|
|
||||||
|
2. Convert the CSV to binary using ESP-IDF's partition tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python $IDF_PATH/components/partition_table/gen_esp32part.py \
|
||||||
|
partitions.csv partitions.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Flash the partition table to the device:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_flash_firmware({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_path": "partitions.bin",
|
||||||
|
"address": "0x8000"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
If your application binary exceeds 1MB, increase `app_size` accordingly. Both OTA slots must be the same size, so doubling the slot size reduces available storage space.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Build an OTA package
|
||||||
|
|
||||||
|
The `esp_ota_package_create` tool bundles your firmware binary with a manifest containing version, SHA-256 hash, and timestamp into a zip archive.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_ota_package_create({
|
||||||
|
"firmware_path": "build/my_app.bin",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"output_path": "releases/my_app-1.2.0-ota.zip"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The response includes the manifest contents and package size. The SHA-256 hash in the manifest can be used by the device to verify the firmware before applying it.
|
||||||
|
|
||||||
|
## Deploy to a device
|
||||||
|
|
||||||
|
Your device must be running an HTTP OTA server -- either the ESP-IDF `esp_https_ota` component or a custom HTTP handler that accepts firmware binary data via POST.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. Ensure the device is connected to the network and its OTA endpoint is reachable.
|
||||||
|
|
||||||
|
2. Deploy the package:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_ota_deploy({
|
||||||
|
"package_path": "releases/my_app-1.2.0-ota.zip",
|
||||||
|
"target_url": "http://192.168.1.100/ota/update"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. The tool extracts `firmware.bin` from the zip and POSTs it to the target URL. A `2xx` HTTP response indicates the device accepted the update.
|
||||||
|
|
||||||
|
4. The device typically reboots into the new firmware automatically. Monitor the serial output to confirm:
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_serial_monitor({
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"duration_seconds": 10,
|
||||||
|
"reset_on_connect": false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Roll back to previous firmware
|
||||||
|
|
||||||
|
If the new firmware is broken or the device becomes unresponsive over the network, you can force a rollback via the serial port.
|
||||||
|
|
||||||
|
```json
|
||||||
|
esp_ota_rollback({
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Rollback works by erasing the `otadata` partition. When otadata is blank (all `0xFF`), the bootloader falls back to the `ota_0` slot -- the first firmware that was flashed. This means rollback always returns to the ota_0 image, not the "previous" version.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### How rollback works internally
|
||||||
|
|
||||||
|
The tool reads the partition table from the device to locate the otadata partition, then erases that region. On the next boot:
|
||||||
|
|
||||||
|
1. The bootloader checks the otadata partition for a valid boot selection.
|
||||||
|
2. Finding all `0xFF` (erased state), it falls back to the factory app if one exists, or `ota_0` otherwise.
|
||||||
|
3. The device boots the fallback firmware.
|
||||||
|
|
||||||
|
For more precise control over which partition to erase, use `esp_partition_analyze` to find the otadata offset and `esp_flash_erase` to clear it manually.
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="OTA Management Reference"
|
||||||
|
description="Full parameter details for esp_ota_package_create, esp_ota_deploy, and esp_ota_rollback."
|
||||||
|
href="/reference/ota-management/"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinkCard
|
||||||
|
title="Custom Partition Layouts"
|
||||||
|
description="Design partition tables with larger OTA slots or additional storage."
|
||||||
|
href="/guides/partition-layouts/"
|
||||||
|
/>
|
||||||
39
docs-site/src/content/docs/index.mdx
Normal file
39
docs-site/src/content/docs/index.mdx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: mcesptool
|
||||||
|
description: ESP32 and ESP8266 development through Model Context Protocol
|
||||||
|
template: splash
|
||||||
|
hero:
|
||||||
|
tagline: ESP32 and ESP8266 development through Model Context Protocol
|
||||||
|
actions:
|
||||||
|
- text: Get Started
|
||||||
|
link: /tutorials/getting-started/
|
||||||
|
icon: right-arrow
|
||||||
|
- text: Tool Reference
|
||||||
|
link: /reference/
|
||||||
|
variant: minimal
|
||||||
|
---
|
||||||
|
|
||||||
|
import { CardGrid, Card } from "@astrojs/starlight/components";
|
||||||
|
|
||||||
|
<CardGrid stagger>
|
||||||
|
<Card title="40+ Tools" icon="rocket">
|
||||||
|
Complete ESP development toolkit from chip detection to production
|
||||||
|
programming. Flash firmware, manage partitions, read eFuses, run
|
||||||
|
diagnostics, and deploy OTA updates -- all through natural language.
|
||||||
|
</Card>
|
||||||
|
<Card title="Zero Hardware" icon="laptop">
|
||||||
|
Full QEMU emulation lets you flash, debug, and test without physical
|
||||||
|
devices. Virtual ESP32 instances expose real serial-over-TCP ports that
|
||||||
|
work identically to USB-connected hardware.
|
||||||
|
</Card>
|
||||||
|
<Card title="Production Ready" icon="setting">
|
||||||
|
Factory programming, batch operations, and quality control workflows
|
||||||
|
built for the assembly line. Program hundreds of devices with consistent
|
||||||
|
configuration and automated validation.
|
||||||
|
</Card>
|
||||||
|
<Card title="Safety First" icon="warning">
|
||||||
|
eFuse warnings, flash encryption guidance, and confirmation prompts for
|
||||||
|
destructive operations. Irreversible actions are clearly labeled and
|
||||||
|
require explicit intent.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
337
docs-site/src/content/docs/reference/chip-control.mdx
Normal file
337
docs-site/src/content/docs/reference/chip-control.mdx
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
---
|
||||||
|
title: Chip Control
|
||||||
|
description: ESP device detection, connection, and control tools
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Badge, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Chip Control component provides 7 tools for detecting ESP devices, managing connections, resetting chips, and capturing serial output. These tools form the foundation for all device interactions in mcesptool.
|
||||||
|
|
||||||
|
Port parameters accept physical serial ports (`/dev/ttyUSB0`, `COM3`) and QEMU virtual serial ports (`socket://localhost:5555`). When `port` is optional and omitted, auto-detection probes common serial ports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_detect_chip
|
||||||
|
|
||||||
|
Detect the chip type and gather device information by running `esptool chip-id` (and optionally `flash-id`) as a subprocess.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or socket URI. Auto-detects if omitted. |
|
||||||
|
| `baud_rate` | `int \| None` | `None` | Baud rate. Uses server default (460800) if omitted. |
|
||||||
|
| `detailed` | `bool` | `False` | Include flash size, crystal frequency, and feature list. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Basic detection
|
||||||
|
result = await client.call_tool("esp_detect_chip", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Detailed detection with custom baud rate
|
||||||
|
result = await client.call_tool("esp_detect_chip", {
|
||||||
|
"port": "socket://localhost:5555",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"detailed": True
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 460800,
|
||||||
|
"connection_time_seconds": 1.23,
|
||||||
|
"chip_info": {
|
||||||
|
"chip_type": "ESP32-S3",
|
||||||
|
"mac_address": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"crystal_frequency": "40MHz",
|
||||||
|
"features": ["WiFi", "BLE", "Embedded PSRAM 8MB"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When `detailed` is `False`, the `chip_info` object contains only `chip_type` and `mac_address`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_connect_advanced
|
||||||
|
|
||||||
|
Verify device connectivity with configurable retries, timeout, and stub loader settings. Internally runs `esptool chip-id` up to `retry_count` times with a delay between attempts.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or socket URI. Auto-detects if omitted. |
|
||||||
|
| `baud_rate` | `int \| None` | `None` | Baud rate. Uses server default if omitted. |
|
||||||
|
| `timeout` | `int \| None` | `None` | Connection timeout in seconds. Uses config default (30) if omitted. |
|
||||||
|
| `use_stub` | `bool` | `True` | Load ROM bootloader stub for faster operations. |
|
||||||
|
| `retry_count` | `int` | `3` | Number of connection attempts before giving up. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_connect_advanced", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"timeout": 10,
|
||||||
|
"retry_count": 5
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 460800,
|
||||||
|
"attempt": 2,
|
||||||
|
"stub_loaded": true,
|
||||||
|
"chip_type": "ESP32",
|
||||||
|
"mac_address": "aa:bb:cc:dd:ee:ff"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On failure after all retries:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Timeout (30.0s)",
|
||||||
|
"attempts": 5,
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_reset_chip
|
||||||
|
|
||||||
|
Reset the ESP chip using different reset methods. Internally runs `esptool chip-id` with the `--after` flag set according to the reset type.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or socket URI. Auto-detects if omitted. |
|
||||||
|
| `reset_type` | `str` | `"hard"` | Reset method: `hard`, `soft`, or `bootloader`. |
|
||||||
|
|
||||||
|
### Reset Types
|
||||||
|
|
||||||
|
| Value | esptool `--after` | Behavior |
|
||||||
|
|-------|-------------------|----------|
|
||||||
|
| `hard` | `hard_reset` | Full hardware reset via DTR/RTS toggle. Device reboots and runs application. |
|
||||||
|
| `soft` | `soft_reset` | Soft reset. Device restarts without full power cycle. |
|
||||||
|
| `bootloader` | `no_reset` | Leaves the chip in bootloader mode after the command completes. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_reset_chip", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"reset_type": "hard"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"reset_type": "hard",
|
||||||
|
"timestamp": 1708732800.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_scan_ports
|
||||||
|
|
||||||
|
Scan all common serial ports for connected ESP devices. Also includes any running QEMU virtual devices in the results.
|
||||||
|
|
||||||
|
Scans `/dev/ttyUSB0`-`3` and `/dev/ttyACM0`-`3` on Linux, `COM1`-`COM20` on Windows, and `/dev/cu.usbserial-*` on macOS.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `detailed` | `bool` | `False` | Include flash ID and extended info for each detected device. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_scan_ports", {
|
||||||
|
"detailed": True
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"detected_devices": [
|
||||||
|
{
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"available": true,
|
||||||
|
"chip_type": "ESP32",
|
||||||
|
"mac_address": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"flash_size": "4MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"port": "socket://localhost:5555",
|
||||||
|
"chip_type": "esp32c3",
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"source": "qemu",
|
||||||
|
"available": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_scanned": 9,
|
||||||
|
"checked_ports": ["/dev/ttyUSB0", "/dev/ttyUSB1", "..."],
|
||||||
|
"available_ports": ["/dev/ttyUSB0"],
|
||||||
|
"qemu_devices": [{"..."}],
|
||||||
|
"timestamp": 1708732800.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_load_test_firmware
|
||||||
|
|
||||||
|
Load a pre-built test firmware for quick chip validation. Currently returns firmware metadata; actual flashing requires ESP-IDF integration.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or socket URI. Auto-detects if omitted. |
|
||||||
|
| `firmware_type` | `str` | `"blink"` | Test firmware type: `blink`, `hello_world`, or `wifi_scan`. |
|
||||||
|
|
||||||
|
### Firmware Types
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `blink` | Simple LED blink test |
|
||||||
|
| `hello_world` | Serial output hello world |
|
||||||
|
| `wifi_scan` | WiFi network scanner |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_load_test_firmware", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_type": "hello_world"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_type": "hello_world",
|
||||||
|
"description": "Serial output hello world",
|
||||||
|
"note": "Test firmware loading requires ESP-IDF integration (coming soon)",
|
||||||
|
"timestamp": 1708732800.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Test firmware loading currently requires ESP-IDF integration. The tool validates the firmware type and port but does not yet flash firmware to the device.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_load_ram
|
||||||
|
|
||||||
|
Load and execute a binary in device RAM without touching flash. Uses `esptool load-ram` to transfer the binary. The program runs until the device is physically reset.
|
||||||
|
|
||||||
|
This is ideal for rapid development iteration -- test changes without wearing out flash or waiting for a full flash cycle.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `binary_path` | `str` | *(required)* | Path to the RAM-executable binary. Must be compiled for RAM execution. |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or socket URI. Auto-detects if omitted. |
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The binary must be compiled specifically for RAM execution (no flash relocation). Use `esp_elf_to_ram_binary` to convert an ELF file. Features that require flash storage (OTA, NVS, SPIFFS/LittleFS) will not work from RAM.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_load_ram", {
|
||||||
|
"binary_path": "/path/to/firmware-ram.bin",
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"binary_path": "/path/to/firmware-ram.bin",
|
||||||
|
"file_size": 32768,
|
||||||
|
"elapsed_seconds": 2.15,
|
||||||
|
"note": "Binary loaded to RAM and executing. Reset device to stop."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_serial_monitor
|
||||||
|
|
||||||
|
Open the serial port and capture device output for a specified duration. Useful for reading boot messages, debug output, or application logs without switching to a separate terminal monitor.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str` | *(required)* | Serial port. No auto-detect for monitor operations. |
|
||||||
|
| `baud_rate` | `int` | `115200` | Serial baud rate. |
|
||||||
|
| `duration_seconds` | `float` | `5.0` | Capture duration. Clamped to 0.1--30 seconds. |
|
||||||
|
| `reset_on_connect` | `bool` | `True` | Reset device before capturing to get boot messages. |
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The `port` parameter is **required** for this tool. Auto-detection is not supported because the monitor needs exclusive serial access for a continuous read.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_serial_monitor", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"duration_seconds": 10.0,
|
||||||
|
"reset_on_connect": True
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"duration_seconds": 10.04,
|
||||||
|
"reset_performed": true,
|
||||||
|
"line_count": 42,
|
||||||
|
"output": "rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)\nI (0) cpu_start: Starting scheduler...\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `output` field contains the raw serial text with lines joined by `\n`. The `line_count` reflects the number of complete lines captured during the duration window.
|
||||||
169
docs-site/src/content/docs/reference/configuration.mdx
Normal file
169
docs-site/src/content/docs/reference/configuration.mdx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
title: Configuration
|
||||||
|
description: Environment variables and configuration options
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
mcesptool is configured through environment variables. All settings have sensible defaults and can be overridden individually. The server loads configuration at startup from `ESPToolServerConfig`, which reads environment variables and auto-detects paths where possible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Settings
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `ESPTOOL_PATH` | `esptool` | Path or command name for the esptool binary. Set this if esptool is not on your PATH or you want to use a specific version. |
|
||||||
|
| `ESP_IDF_PATH` | *(auto-detect)* | Path to the ESP-IDF installation directory. Auto-detected from `~/esp/esp-idf`, `/opt/esp-idf`, or `/usr/local/esp-idf` if not set. |
|
||||||
|
| `MCP_PROJECT_ROOTS` | *(auto-detect)* | Colon-separated list of directories to scan for ESP projects. Auto-detects from `~/esp_projects`, `~/Arduino`, `~/Documents/Arduino`, and `/workspace/projects`. |
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
`MCP_PROJECT_ROOTS` uses colon (`:`) as the separator, matching the PATH convention:
|
||||||
|
```
|
||||||
|
MCP_PROJECT_ROOTS=/home/user/esp_projects:/home/user/arduino_projects
|
||||||
|
```
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Communication Settings
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `ESP_DEFAULT_BAUD_RATE` | `460800` | Default serial baud rate for device connections. Valid values: `9600`, `57600`, `115200`, `230400`, `460800`, `921600`. Unusual values produce a warning but are not rejected. |
|
||||||
|
| `ESP_CONNECTION_TIMEOUT` | `30` | Connection timeout in seconds. Must be between 5 and 300. |
|
||||||
|
| `ESP_ENABLE_STUB_FLASHER` | `true` | Load the ROM bootloader stub for faster flash operations. Set to `false` for raw ROM communication. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Integration Settings
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `MCP_ENABLE_PROGRESS` | `true` | Enable MCP progress notifications for long-running operations. |
|
||||||
|
| `MCP_ENABLE_ELICITATION` | `true` | Enable MCP elicitation for interactive confirmation prompts (e.g., eFuse burns). |
|
||||||
|
| `MCP_LOG_LEVEL` | `INFO` | Logging level. Valid values: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Settings
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `ESP_MAX_CONCURRENT_OPERATIONS` | `5` | Maximum number of parallel esptool subprocess operations. Must be between 1 and 20. Controls concurrency for batch programming and other parallelized operations. |
|
||||||
|
| `ESP_OPERATION_TIMEOUT` | `300` | Global timeout in seconds for individual esptool operations. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Settings
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DEV_ENABLE_HOT_RELOAD` | `false` | Enable hot reload for server code changes during development. |
|
||||||
|
| `DEV_MOCK_HARDWARE` | `false` | Enable hardware mocking for testing without physical devices. |
|
||||||
|
| `DEV_ENABLE_TRACING` | `false` | Enable detailed operation tracing for debugging. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Settings
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PRODUCTION_MODE` | `false` | Enable production mode. Affects logging verbosity and security defaults. Can also be enabled via the `--production` CLI flag. |
|
||||||
|
| `PROD_ENABLE_SECURITY_AUDIT` | `true` | Run security audits as part of production operations. |
|
||||||
|
| `PROD_REQUIRE_CONFIRMATIONS` | `true` | Require explicit confirmation for destructive operations in production mode. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## QEMU Settings
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `QEMU_XTENSA_PATH` | *(auto-detect)* | Path to `qemu-system-xtensa` binary from the Espressif QEMU fork. Auto-detected from `~/.espressif/tools/qemu-xtensa/*/qemu/bin/qemu-system-xtensa`. |
|
||||||
|
| `QEMU_RISCV_PATH` | *(auto-detect)* | Path to `qemu-system-riscv32` binary from the Espressif QEMU fork. Auto-detected from `~/.espressif/tools/qemu-riscv32/*/qemu/bin/qemu-system-riscv32`. |
|
||||||
|
| `QEMU_BASE_PORT` | `5555` | Starting TCP port number for QEMU virtual serial connections. Instances are assigned sequential ports from this base. |
|
||||||
|
| `QEMU_MAX_INSTANCES` | `4` | Maximum number of concurrent QEMU instances. Each instance uses one TCP port from the pool. |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
QEMU binary auto-detection uses glob patterns and selects the latest installed version (sorted alphabetically). If you have multiple QEMU versions installed, set the path explicitly to control which version is used.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auto-Detection Behavior
|
||||||
|
|
||||||
|
### ESP-IDF Path
|
||||||
|
|
||||||
|
When `ESP_IDF_PATH` is not set, the server checks these locations in order:
|
||||||
|
|
||||||
|
1. `~/esp/esp-idf`
|
||||||
|
2. `/opt/esp-idf`
|
||||||
|
3. `/usr/local/esp-idf`
|
||||||
|
|
||||||
|
A directory is only accepted if it exists and contains `idf.py` at the root.
|
||||||
|
|
||||||
|
### QEMU Binaries
|
||||||
|
|
||||||
|
When `QEMU_XTENSA_PATH` or `QEMU_RISCV_PATH` is not set, the server searches:
|
||||||
|
|
||||||
|
- Xtensa: `~/.espressif/tools/qemu-xtensa/*/qemu/bin/qemu-system-xtensa`
|
||||||
|
- RISC-V: `~/.espressif/tools/qemu-riscv32/*/qemu/bin/qemu-system-riscv32`
|
||||||
|
|
||||||
|
The latest matching version (by path sort order) is selected.
|
||||||
|
|
||||||
|
### Project Roots
|
||||||
|
|
||||||
|
When `MCP_PROJECT_ROOTS` is not set, the server checks these directories and includes any that exist:
|
||||||
|
|
||||||
|
- `~/esp_projects`
|
||||||
|
- `~/Arduino`
|
||||||
|
- `~/Documents/Arduino`
|
||||||
|
- `/workspace/projects` (Docker environments)
|
||||||
|
|
||||||
|
Additional roots are added at runtime when the MCP client provides workspace roots via the MCP roots protocol.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The server validates configuration at startup and raises an error if:
|
||||||
|
|
||||||
|
- `esptool` is not found at the configured path
|
||||||
|
- `ESP_CONNECTION_TIMEOUT` is outside the 5--300 second range
|
||||||
|
- `ESP_MAX_CONCURRENT_OPERATIONS` is outside the 1--20 range
|
||||||
|
|
||||||
|
An unusual (but valid) `ESP_DEFAULT_BAUD_RATE` value produces a warning but does not prevent startup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
Minimal `.env` file for a development setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use a specific esptool version
|
||||||
|
ESPTOOL_PATH=/home/user/.local/bin/esptool
|
||||||
|
|
||||||
|
# Point to ESP-IDF
|
||||||
|
ESP_IDF_PATH=/home/user/esp/esp-idf
|
||||||
|
|
||||||
|
# Project directories
|
||||||
|
MCP_PROJECT_ROOTS=/home/user/esp_projects:/home/user/arduino_projects
|
||||||
|
|
||||||
|
# Enable debug logging
|
||||||
|
MCP_LOG_LEVEL=DEBUG
|
||||||
|
|
||||||
|
# Development features
|
||||||
|
DEV_ENABLE_HOT_RELOAD=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Production `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ESPTOOL_PATH=esptool
|
||||||
|
PRODUCTION_MODE=true
|
||||||
|
PROD_ENABLE_SECURITY_AUDIT=true
|
||||||
|
PROD_REQUIRE_CONFIRMATIONS=true
|
||||||
|
MCP_LOG_LEVEL=WARNING
|
||||||
|
ESP_MAX_CONCURRENT_OPERATIONS=10
|
||||||
|
```
|
||||||
166
docs-site/src/content/docs/reference/diagnostics.mdx
Normal file
166
docs-site/src/content/docs/reference/diagnostics.mdx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
---
|
||||||
|
title: Diagnostics
|
||||||
|
description: Memory dumps, performance profiling, and diagnostic reports
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Diagnostics component provides 3 tools for inspecting device memory, profiling serial communication performance, and generating comprehensive device reports. All operations use `esptool` subprocess commands and work with both physical devices and QEMU virtual devices.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_memory_dump
|
||||||
|
|
||||||
|
Read raw bytes from an arbitrary memory address on the device using `esptool dump-mem`. The output is formatted as a hex dump with ASCII representation.
|
||||||
|
|
||||||
|
For flash memory reads, use [esp_flash_read](/reference/flash-operations/#esp_flash_read) instead -- it supports larger ranges and is faster for flash-specific operations.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `start_address` | `str` | `"0x0"` | Memory address to start reading (hex string). |
|
||||||
|
| `size` | `str` | `"1KB"` | Number of bytes to read. Accepts `"256B"`, `"1KB"`, `"4KB"`, hex, or decimal. Maximum 1MB. |
|
||||||
|
|
||||||
|
### Size Format
|
||||||
|
|
||||||
|
| Format | Example | Bytes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| Bytes | `"256B"` | 256 |
|
||||||
|
| Kilobytes | `"1KB"`, `"4KB"` | 1024, 4096 |
|
||||||
|
| Megabytes | `"1MB"` | 1048576 |
|
||||||
|
| Hex | `"0x1000"` | 4096 |
|
||||||
|
| Decimal | `"512"` | 512 |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_memory_dump", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x3ff00000",
|
||||||
|
"size": "256B"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x3ff00000",
|
||||||
|
"bytes_read": 256,
|
||||||
|
"hex_dump": "0x3ff00000: 00 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 ................\n0x3ff00010: 13 00 00 60 00 00 00 00 00 00 00 00 00 00 00 00 ...`............\n...",
|
||||||
|
"truncated": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The hex dump is capped at 64 lines (1KB of data). When the dump exceeds this limit, `truncated` is `true`.
|
||||||
|
|
||||||
|
Each line shows: `address: hex bytes (16 per line) ASCII representation`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_performance_profile
|
||||||
|
|
||||||
|
Measure serial transport speed by timing a sequence of `esptool` operations. Reports individual operation latencies and an overall average. Useful for comparing physical serial vs QEMU socket performance, or for diagnosing slow connections.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `duration` | `int` | `30` | Reserved for future use. Does not control timing. |
|
||||||
|
|
||||||
|
### Operations Tested
|
||||||
|
|
||||||
|
| Operation | What it measures |
|
||||||
|
|-----------|-----------------|
|
||||||
|
| `chip-id` | Lightweight command round-trip latency |
|
||||||
|
| `flash-id` | SPI flash register read latency |
|
||||||
|
| `read-mac` | eFuse read latency |
|
||||||
|
| `read-flash (4KB)` | Bulk data transfer throughput |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_performance_profile", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"measurements": [
|
||||||
|
{"operation": "chip-id", "elapsed_seconds": 0.845, "success": true},
|
||||||
|
{"operation": "flash-id", "elapsed_seconds": 0.912, "success": true},
|
||||||
|
{"operation": "read-mac", "elapsed_seconds": 0.789, "success": true},
|
||||||
|
{
|
||||||
|
"operation": "read-flash (4KB)",
|
||||||
|
"elapsed_seconds": 1.234,
|
||||||
|
"success": true,
|
||||||
|
"throughput_bytes_per_sec": 3319.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"operations_tested": 4,
|
||||||
|
"operations_succeeded": 4,
|
||||||
|
"average_latency_seconds": 0.945
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `throughput_bytes_per_sec` field is only present for the flash read operation. The `average_latency_seconds` is computed across all successful operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_diagnostic_report
|
||||||
|
|
||||||
|
Generate a comprehensive device identification report by gathering chip identity, MAC address, flash information, and optionally a small memory dump. All data is collected in a single call.
|
||||||
|
|
||||||
|
For security-focused analysis, use [esp_security_audit](/reference/security/#esp_security_audit) instead.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `include_memory` | `bool` | `False` | Include a 256-byte memory dump from address `0x0`. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_diagnostic_report", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"include_memory": True
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"chip": "ESP32-S3",
|
||||||
|
"chip_id": "0x00f01d83",
|
||||||
|
"mac_address": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"flash": {
|
||||||
|
"manufacturer": "0xef",
|
||||||
|
"device": "0x4016",
|
||||||
|
"size": "4MB"
|
||||||
|
},
|
||||||
|
"memory_dump_0x0": "0x00000000: e9 07 02 30 ..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `memory_dump_0x0` field is only present when `include_memory` is `True`. If the initial `chip-id` command fails, the tool returns immediately with the connection error instead of attempting further operations.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
The diagnostic report is a good first step when working with an unknown device. It confirms connectivity and identifies the hardware before attempting flash operations.
|
||||||
|
</Aside>
|
||||||
152
docs-site/src/content/docs/reference/firmware-builder.mdx
Normal file
152
docs-site/src/content/docs/reference/firmware-builder.mdx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
title: Firmware Builder
|
||||||
|
description: ELF conversion and firmware analysis tools
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Firmware Builder component provides 3 tools for converting ELF files to flashable or RAM-loadable binaries and for analyzing firmware image structure. These tools wrap `esptool elf2image` and `esptool image-info` as async subprocesses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_elf_to_binary
|
||||||
|
|
||||||
|
Convert an ELF file to a flashable binary image using `esptool elf2image`. The resulting `.bin` file can be written to flash with `esp_flash_firmware` or `esp_flash_multi`.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `elf_path` | `str` | *(required)* | Path to the input ELF file. |
|
||||||
|
| `output_path` | `str \| None` | `None` | Output binary path. Defaults to `<elf_name>.bin` in the same directory. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_elf_to_binary", {
|
||||||
|
"elf_path": "/build/app.elf",
|
||||||
|
"output_path": "/build/app.bin"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"elf_path": "/build/app.elf",
|
||||||
|
"output_path": "/build/app.bin",
|
||||||
|
"output_size_bytes": 524288,
|
||||||
|
"esptool_output": "esptool.py v4.8\nCreating esp32 image...\nMerge segment 0x3f400020...\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_elf_to_ram_binary
|
||||||
|
|
||||||
|
Convert an ELF file to a RAM-loadable binary using `esptool elf2image --ram-only-header`. The resulting binary contains only IRAM/DRAM segments and can be loaded directly into device RAM with `esp_load_ram`.
|
||||||
|
|
||||||
|
This is the first half of the rapid development workflow:
|
||||||
|
1. Build your project with ESP-IDF
|
||||||
|
2. Convert ELF to RAM binary with this tool
|
||||||
|
3. Load to device with `esp_load_ram`
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `elf_path` | `str` | *(required)* | Path to the input ELF file. |
|
||||||
|
| `output_path` | `str \| None` | `None` | Output binary path. Defaults to `<elf_name>-ram.bin`. |
|
||||||
|
| `chip` | `str` | `"auto"` | Target chip type (`auto`, `esp32`, `esp32s3`, `esp32c3`, etc.). |
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
RAM binaries have specific requirements:
|
||||||
|
|
||||||
|
- The ELF must **not** have an embedded SHA256 digest at the reserved offset.
|
||||||
|
- Disable `CONFIG_SECURE_BOOT` and `CONFIG_SECURE_SIGNED_APPS` in your sdkconfig.
|
||||||
|
- Features requiring flash storage (OTA, NVS, SPIFFS, LittleFS) will not work from RAM.
|
||||||
|
- Some PlatformIO build configurations may produce incompatible ELF files.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_elf_to_ram_binary", {
|
||||||
|
"elf_path": "/build/app.elf",
|
||||||
|
"chip": "esp32s3"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"elf_path": "/build/app.elf",
|
||||||
|
"output_path": "/build/app-ram.bin",
|
||||||
|
"chip": "esp32s3",
|
||||||
|
"ram_optimized": true,
|
||||||
|
"output_size_bytes": 131072,
|
||||||
|
"usage_hint": "Load to device with: esp_load_ram(binary_path='/build/app-ram.bin', port='<your-port>')"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_firmware_analyze
|
||||||
|
|
||||||
|
Analyze a firmware binary's structure using `esptool image-info --version 2`. Returns parsed metadata including chip target, flash configuration, entry point, and segment layout.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `firmware_path` | `str` | *(required)* | Path to the firmware binary (`.bin`) file. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_firmware_analyze", {
|
||||||
|
"firmware_path": "/build/app.bin"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"file_size_bytes": 524288,
|
||||||
|
"entry_point": "0x40081d5c",
|
||||||
|
"chip": "ESP32",
|
||||||
|
"flash_mode": "DIO",
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"flash_freq": "40m",
|
||||||
|
"segments": [
|
||||||
|
{"index": 0, "length": "0x07a54", "load_address": "0x3f400020"},
|
||||||
|
{"index": 1, "length": "0x03bb0", "load_address": "0x3ffb0000"},
|
||||||
|
{"index": 2, "length": "0x5c1e4", "load_address": "0x40080000"}
|
||||||
|
],
|
||||||
|
"segment_count": 3,
|
||||||
|
"validation_hash": "valid",
|
||||||
|
"raw_output": "esptool.py v4.8\nImage version: 1\nEntry point: 0x40081d5c\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parsed Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `entry_point` | Address where execution begins after load. |
|
||||||
|
| `chip` | Target chip the image was built for. |
|
||||||
|
| `flash_mode` | SPI flash mode (`QIO`, `QOUT`, `DIO`, `DOUT`). |
|
||||||
|
| `flash_size` | Expected flash size encoded in the header. |
|
||||||
|
| `flash_freq` | SPI flash frequency (`40m`, `80m`, etc.). |
|
||||||
|
| `segments` | List of binary segments with load addresses and lengths. |
|
||||||
|
| `validation_hash` | Image hash validation result, if present. |
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Compare the `flash_size` in the firmware header with the actual device flash size (from `esp_detect_chip` with `detailed: true`) to catch configuration mismatches before flashing.
|
||||||
|
</Aside>
|
||||||
316
docs-site/src/content/docs/reference/flash-operations.mdx
Normal file
316
docs-site/src/content/docs/reference/flash-operations.mdx
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
---
|
||||||
|
title: Flash Operations
|
||||||
|
description: Flash memory read, write, erase, and verification tools
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Badge, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Flash Manager component provides 7 tools for reading, writing, erasing, and verifying ESP device flash memory. All operations shell out to `esptool` as async subprocesses and support both physical serial ports and QEMU `socket://` URIs.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Flash operations require an explicit `port` parameter. Auto-detection is disabled for these tools to prevent accidental writes to the wrong device. You will receive an error if you omit the port.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_flash_firmware
|
||||||
|
|
||||||
|
Write a single firmware binary to flash at a specified address using `esptool write-flash`.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `firmware_path` | `str` | *(required)* | Path to the firmware binary (`.bin`) file. |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required** -- returns error if omitted. |
|
||||||
|
| `address` | `str` | `"0x0"` | Flash address to write to (hex string). Use partition offsets for non-firmware images. |
|
||||||
|
| `verify` | `bool` | `True` | Verify flash contents after writing. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Flash application firmware at the standard app offset
|
||||||
|
result = await client.call_tool("esp_flash_firmware", {
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"address": "0x10000",
|
||||||
|
"verify": True
|
||||||
|
})
|
||||||
|
|
||||||
|
# Flash a filesystem image at a custom offset
|
||||||
|
result = await client.call_tool("esp_flash_firmware", {
|
||||||
|
"firmware_path": "/build/littlefs.bin",
|
||||||
|
"port": "socket://localhost:5555",
|
||||||
|
"address": "0x290000"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"address": "0x10000",
|
||||||
|
"firmware_size": 524288,
|
||||||
|
"bytes_written": 524288,
|
||||||
|
"verified": true,
|
||||||
|
"elapsed_seconds": 12.3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_flash_multi
|
||||||
|
|
||||||
|
Flash multiple binary files at different addresses in a single `esptool write-flash` invocation. Faster than separate flash operations because it connects to the device only once.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `files` | `list[dict]` | *(required)* | List of objects with `address` (hex string) and `path` (file path). |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `verify` | `bool` | `True` | Verify flash contents after writing. |
|
||||||
|
| `compress` | `bool` | `True` | Use compression for faster transfer. |
|
||||||
|
|
||||||
|
Each entry in `files` must have exactly two keys:
|
||||||
|
|
||||||
|
| Key | Type | Description |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| `address` | `str` | Hex flash offset (e.g., `"0x0"`, `"0x8000"`, `"0x10000"`). |
|
||||||
|
| `path` | `str` | Absolute path to the binary file. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_flash_multi", {
|
||||||
|
"files": [
|
||||||
|
{"address": "0x0", "path": "/build/bootloader.bin"},
|
||||||
|
{"address": "0x8000", "path": "/build/partitions.bin"},
|
||||||
|
{"address": "0x10000", "path": "/build/app.bin"}
|
||||||
|
],
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"verify": True,
|
||||||
|
"compress": True
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"files": [
|
||||||
|
{"address": "0x0", "path": "/build/bootloader.bin", "size": 26384},
|
||||||
|
{"address": "0x8000", "path": "/build/partitions.bin", "size": 3072},
|
||||||
|
{"address": "0x10000", "path": "/build/app.bin", "size": 524288}
|
||||||
|
],
|
||||||
|
"file_count": 3,
|
||||||
|
"total_size": 553744,
|
||||||
|
"bytes_written": 553744,
|
||||||
|
"compressed": true,
|
||||||
|
"verified": true,
|
||||||
|
"elapsed_seconds": 15.7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_flash_read
|
||||||
|
|
||||||
|
Read raw bytes from flash memory and save them to a file. If `size` is not specified, the tool auto-detects the flash size and reads the entire contents.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `output_path` | `str` | *(required)* | File path to save the flash contents. Parent directories are created automatically. |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `start_address` | `str` | `"0x0"` | Flash offset to start reading from (hex string). |
|
||||||
|
| `size` | `str \| None` | `None` | Number of bytes to read (hex or decimal string). Reads entire flash if omitted. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Read entire flash
|
||||||
|
result = await client.call_tool("esp_flash_read", {
|
||||||
|
"output_path": "/backups/device_flash.bin",
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Read just the partition table region
|
||||||
|
result = await client.call_tool("esp_flash_read", {
|
||||||
|
"output_path": "/backups/partitions.bin",
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x8000",
|
||||||
|
"size": "0xC00"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"output_path": "/backups/device_flash.bin",
|
||||||
|
"start_address": "0x0",
|
||||||
|
"bytes_read": 4194304,
|
||||||
|
"elapsed_seconds": 45.2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_flash_erase
|
||||||
|
|
||||||
|
Erase flash memory. Without a `size` parameter, the entire flash is erased. With `size`, only the specified region is erased. Erased bytes become `0xFF`.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `start_address` | `str` | `"0x0"` | Flash offset to start erasing (hex string). |
|
||||||
|
| `size` | `str \| None` | `None` | Number of bytes to erase (hex or decimal string). Erases entire flash if omitted. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Erase Entire Flash">
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_flash_erase", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Erase Region">
|
||||||
|
```python
|
||||||
|
# Erase the NVS partition (64KB at 0x9000)
|
||||||
|
result = await client.call_tool("esp_flash_erase", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"start_address": "0x9000",
|
||||||
|
"size": "0x10000"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"erase_type": "full",
|
||||||
|
"start_address": "0x0",
|
||||||
|
"size": null,
|
||||||
|
"elapsed_seconds": 8.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For region erases, `erase_type` is `"region"` and `size` contains the requested byte count.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_flash_backup
|
||||||
|
|
||||||
|
Create a complete flash backup by reading the entire flash to a file. This is a convenience wrapper around `esp_flash_read` that handles address and size selection.
|
||||||
|
|
||||||
|
The resulting file can be restored with `esp_flash_firmware`.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `backup_path` | `str` | *(required)* | File path to save the backup. |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `include_bootloader` | `bool` | `True` | Start from address `0x0` to include the bootloader. When `False`, starts at `0x1000`. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_flash_backup", {
|
||||||
|
"backup_path": "/backups/full_device_20260223.bin",
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"include_bootloader": True
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
The return value matches `esp_flash_read`. The `start_address` will be `"0x0"` or `"0x1000"` depending on `include_bootloader`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"output_path": "/backups/full_device_20260223.bin",
|
||||||
|
"start_address": "0x0",
|
||||||
|
"bytes_read": 4194304,
|
||||||
|
"elapsed_seconds": 45.2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_verify_flash
|
||||||
|
|
||||||
|
Verify that flash contents at a given address match a local binary file, without re-flashing. Uses `esptool verify-flash` internally.
|
||||||
|
|
||||||
|
Useful for confirming a successful flash operation or checking whether an update is needed.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `firmware_path` | `str` | *(required)* | Path to the binary file to compare against. |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `address` | `str` | `"0x0"` | Flash address to verify from (hex string). |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_verify_flash", {
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"address": "0x10000"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
On match:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"verified": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"address": "0x10000",
|
||||||
|
"file_size": 524288,
|
||||||
|
"elapsed_seconds": 8.1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On mismatch:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"verified": false,
|
||||||
|
"mismatch": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"address": "0x10000",
|
||||||
|
"file_size": 524288,
|
||||||
|
"elapsed_seconds": 8.3,
|
||||||
|
"details": "Verify failed at offset 0x00001000..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that a mismatch still returns `"success": true` because the verification operation itself completed without error. Check the `verified` field to determine the outcome.
|
||||||
111
docs-site/src/content/docs/reference/index.mdx
Normal file
111
docs-site/src/content/docs/reference/index.mdx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
title: Tool Reference
|
||||||
|
description: Complete reference for all mcesptool MCP tools and resources
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Badge, Card, CardGrid, LinkCard } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
This is the complete reference for every MCP tool exposed by mcesptool. Tools are grouped by component. Each component page documents parameter signatures, return values, and usage examples.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
All tools follow a consistent pattern: they return a JSON object with a `success` boolean field. On failure, an `error` string describes what went wrong.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Tool Summary
|
||||||
|
|
||||||
|
### Chip Control (7 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_detect_chip` | Detect chip type and basic information |
|
||||||
|
| `esp_connect_advanced` | Advanced connection with retry logic and stub loading |
|
||||||
|
| `esp_reset_chip` | Reset chip via hard, soft, or bootloader reset |
|
||||||
|
| `esp_scan_ports` | Scan all serial ports for ESP devices (includes QEMU) |
|
||||||
|
| `esp_load_test_firmware` | Load test firmware for chip validation |
|
||||||
|
| `esp_load_ram` | Load and execute binary in RAM without touching flash |
|
||||||
|
| `esp_serial_monitor` | Capture serial output from an ESP device |
|
||||||
|
|
||||||
|
### Flash Operations (7 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_flash_firmware` | Flash a single binary to device |
|
||||||
|
| `esp_flash_multi` | Flash multiple binaries at different addresses in one operation |
|
||||||
|
| `esp_flash_read` | Read flash memory contents to a file |
|
||||||
|
| `esp_flash_erase` | Erase flash regions or entire flash |
|
||||||
|
| `esp_flash_backup` | Create a complete flash backup |
|
||||||
|
| `esp_verify_flash` | Verify flash contents match a file |
|
||||||
|
|
||||||
|
### Partition Management (3 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_partition_create_ota` | Generate an OTA-capable partition table |
|
||||||
|
| `esp_partition_custom` | Create a custom partition table from configuration |
|
||||||
|
| `esp_partition_analyze` | Read and parse partition table from a connected device |
|
||||||
|
|
||||||
|
### Security (4 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_security_audit` | Comprehensive security audit of device posture |
|
||||||
|
| `esp_enable_flash_encryption` | Check or enable flash encryption status |
|
||||||
|
| `esp_efuse_read` | Read eFuse values (individual or full summary) |
|
||||||
|
| `esp_efuse_burn` | Burn an eFuse value (irreversible on real hardware) |
|
||||||
|
|
||||||
|
### Firmware Builder (3 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_elf_to_binary` | Convert ELF file to flashable binary |
|
||||||
|
| `esp_elf_to_ram_binary` | Convert ELF file to RAM-loadable binary |
|
||||||
|
| `esp_firmware_analyze` | Analyze firmware binary structure and segments |
|
||||||
|
|
||||||
|
### OTA Management (3 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_ota_package_create` | Create OTA update package (zip with manifest) |
|
||||||
|
| `esp_ota_deploy` | Deploy OTA update to a device via HTTP POST |
|
||||||
|
| `esp_ota_rollback` | Rollback to previous firmware by erasing otadata |
|
||||||
|
|
||||||
|
### Production Tools (3 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_factory_program` | Full factory programming pipeline (erase, flash, verify) |
|
||||||
|
| `esp_batch_program` | Program multiple devices concurrently |
|
||||||
|
| `esp_quality_control` | Run quality control tests (basic or extended) |
|
||||||
|
|
||||||
|
### Diagnostics (3 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_memory_dump` | Hex dump of device memory via dump-mem |
|
||||||
|
| `esp_performance_profile` | Profile serial transport speed and latency |
|
||||||
|
| `esp_diagnostic_report` | Comprehensive device identification report |
|
||||||
|
|
||||||
|
### QEMU Manager (5 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_qemu_start` | Start a virtual ESP device with QEMU |
|
||||||
|
| `esp_qemu_stop` | Stop one or all QEMU instances |
|
||||||
|
| `esp_qemu_list` | List all QEMU instances and their status |
|
||||||
|
| `esp_qemu_status` | Detailed status of a specific instance |
|
||||||
|
| `esp_qemu_flash` | Write firmware directly into a stopped instance's flash image |
|
||||||
|
|
||||||
|
### Server Tools (3 tools)
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `esp_server_info` | Server name, version, uptime, and capabilities |
|
||||||
|
| `esp_list_tools` | List all tools by category |
|
||||||
|
| `esp_health_check` | Environment health check |
|
||||||
|
|
||||||
|
## Additional References
|
||||||
|
|
||||||
|
<CardGrid>
|
||||||
|
<LinkCard title="MCP Resources" href="/reference/resources/" description="Real-time server status, configuration, and capability resources" />
|
||||||
|
<LinkCard title="Configuration" href="/reference/configuration/" description="Environment variables and server configuration options" />
|
||||||
|
</CardGrid>
|
||||||
158
docs-site/src/content/docs/reference/ota-management.mdx
Normal file
158
docs-site/src/content/docs/reference/ota-management.mdx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
title: OTA Management
|
||||||
|
description: Over-the-air update package creation, deployment, and rollback
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The OTA Manager component provides 3 tools for creating OTA update packages, deploying them to devices over HTTP, and rolling back to previous firmware versions. These tools work with any ESP device that has an OTA-capable partition layout (see [Partition Management](/reference/partition-management/)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_ota_package_create
|
||||||
|
|
||||||
|
Create a self-contained OTA update package as a ZIP archive. The package contains the firmware binary and a JSON manifest with version, checksum, and timestamp metadata.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `firmware_path` | `str` | *(required)* | Path to the firmware binary (`.bin`) to package. |
|
||||||
|
| `version` | `str` | *(required)* | Version string for the update (e.g., `"1.2.0"`, `"2026.02.23"`). |
|
||||||
|
| `output_path` | `str` | *(required)* | Path for the output ZIP package. |
|
||||||
|
|
||||||
|
### Package Contents
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `firmware.bin` | The raw application binary. |
|
||||||
|
| `manifest.json` | Metadata: version, filename, size, SHA-256 hash, creation timestamp. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_ota_package_create", {
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"version": "2026.02.23",
|
||||||
|
"output_path": "/releases/ota-2026.02.23.zip"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"output_path": "/releases/ota-2026.02.23.zip",
|
||||||
|
"package_size_bytes": 312456,
|
||||||
|
"manifest": {
|
||||||
|
"version": "2026.02.23",
|
||||||
|
"firmware_name": "app.bin",
|
||||||
|
"firmware_size": 524288,
|
||||||
|
"firmware_sha256": "a1b2c3d4e5f6...64 hex chars...",
|
||||||
|
"created_at": "2026-02-23T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_ota_deploy
|
||||||
|
|
||||||
|
Deploy an OTA update package to a device over HTTP. Extracts `firmware.bin` from the ZIP package and POSTs it to the device's OTA endpoint.
|
||||||
|
|
||||||
|
The target device must be running an HTTP OTA server -- for example, the `esp_https_ota` component from ESP-IDF or a custom HTTP handler.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `package_path` | `str` | *(required)* | Path to the OTA ZIP package created by `esp_ota_package_create`. |
|
||||||
|
| `target_url` | `str` | *(required)* | Device OTA endpoint URL (e.g., `http://192.168.1.100/ota/update`). |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
This tool uses `curl` as a subprocess for the HTTP transfer. The `curl` command must be available in the system PATH. The request times out after 130 seconds.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_ota_deploy", {
|
||||||
|
"package_path": "/releases/ota-2026.02.23.zip",
|
||||||
|
"target_url": "http://192.168.1.100/ota/update"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
On success (HTTP 2xx):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"target_url": "http://192.168.1.100/ota/update",
|
||||||
|
"http_status": "200",
|
||||||
|
"firmware_size_bytes": 524288,
|
||||||
|
"version": "2026.02.23"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On HTTP error:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"target_url": "http://192.168.1.100/ota/update",
|
||||||
|
"http_status": "500",
|
||||||
|
"firmware_size_bytes": 524288,
|
||||||
|
"error": "Device returned HTTP 500"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_ota_rollback
|
||||||
|
|
||||||
|
Roll back to the previous firmware version by erasing the `otadata` partition. When `otadata` is erased (all `0xFF`), the bootloader falls back to the factory app or `ota_0` slot on the next boot.
|
||||||
|
|
||||||
|
The tool reads the device's partition table to locate the `otadata` partition automatically, then erases that region.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_ota_rollback", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"otadata_offset": "0xf000",
|
||||||
|
"otadata_size": "0x2000",
|
||||||
|
"message": "OTA data partition erased. On next boot, the device will fall back to the factory app or ota_0 slot."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the device does not have an OTA partition layout:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "No otadata partition found -- device may not use OTA layout",
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
For more precise control over OTA slots, use `esp_partition_analyze` to inspect the partition table, then `esp_flash_erase` to clear specific regions. The rollback tool is a convenience shortcut for the common case.
|
||||||
|
</Aside>
|
||||||
254
docs-site/src/content/docs/reference/partition-management.mdx
Normal file
254
docs-site/src/content/docs/reference/partition-management.mdx
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
---
|
||||||
|
title: Partition Management
|
||||||
|
description: ESP partition table creation, customization, and analysis tools
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Partition Manager component provides 3 tools for generating and analyzing ESP partition tables. Partition tables define how flash memory is divided into regions for bootloader, application code, OTA slots, NVS storage, and filesystems.
|
||||||
|
|
||||||
|
Partition tables are stored at flash offset `0x8000` and occupy up to `0xC00` (3072) bytes. The tools in this component generate CSV-format tables compatible with ESP-IDF's `gen_esp32part.py` tool.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_partition_create_ota
|
||||||
|
|
||||||
|
Generate an OTA-capable partition table with two application slots, NVS storage, OTA data, and PHY calibration data. Follows Espressif's recommended OTA layout. Any remaining flash space is allocated to a SPIFFS storage partition.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `flash_size` | `str` | `"4MB"` | Total flash size. Accepts values like `"4MB"`, `"8MB"`, `"16MB"`. |
|
||||||
|
| `app_size` | `str` | `"1MB"` | Size for each OTA application slot. Accepts values like `"1MB"`, `"1536K"`. |
|
||||||
|
|
||||||
|
### Generated Layout
|
||||||
|
|
||||||
|
The tool produces the following partition structure (sizes shown for 4MB flash, 1MB app slots):
|
||||||
|
|
||||||
|
| Partition | Type | Subtype | Offset | Size |
|
||||||
|
|-----------|------|---------|--------|------|
|
||||||
|
| `nvs` | data | nvs | 0x9000 | 24KB |
|
||||||
|
| `otadata` | data | ota | 0xf000 | 8KB |
|
||||||
|
| `phy_init` | data | phy | 0x11000 | 4KB |
|
||||||
|
| `ota_0` | app | ota_0 | 0x12000 | 1MB |
|
||||||
|
| `ota_1` | app | ota_1 | 0x112000 | 1MB |
|
||||||
|
| `storage` | data | spiffs | 0x212000 | (remaining) |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_partition_create_ota", {
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"app_size": "1MB"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"app_size": "1MB",
|
||||||
|
"partition_csv": "# ESP-IDF Partition Table (OTA layout)\n# Name, Type, SubType, Offset, Size, Flags\nnvs, data, nvs, 0x9000, 24KB,\n...",
|
||||||
|
"partitions": [
|
||||||
|
{"name": "nvs", "type": "data", "subtype": "nvs", "offset": "0x9000", "size": "24KB"},
|
||||||
|
{"name": "otadata", "type": "data", "subtype": "ota", "offset": "0xf000", "size": "8KB"},
|
||||||
|
{"name": "phy_init", "type": "data", "subtype": "phy", "offset": "0x11000", "size": "4KB"},
|
||||||
|
{"name": "ota_0", "type": "app", "subtype": "ota_0", "offset": "0x12000", "size": "1MB"},
|
||||||
|
{"name": "ota_1", "type": "app", "subtype": "ota_1", "offset": "0x112000", "size": "1MB"},
|
||||||
|
{"name": "storage", "type": "data", "subtype": "spiffs", "offset": "0x212000", "size": "1984KB"}
|
||||||
|
],
|
||||||
|
"space_remaining": "1984KB",
|
||||||
|
"note": "Flash this CSV with gen_esp32part.py to binary, then write to 0x8000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool returns an error if the requested layout does not fit within the specified flash size.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_partition_custom
|
||||||
|
|
||||||
|
Create a custom partition table from a configuration dictionary. Offsets are auto-calculated starting from `0x9000` if not explicitly provided. App-type partitions are automatically aligned to 64KB boundaries.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `partition_config` | `dict` | *(required)* | Configuration with a `"partitions"` key containing a list of partition entries. |
|
||||||
|
|
||||||
|
Each partition entry supports these fields:
|
||||||
|
|
||||||
|
| Key | Type | Required | Description |
|
||||||
|
|-----|------|----------|-------------|
|
||||||
|
| `name` | `str` | Yes | Partition name (max 15 characters). |
|
||||||
|
| `type` | `str` | Yes | `"app"` or `"data"`. |
|
||||||
|
| `subtype` | `str` | Yes | Subtype name (see reference tables below). |
|
||||||
|
| `size` | `str` | Yes | Partition size (e.g., `"64K"`, `"1MB"`, `"0x10000"`). |
|
||||||
|
| `offset` | `str` | No | Explicit hex offset. Auto-calculated if omitted. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_partition_custom", {
|
||||||
|
"partition_config": {
|
||||||
|
"partitions": [
|
||||||
|
{"name": "nvs", "type": "data", "subtype": "nvs", "size": "24K"},
|
||||||
|
{"name": "factory", "type": "app", "subtype": "factory", "size": "1MB"},
|
||||||
|
{"name": "storage", "type": "data", "subtype": "littlefs", "size": "512K"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"partition_csv": "# ESP-IDF Partition Table (custom layout)\n# Name, Type, SubType, Offset, Size, Flags\nnvs, data, nvs, 0x9000, 24KB,\nfactory, app, factory, 0x10000, 1MB,\nstorage, data, littlefs, 0x110000, 512KB,\n",
|
||||||
|
"partitions": [
|
||||||
|
{"name": "nvs", "type": "data", "subtype": "nvs", "offset": "0x9000", "size": "24KB"},
|
||||||
|
{"name": "factory", "type": "app", "subtype": "factory", "offset": "0x10000", "size": "1MB"},
|
||||||
|
{"name": "storage", "type": "data", "subtype": "littlefs", "offset": "0x110000", "size": "512KB"}
|
||||||
|
],
|
||||||
|
"total_size": "1560KB",
|
||||||
|
"note": "Flash this CSV with gen_esp32part.py to binary, then write to 0x8000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Validation errors are returned per-partition:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"errors": [
|
||||||
|
"Partition 'badpart': invalid subtype 'unknown' (use: ['ota', 'phy', 'nvs', ...])"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_partition_analyze
|
||||||
|
|
||||||
|
Read and parse the partition table from a connected device. Reads 3072 bytes from flash offset `0x8000`, then decodes the 32-byte binary entries into a structured table.
|
||||||
|
|
||||||
|
Works with both physical devices and QEMU virtual devices.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required** -- returns error if omitted. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_partition_analyze", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"partition_count": 6,
|
||||||
|
"partitions": [
|
||||||
|
{
|
||||||
|
"name": "nvs",
|
||||||
|
"type": "data",
|
||||||
|
"subtype": "nvs",
|
||||||
|
"offset": "0x9000",
|
||||||
|
"size": "24KB",
|
||||||
|
"size_bytes": 24576,
|
||||||
|
"encrypted": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ota_0",
|
||||||
|
"type": "app",
|
||||||
|
"subtype": "ota_0",
|
||||||
|
"offset": "0x10000",
|
||||||
|
"size": "1MB",
|
||||||
|
"size_bytes": 1048576,
|
||||||
|
"encrypted": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the flash is blank or erased, the tool returns an empty partition list:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"partitions": [],
|
||||||
|
"note": "No valid partition entries found (flash may be blank or erased)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary Format
|
||||||
|
|
||||||
|
Each partition table entry is 32 bytes:
|
||||||
|
|
||||||
|
| Offset | Size | Field |
|
||||||
|
|--------|------|-------|
|
||||||
|
| 0 | 2 bytes | Magic number (`0xAA50` little-endian) |
|
||||||
|
| 2 | 1 byte | Type |
|
||||||
|
| 3 | 1 byte | Subtype |
|
||||||
|
| 4 | 4 bytes | Offset (little-endian) |
|
||||||
|
| 8 | 4 bytes | Size (little-endian) |
|
||||||
|
| 12 | 16 bytes | Name (null-terminated ASCII) |
|
||||||
|
| 28 | 4 bytes | Flags (bit 0 = encrypted) |
|
||||||
|
|
||||||
|
Entries with magic `0xFFFF` indicate the end of the table (erased flash).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partition Type Reference
|
||||||
|
|
||||||
|
### App Subtypes
|
||||||
|
|
||||||
|
| Subtype | Value | Description |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| `factory` | `0x00` | Factory application. Boot target when no OTA data exists. |
|
||||||
|
| `ota_0` | `0x10` | OTA slot 0. |
|
||||||
|
| `ota_1` | `0x11` | OTA slot 1. |
|
||||||
|
| `ota_2` | `0x12` | OTA slot 2. |
|
||||||
|
| `ota_3` | `0x13` | OTA slot 3. |
|
||||||
|
| `test` | `0x20` | Test application. Boot target when GPIO test mode is selected. |
|
||||||
|
|
||||||
|
### Data Subtypes
|
||||||
|
|
||||||
|
| Subtype | Value | Description |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| `ota` | `0x00` | OTA selection data. Tracks which OTA slot is active. |
|
||||||
|
| `phy` | `0x01` | PHY calibration data. |
|
||||||
|
| `nvs` | `0x02` | Non-volatile storage (key-value pairs). |
|
||||||
|
| `coredump` | `0x03` | Core dump storage for post-mortem debugging. |
|
||||||
|
| `nvs_keys` | `0x04` | NVS encryption keys. |
|
||||||
|
| `efuse` | `0x05` | eFuse emulation data. |
|
||||||
|
| `fat` | `0x81` | FAT filesystem. |
|
||||||
|
| `spiffs` | `0x82` | SPIFFS filesystem. |
|
||||||
|
| `littlefs` | `0x83` | LittleFS filesystem. |
|
||||||
|
|
||||||
|
### Size Format
|
||||||
|
|
||||||
|
The `size` parameter accepts several formats:
|
||||||
|
|
||||||
|
| Format | Example | Bytes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| Megabytes | `"1MB"`, `"4M"` | 1048576, 4194304 |
|
||||||
|
| Kilobytes | `"64K"`, `"24KB"` | 65536, 24576 |
|
||||||
|
| Hex | `"0x10000"` | 65536 |
|
||||||
|
| Decimal | `"4096"` | 4096 |
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
App-type partitions must be aligned to 64KB (0x10000) boundaries. The custom partition tool handles this automatically by rounding up the offset when needed.
|
||||||
|
</Aside>
|
||||||
225
docs-site/src/content/docs/reference/production-tools.mdx
Normal file
225
docs-site/src/content/docs/reference/production-tools.mdx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
---
|
||||||
|
title: Production Tools
|
||||||
|
description: Factory programming, batch operations, and quality control
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Production Tools component provides 3 tools for factory device programming, parallel batch operations, and quality control testing. These tools are designed for production line workflows where multiple devices need consistent, verified firmware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_factory_program
|
||||||
|
|
||||||
|
Execute a full factory programming pipeline on a single device: erase flash, flash bootloader, flash partition table, flash application firmware, and verify. Each step is tracked individually so failures can be diagnosed.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `program_config` | `dict` | *(required)* | Programming configuration (see fields below). |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
|
||||||
|
### Configuration Fields
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|-----|------|---------|-------------|
|
||||||
|
| `firmware_path` | `str` | *(required)* | Path to the main application binary. |
|
||||||
|
| `address` | `str` | `"0x0"` | Flash address for the application binary. |
|
||||||
|
| `erase_before` | `bool` | `true` | Erase entire flash before programming. |
|
||||||
|
| `verify` | `bool` | `true` | Verify firmware after writing. |
|
||||||
|
| `bootloader` | `str` | `None` | Path to bootloader binary. Skipped if omitted. |
|
||||||
|
| `bootloader_address` | `str` | `"0x1000"` | Flash address for the bootloader. |
|
||||||
|
| `partition_table` | `str` | `None` | Path to partition table binary. Skipped if omitted. |
|
||||||
|
| `partition_table_address` | `str` | `"0x8000"` | Flash address for the partition table. |
|
||||||
|
|
||||||
|
### Pipeline Steps
|
||||||
|
|
||||||
|
1. **Erase flash** (if `erase_before` is `true`)
|
||||||
|
2. **Flash bootloader** (if `bootloader` path is provided)
|
||||||
|
3. **Flash partition table** (if `partition_table` path is provided)
|
||||||
|
4. **Flash application firmware** (with optional verification)
|
||||||
|
|
||||||
|
Each step is executed sequentially. If any step fails, the pipeline stops and reports the failure along with all completed steps.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_factory_program", {
|
||||||
|
"program_config": {
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"address": "0x10000",
|
||||||
|
"erase_before": True,
|
||||||
|
"verify": True,
|
||||||
|
"bootloader": "/build/bootloader.bin",
|
||||||
|
"bootloader_address": "0x1000",
|
||||||
|
"partition_table": "/build/partitions.bin",
|
||||||
|
"partition_table_address": "0x8000"
|
||||||
|
},
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"steps": [
|
||||||
|
{"step": "erase_flash", "success": true, "error": null},
|
||||||
|
{"step": "flash_bootloader", "address": "0x1000", "success": true, "error": null},
|
||||||
|
{"step": "flash_partition_table", "address": "0x8000", "success": true, "error": null},
|
||||||
|
{"step": "flash_firmware", "address": "0x10000", "success": true, "error": null}
|
||||||
|
],
|
||||||
|
"total_time_seconds": 28.45,
|
||||||
|
"firmware_path": "/build/app.bin",
|
||||||
|
"firmware_size_bytes": 524288
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On step failure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Firmware flash failed: Timeout after 300.0s",
|
||||||
|
"steps": [
|
||||||
|
{"step": "erase_flash", "success": true, "error": null},
|
||||||
|
{"step": "flash_bootloader", "address": "0x1000", "success": true, "error": null},
|
||||||
|
{"step": "flash_partition_table", "address": "0x8000", "success": true, "error": null},
|
||||||
|
{"step": "flash_firmware", "address": "0x10000", "success": false, "error": "Timeout after 300.0s"}
|
||||||
|
],
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_batch_program
|
||||||
|
|
||||||
|
Program multiple devices concurrently with the same firmware. Each device goes through the full factory programming pipeline (erase, flash, verify) in parallel using `asyncio.gather`.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `device_list` | `list[str]` | *(required)* | List of serial ports to program (e.g., `["/dev/ttyUSB0", "/dev/ttyUSB1"]`). |
|
||||||
|
| `firmware_path` | `str` | *(required)* | Path to the firmware binary. Same firmware is flashed to all devices. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_batch_program", {
|
||||||
|
"device_list": ["/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2"],
|
||||||
|
"firmware_path": "/build/production_app.bin"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"total_devices": 3,
|
||||||
|
"succeeded": 3,
|
||||||
|
"failed": 0,
|
||||||
|
"total_time_seconds": 32.15,
|
||||||
|
"firmware_path": "/build/production_app.bin",
|
||||||
|
"devices": [
|
||||||
|
{"port": "/dev/ttyUSB0", "success": true, "error": null, "time_seconds": 28.45},
|
||||||
|
{"port": "/dev/ttyUSB1", "success": true, "error": null, "time_seconds": 30.12},
|
||||||
|
{"port": "/dev/ttyUSB2", "success": true, "error": null, "time_seconds": 32.15}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The top-level `success` is `true` only when all devices succeed. Individual device results are always reported in the `devices` array regardless of outcome.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Batch programming applies the same defaults to every device: full flash erase, firmware at address `0x0`, and post-write verification. For more granular control per device, call `esp_factory_program` individually.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_quality_control
|
||||||
|
|
||||||
|
Run quality control tests on a connected device. The basic test suite verifies chip identification, flash identification, and MAC address readability. The extended suite adds a flash read/write connectivity test.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `test_suite` | `str` | `"basic"` | Test suite to run: `"basic"` or `"extended"`. |
|
||||||
|
|
||||||
|
### Test Suites
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Basic (3 tests)">
|
||||||
|
| Test | Command | Checks |
|
||||||
|
|------|---------|--------|
|
||||||
|
| Chip identification | `chip-id` | Chip type and ID |
|
||||||
|
| Flash identification | `flash-id` | Manufacturer and flash size |
|
||||||
|
| MAC address | `read-mac` | Factory MAC address |
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Extended (4 tests)">
|
||||||
|
Includes all basic tests plus:
|
||||||
|
|
||||||
|
| Test | Command | Checks |
|
||||||
|
|------|---------|--------|
|
||||||
|
| Flash read (4KB) | `read-flash` | Flash bus connectivity, data vs erased state |
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_quality_control", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"test_suite": "extended"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"test_suite": "extended",
|
||||||
|
"verdict": "PASS",
|
||||||
|
"tests_run": 4,
|
||||||
|
"tests_passed": 4,
|
||||||
|
"tests_failed": 0,
|
||||||
|
"total_time_seconds": 6.82,
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"test": "chip_identification",
|
||||||
|
"success": true,
|
||||||
|
"chip": "ESP32-S3",
|
||||||
|
"chip_id": "0x00f01d83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test": "flash_identification",
|
||||||
|
"success": true,
|
||||||
|
"manufacturer": "0xef",
|
||||||
|
"flash_size": "4MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test": "mac_address",
|
||||||
|
"success": true,
|
||||||
|
"mac": "aa:bb:cc:dd:ee:ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test": "flash_read_4kb",
|
||||||
|
"success": true,
|
||||||
|
"bytes_read": 4096,
|
||||||
|
"has_data": true,
|
||||||
|
"non_erased_bytes": 3841
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `verdict` field is `"PASS"` when all tests succeed and `"FAIL"` when any test fails. Individual test results are always included in the `tests` array.
|
||||||
268
docs-site/src/content/docs/reference/qemu-manager.mdx
Normal file
268
docs-site/src/content/docs/reference/qemu-manager.mdx
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
---
|
||||||
|
title: QEMU Manager
|
||||||
|
description: Virtual ESP device creation and management
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The QEMU Manager component provides 5 tools for creating and managing virtual ESP devices using Espressif's QEMU fork. Virtual devices expose a TCP serial port that accepts `socket://` URIs, making them fully transparent to all other mcesptool operations.
|
||||||
|
|
||||||
|
QEMU emulation is only available when the Espressif QEMU fork binaries are installed. The server auto-detects binaries in `~/.espressif/tools/qemu-xtensa/` and `~/.espressif/tools/qemu-riscv32/`.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
ESP32-S2 is **not supported** by the Espressif QEMU fork. No machine type exists for this chip. Use a physical ESP32-S2 device or choose a supported virtual chip.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Supported Chips
|
||||||
|
|
||||||
|
| Chip | Architecture | QEMU Machine | Notes |
|
||||||
|
|------|-------------|--------------|-------|
|
||||||
|
| `esp32` | Xtensa | `esp32` | Full support, most mature emulation |
|
||||||
|
| `esp32s3` | Xtensa | `esp32s3` | Shares esp32c3 eFuse device in QEMU |
|
||||||
|
| `esp32c3` | RISC-V | `esp32c3` | Full support |
|
||||||
|
|
||||||
|
### Boot Modes
|
||||||
|
|
||||||
|
| Mode | Behavior |
|
||||||
|
|------|----------|
|
||||||
|
| `download` | GPIO strap forces ROM into serial bootloader. esptool can connect, flash, read, and identify the chip -- same as holding BOOT on real hardware. This is the default. |
|
||||||
|
| `normal` | Boots from flash and runs firmware. Use this to observe application output after flashing. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_qemu_start
|
||||||
|
|
||||||
|
Start a virtual ESP device. Returns a `socket://` URI that works with all esptool-based tools.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `chip_type` | `str` | `"esp32"` | Target chip: `esp32`, `esp32s3`, or `esp32c3`. |
|
||||||
|
| `flash_image` | `str \| None` | `None` | Path to an existing flash image file. Creates a blank (all `0xFF`) flash if omitted. |
|
||||||
|
| `flash_size_mb` | `int` | `4` | Flash size in MB when creating blank images. |
|
||||||
|
| `tcp_port` | `int \| None` | `None` | TCP port for virtual serial. Auto-assigned from the port pool (starting at 5555) if omitted. |
|
||||||
|
| `boot_mode` | `str` | `"download"` | `"download"` for esptool interaction, `"normal"` to boot from flash. |
|
||||||
|
| `extra_args` | `list[str] \| None` | `None` | Additional QEMU command-line arguments. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Start a virtual ESP32 in download mode (default)
|
||||||
|
result = await client.call_tool("esp_qemu_start", {
|
||||||
|
"chip_type": "esp32"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Start ESP32-C3 with an existing flash image in normal boot mode
|
||||||
|
result = await client.call_tool("esp_qemu_start", {
|
||||||
|
"chip_type": "esp32c3",
|
||||||
|
"flash_image": "/images/my_firmware.bin",
|
||||||
|
"boot_mode": "normal"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"chip_type": "esp32",
|
||||||
|
"tcp_port": 5555,
|
||||||
|
"socket_uri": "socket://localhost:5555",
|
||||||
|
"flash_image": "/home/user/.../flash_esp32_5555.bin",
|
||||||
|
"boot_mode": "download",
|
||||||
|
"pid": 12345,
|
||||||
|
"hint": "Use port='socket://localhost:5555' with other esp_ tools to interact with this virtual device"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typical Workflow
|
||||||
|
|
||||||
|
1. Start in download mode: `esp_qemu_start` -- get the socket URI
|
||||||
|
2. Flash firmware: `esp_flash_firmware` with the socket URI as `port`
|
||||||
|
3. Stop the instance: `esp_qemu_stop`
|
||||||
|
4. Restart in normal mode: `esp_qemu_start` with the same `flash_image` and `boot_mode: "normal"`
|
||||||
|
|
||||||
|
### Instance Limits
|
||||||
|
|
||||||
|
The maximum number of concurrent QEMU instances is controlled by the `QEMU_MAX_INSTANCES` configuration (default: 4). Each instance occupies one TCP port from the pool starting at `QEMU_BASE_PORT` (default: 5555).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_qemu_stop
|
||||||
|
|
||||||
|
Stop a running QEMU instance. The flash image is preserved on disk so the instance can be restarted later.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `instance_id` | `str \| None` | `None` | Instance ID to stop (e.g., `"qemu-1"`). Stops **all** running instances if omitted. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Stop a specific instance
|
||||||
|
result = await client.call_tool("esp_qemu_stop", {
|
||||||
|
"instance_id": "qemu-1"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Stop all instances
|
||||||
|
result = await client.call_tool("esp_qemu_stop", {})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"stopped": ["qemu-1"],
|
||||||
|
"remaining": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_qemu_list
|
||||||
|
|
||||||
|
List all QEMU instances with their status, chip type, port, and uptime.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
This tool takes no parameters.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_qemu_list", {})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"chip_type": "esp32",
|
||||||
|
"tcp_port": 5555,
|
||||||
|
"socket_uri": "socket://localhost:5555",
|
||||||
|
"running": true,
|
||||||
|
"pid": 12345,
|
||||||
|
"uptime_seconds": 342.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"instance_id": "qemu-2",
|
||||||
|
"chip_type": "esp32c3",
|
||||||
|
"tcp_port": 5556,
|
||||||
|
"socket_uri": "socket://localhost:5556",
|
||||||
|
"running": false,
|
||||||
|
"pid": 12350,
|
||||||
|
"uptime_seconds": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
"running": 1,
|
||||||
|
"max_instances": 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_qemu_status
|
||||||
|
|
||||||
|
Get detailed status of a specific QEMU instance including uptime, PID, boot mode, and flash/efuse image paths.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `instance_id` | `str \| None` | `None` | Instance to inspect. Returns the first running instance if omitted. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_qemu_status", {
|
||||||
|
"instance_id": "qemu-1"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"chip_type": "esp32",
|
||||||
|
"machine": "esp32",
|
||||||
|
"tcp_port": 5555,
|
||||||
|
"socket_uri": "socket://localhost:5555",
|
||||||
|
"flash_image": "/home/user/.../flash_esp32_5555.bin",
|
||||||
|
"flash_size_mb": 4,
|
||||||
|
"boot_mode": "download",
|
||||||
|
"running": true,
|
||||||
|
"pid": 12345,
|
||||||
|
"started_at": 1708732800.0,
|
||||||
|
"uptime_seconds": 342.5,
|
||||||
|
"extra_args": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_qemu_flash
|
||||||
|
|
||||||
|
Write a firmware binary directly into a stopped QEMU instance's flash image file. This is an offline operation that patches the raw binary image at the specified offset.
|
||||||
|
|
||||||
|
For most workflows, prefer using `esp_flash_firmware` with the instance's socket URI while it is running in download mode. That approach uses esptool's full flash protocol including verification. Use this tool when you need direct image manipulation -- for example, pre-loading a merged binary without starting the emulator.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `instance_id` | `str` | *(required)* | Target QEMU instance. Must be stopped. |
|
||||||
|
| `firmware_path` | `str` | *(required)* | Path to the firmware binary to write. |
|
||||||
|
| `address` | `str` | `"0x0"` | Flash address offset as hex string. |
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The instance must be stopped before using this tool. If the instance is running, the tool returns an error. Use `esp_qemu_stop` first, then flash, then restart with `esp_qemu_start`.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Stop instance, write firmware, restart in normal mode
|
||||||
|
await client.call_tool("esp_qemu_stop", {"instance_id": "qemu-1"})
|
||||||
|
|
||||||
|
result = await client.call_tool("esp_qemu_flash", {
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"firmware_path": "/build/merged_firmware.bin",
|
||||||
|
"address": "0x0"
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.call_tool("esp_qemu_start", {
|
||||||
|
"chip_type": "esp32",
|
||||||
|
"flash_image": result["flash_image"],
|
||||||
|
"boot_mode": "normal"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"firmware_path": "/build/merged_firmware.bin",
|
||||||
|
"address": "0x00000000",
|
||||||
|
"bytes_written": 524288,
|
||||||
|
"flash_image": "/home/user/.../flash_esp32_5555.bin",
|
||||||
|
"hint": "Use esp_qemu_start to restart the instance with the new firmware"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scan Integration
|
||||||
|
|
||||||
|
Running QEMU instances automatically appear in `esp_scan_ports` results with `"source": "qemu"`. The server cross-wires the QEMU Manager with the Chip Control component so that port scanning discovers both physical and virtual devices.
|
||||||
160
docs-site/src/content/docs/reference/resources.mdx
Normal file
160
docs-site/src/content/docs/reference/resources.mdx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
title: MCP Resources
|
||||||
|
description: Real-time MCP resources exposed by mcesptool
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
mcesptool exposes 3 MCP resources that provide real-time server state without requiring tool invocations. Resources are read-only and can be accessed by any MCP client using the `read_resource` protocol method.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Resources are lighter-weight than tools -- they do not run subprocess commands or interact with hardware. Use them for dashboard displays, status polling, or client-side configuration checks.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp://server/status
|
||||||
|
|
||||||
|
Real-time server status including uptime, component count, and operating mode.
|
||||||
|
|
||||||
|
### URI
|
||||||
|
|
||||||
|
```
|
||||||
|
esp://server/status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.read_resource("esp://server/status")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "running",
|
||||||
|
"uptime_seconds": 1234.56,
|
||||||
|
"components_loaded": 9,
|
||||||
|
"production_mode": false,
|
||||||
|
"last_updated": 1708732800.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `status` | `str` | Always `"running"` while the server is operational. |
|
||||||
|
| `uptime_seconds` | `float` | Seconds since server startup. |
|
||||||
|
| `components_loaded` | `int` | Number of components successfully initialized. |
|
||||||
|
| `production_mode` | `bool` | Whether the server is running in production mode. |
|
||||||
|
| `last_updated` | `float` | Unix timestamp of the response. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp://config
|
||||||
|
|
||||||
|
Current server configuration as a serialized dictionary. Reflects the active runtime configuration including any environment variable overrides.
|
||||||
|
|
||||||
|
### URI
|
||||||
|
|
||||||
|
```
|
||||||
|
esp://config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.read_resource("esp://config")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"esptool_path": "esptool",
|
||||||
|
"esp_idf_path": "/home/user/esp/esp-idf",
|
||||||
|
"project_roots": ["/home/user/esp_projects"],
|
||||||
|
"default_baud_rate": 460800,
|
||||||
|
"connection_timeout": 30,
|
||||||
|
"enable_stub_flasher": true,
|
||||||
|
"max_concurrent_operations": 5,
|
||||||
|
"production_mode": false,
|
||||||
|
"idf_available": true,
|
||||||
|
"qemu_available": true,
|
||||||
|
"qemu_xtensa_path": "/home/user/.espressif/tools/qemu-xtensa/.../qemu-system-xtensa",
|
||||||
|
"qemu_riscv_path": "/home/user/.espressif/tools/qemu-riscv32/.../qemu-system-riscv32"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `esptool_path` | `str` | Path or command name for esptool. |
|
||||||
|
| `esp_idf_path` | `str \| null` | ESP-IDF installation path, or null if not detected. |
|
||||||
|
| `project_roots` | `list[str]` | Directories scanned for ESP projects. |
|
||||||
|
| `default_baud_rate` | `int` | Default serial baud rate for connections. |
|
||||||
|
| `connection_timeout` | `int` | Default connection timeout in seconds. |
|
||||||
|
| `enable_stub_flasher` | `bool` | Whether the ROM stub loader is enabled. |
|
||||||
|
| `max_concurrent_operations` | `int` | Maximum parallel esptool operations. |
|
||||||
|
| `production_mode` | `bool` | Production mode flag. |
|
||||||
|
| `idf_available` | `bool` | Whether ESP-IDF was detected and is usable. |
|
||||||
|
| `qemu_available` | `bool` | Whether at least one QEMU binary is available. |
|
||||||
|
| `qemu_xtensa_path` | `str \| null` | Path to qemu-system-xtensa binary. |
|
||||||
|
| `qemu_riscv_path` | `str \| null` | Path to qemu-system-riscv32 binary. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp://capabilities
|
||||||
|
|
||||||
|
Server capabilities listing supported chips, available feature categories, and integration availability. Useful for clients that need to adapt their UI based on what the server can do.
|
||||||
|
|
||||||
|
### URI
|
||||||
|
|
||||||
|
```
|
||||||
|
esp://capabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.read_resource("esp://capabilities")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"esp_chip_support": [
|
||||||
|
"ESP32", "ESP32-S2", "ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP8266"
|
||||||
|
],
|
||||||
|
"flash_operations": ["read", "write", "erase", "verify", "encrypt"],
|
||||||
|
"partition_features": ["custom_tables", "ota_support", "nvs_management"],
|
||||||
|
"security_features": ["efuse_management", "secure_boot", "flash_encryption"],
|
||||||
|
"production_features": ["factory_programming", "batch_operations", "quality_control"],
|
||||||
|
"debugging_features": ["memory_dump", "performance_profiling", "diagnostic_reports"],
|
||||||
|
"esp_idf_integration": true,
|
||||||
|
"host_applications": true,
|
||||||
|
"qemu_emulation": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `esp_chip_support` | `list[str]` | Chip families supported by esptool operations. |
|
||||||
|
| `flash_operations` | `list[str]` | Available flash operation types. |
|
||||||
|
| `partition_features` | `list[str]` | Partition management capabilities. |
|
||||||
|
| `security_features` | `list[str]` | Security-related capabilities. |
|
||||||
|
| `production_features` | `list[str]` | Production line capabilities. |
|
||||||
|
| `debugging_features` | `list[str]` | Diagnostic and debugging capabilities. |
|
||||||
|
| `esp_idf_integration` | `bool` | Whether ESP-IDF tools are available. |
|
||||||
|
| `host_applications` | `bool` | Whether host application building is available (requires ESP-IDF). |
|
||||||
|
| `qemu_emulation` | `bool` | Whether QEMU virtual device support is available. |
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
The `esp_chip_support` list reflects what esptool can communicate with. QEMU emulation supports a smaller subset -- see the [QEMU Manager supported chips table](/reference/qemu-manager/#supported-chips).
|
||||||
|
</Aside>
|
||||||
262
docs-site/src/content/docs/reference/security.mdx
Normal file
262
docs-site/src/content/docs/reference/security.mdx
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
---
|
||||||
|
title: Security
|
||||||
|
description: eFuse management, flash encryption, and security audit tools
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The Security Manager component provides 4 tools for auditing device security posture, checking flash encryption status, and reading or burning eFuse values. These tools wrap `esptool` and `espefuse` CLI commands as async subprocesses.
|
||||||
|
|
||||||
|
<Aside type="danger">
|
||||||
|
eFuse burn operations are **irreversible** on physical hardware. Burned bits cannot be unset. Always test security configurations on [QEMU virtual devices](/reference/qemu-manager/) first, where eFuses reset when the instance is recreated.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_security_audit
|
||||||
|
|
||||||
|
Perform a comprehensive security audit of a connected ESP device. Gathers chip identity, flash encryption status, secure boot state, JTAG status, and eFuse summary into a single structured report.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_security_audit", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"chip_id": "0x00f01d83",
|
||||||
|
"security_info": {
|
||||||
|
"Secure Boot": "disabled",
|
||||||
|
"Flash Encryption": "disabled"
|
||||||
|
},
|
||||||
|
"efuse_summary": {
|
||||||
|
"MAC": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"FLASH_CRYPT_CNT": "0",
|
||||||
|
"ABS_DONE_0": "0",
|
||||||
|
"JTAG_DISABLE": "0"
|
||||||
|
},
|
||||||
|
"security_fuses": {
|
||||||
|
"FLASH_CRYPT_CNT": "0",
|
||||||
|
"ABS_DONE_0": "0",
|
||||||
|
"JTAG_DISABLE": "0"
|
||||||
|
},
|
||||||
|
"posture": {
|
||||||
|
"flash_encryption": "disabled",
|
||||||
|
"secure_boot": "disabled",
|
||||||
|
"jtag": "enabled (vulnerable)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `posture` object provides a quick summary of the three primary security mechanisms. The `security_fuses` object contains the raw eFuse values for the following security-relevant fields:
|
||||||
|
|
||||||
|
| eFuse | Controls |
|
||||||
|
|-------|----------|
|
||||||
|
| `FLASH_CRYPT_CNT` | Flash encryption enable/disable counter |
|
||||||
|
| `ABS_DONE_0` | Secure Boot v1 permanent enable |
|
||||||
|
| `ABS_DONE_1` | Secure Boot v2 permanent enable |
|
||||||
|
| `JTAG_DISABLE` | JTAG debug interface disable |
|
||||||
|
| `DISABLE_DL_ENCRYPT` | Disable flash encryption in download mode |
|
||||||
|
| `DISABLE_DL_DECRYPT` | Disable flash decryption in download mode |
|
||||||
|
| `DISABLE_DL_CACHE` | Disable flash cache in download mode |
|
||||||
|
| `FLASH_CRYPT_CONFIG` | Flash encryption configuration |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_enable_flash_encryption
|
||||||
|
|
||||||
|
Check the current flash encryption status and provide guidance on enabling it. This tool reads the relevant eFuses but does not perform the actual burn -- use `esp_efuse_burn` for that.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `key_file` | `str \| None` | `None` | Path to an encryption key file. Stored in the response for reference only. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_enable_flash_encryption", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
When encryption is not enabled:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"flash_encryption_enabled": false,
|
||||||
|
"FLASH_CRYPT_CNT": "0",
|
||||||
|
"FLASH_CRYPT_CONFIG": "0",
|
||||||
|
"message": "Flash encryption is NOT enabled. To enable, you need to: 1) Generate or provide an encryption key, 2) Burn FLASH_CRYPT_CNT and FLASH_CRYPT_CONFIG eFuses, 3) Flash encrypted firmware. WARNING: This is irreversible on real hardware. Test on QEMU first."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When encryption is already active:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"flash_encryption_enabled": true,
|
||||||
|
"FLASH_CRYPT_CNT": "1",
|
||||||
|
"FLASH_CRYPT_CONFIG": "0xf",
|
||||||
|
"message": "Flash encryption is already enabled on this device."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_efuse_read
|
||||||
|
|
||||||
|
Read eFuse values from a device. Without a specific `efuse_name`, returns the full human-readable summary from `espefuse summary`. With `efuse_name`, returns just that field's value.
|
||||||
|
|
||||||
|
eFuses are one-time-programmable bits that control chip security, MAC address, calibration data, and more. Reading is non-destructive.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
| `efuse_name` | `str \| None` | `None` | Specific eFuse to read (e.g., `"MAC"`, `"FLASH_CRYPT_CNT"`). Returns full summary if omitted. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Read All">
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_efuse_read", {
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Read Specific">
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_efuse_read", {
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"efuse_name": "MAC"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
Full summary:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"efuses": {
|
||||||
|
"MAC": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"FLASH_CRYPT_CNT": "0",
|
||||||
|
"ABS_DONE_0": "0",
|
||||||
|
"JTAG_DISABLE": "0",
|
||||||
|
"ADC_VREF": "1100"
|
||||||
|
},
|
||||||
|
"raw_output": "EFUSE_NAME (BLOCK0) Description = value R/W (hex)\n..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Specific eFuse:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"efuse_name": "MAC",
|
||||||
|
"value": "aa:bb:cc:dd:ee:ff"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When a requested eFuse is not found, the response includes the list of available names:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "eFuse 'NONEXISTENT' not found",
|
||||||
|
"available_efuses": ["MAC", "FLASH_CRYPT_CNT", "ABS_DONE_0", "..."],
|
||||||
|
"port": "/dev/ttyUSB0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
eFuse name matching is case-insensitive. Both `"mac"` and `"MAC"` will find the `MAC` eFuse.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_efuse_burn
|
||||||
|
|
||||||
|
Permanently program an eFuse bit field on the device. This operation is **irreversible** on real hardware. The tool reads the eFuse value before and after the burn to confirm the change.
|
||||||
|
|
||||||
|
Uses `espefuse burn-efuse` with the `--do-not-confirm` flag because confirmation is handled at the MCP client level.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `efuse_name` | `str` | *(required)* | Name of the eFuse to burn (e.g., `"JTAG_DISABLE"`). |
|
||||||
|
| `value` | `str` | *(required)* | Value to burn (e.g., `"1"`, `"0x1"`). |
|
||||||
|
| `port` | `str \| None` | `None` | Serial port or `socket://` URI. **Required**. |
|
||||||
|
|
||||||
|
<Aside type="danger">
|
||||||
|
This operation is **permanently destructive** on physical hardware. Burned eFuse bits cannot be reset or changed. There is no undo.
|
||||||
|
|
||||||
|
On QEMU virtual devices, eFuses are stored in a temporary file and reset when the instance is recreated. Use QEMU for testing security configurations before applying them to real hardware.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Common eFuses
|
||||||
|
|
||||||
|
| Name | Effect |
|
||||||
|
|------|--------|
|
||||||
|
| `JTAG_DISABLE` | Permanently disables JTAG debugging interface |
|
||||||
|
| `FLASH_CRYPT_CNT` | Enables flash encryption (odd bit count = encrypted) |
|
||||||
|
| `ABS_DONE_0` | Permanently enables Secure Boot v1 |
|
||||||
|
| `DISABLE_DL_ENCRYPT` | Disables flash encryption in UART download mode |
|
||||||
|
| `DISABLE_DL_DECRYPT` | Disables flash decryption in UART download mode |
|
||||||
|
| `DISABLE_DL_CACHE` | Disables flash cache in UART download mode |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Disable JTAG on a QEMU device (safe to test)
|
||||||
|
result = await client.call_tool("esp_efuse_burn", {
|
||||||
|
"efuse_name": "JTAG_DISABLE",
|
||||||
|
"value": "1",
|
||||||
|
"port": "socket://localhost:5555"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "socket://localhost:5555",
|
||||||
|
"efuse_name": "JTAG_DISABLE",
|
||||||
|
"value_requested": "1",
|
||||||
|
"value_before": "0",
|
||||||
|
"value_after": "1",
|
||||||
|
"warning": "eFuse burn is IRREVERSIBLE on real hardware"
|
||||||
|
}
|
||||||
|
```
|
||||||
223
docs-site/src/content/docs/reference/server-tools.mdx
Normal file
223
docs-site/src/content/docs/reference/server-tools.mdx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
---
|
||||||
|
title: Server Tools
|
||||||
|
description: Server information, tool listing, and health check
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
The server exposes 3 meta-tools for inspecting the running mcesptool instance: server information, tool discovery, and environment health checks. These tools do not interact with hardware and are always available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_server_info
|
||||||
|
|
||||||
|
Get comprehensive information about the running server including version, uptime, loaded components, tool count, and available capabilities.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
This tool takes no parameters.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_server_info", {})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_name": "MCP ESPTool Server",
|
||||||
|
"version": "2026.02.23",
|
||||||
|
"uptime_seconds": 1234.56,
|
||||||
|
"configuration": {
|
||||||
|
"esptool_path": "esptool",
|
||||||
|
"esp_idf_path": "/home/user/esp/esp-idf",
|
||||||
|
"project_roots": ["/home/user/esp_projects"],
|
||||||
|
"default_baud_rate": 460800,
|
||||||
|
"connection_timeout": 30,
|
||||||
|
"enable_stub_flasher": true,
|
||||||
|
"max_concurrent_operations": 5,
|
||||||
|
"production_mode": false,
|
||||||
|
"idf_available": true,
|
||||||
|
"qemu_available": true,
|
||||||
|
"qemu_xtensa_path": "/home/user/.espressif/tools/qemu-xtensa/.../qemu-system-xtensa",
|
||||||
|
"qemu_riscv_path": "/home/user/.espressif/tools/qemu-riscv32/.../qemu-system-riscv32"
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
"chip_control", "flash_manager", "partition_manager",
|
||||||
|
"security_manager", "firmware_builder", "ota_manager",
|
||||||
|
"production_tools", "diagnostics", "qemu_manager"
|
||||||
|
],
|
||||||
|
"total_tools": 44,
|
||||||
|
"total_resources": 3,
|
||||||
|
"esp_idf_available": true,
|
||||||
|
"production_mode": false,
|
||||||
|
"capabilities": {
|
||||||
|
"chip_detection": true,
|
||||||
|
"flash_operations": true,
|
||||||
|
"partition_management": true,
|
||||||
|
"security_features": true,
|
||||||
|
"ota_updates": true,
|
||||||
|
"factory_programming": true,
|
||||||
|
"host_applications": true,
|
||||||
|
"qemu_emulation": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
| Capability | When available |
|
||||||
|
|-----------|----------------|
|
||||||
|
| `chip_detection` | Always |
|
||||||
|
| `flash_operations` | Always |
|
||||||
|
| `partition_management` | Always |
|
||||||
|
| `security_features` | Always |
|
||||||
|
| `ota_updates` | Always |
|
||||||
|
| `factory_programming` | Always |
|
||||||
|
| `host_applications` | When ESP-IDF is detected |
|
||||||
|
| `qemu_emulation` | When QEMU binaries are detected |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_list_tools
|
||||||
|
|
||||||
|
List all available MCP tools, optionally filtered by component category. Useful for tool discovery and documentation generation.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `category` | `str \| None` | `None` | Filter by category name. Returns all categories if omitted. |
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
|
||||||
|
| Category | Tools |
|
||||||
|
|----------|-------|
|
||||||
|
| `chip_control` | `esp_detect_chip`, `esp_connect_advanced`, `esp_reset_chip`, `esp_load_test_firmware` |
|
||||||
|
| `flash_operations` | `esp_flash_firmware`, `esp_flash_read`, `esp_flash_erase`, `esp_flash_backup` |
|
||||||
|
| `partition_management` | `esp_partition_create_ota`, `esp_partition_custom`, `esp_partition_analyze` |
|
||||||
|
| `security` | `esp_security_audit`, `esp_enable_flash_encryption`, `esp_efuse_read`, `esp_efuse_burn` |
|
||||||
|
| `firmware` | `esp_elf_to_binary`, `esp_firmware_analyze` |
|
||||||
|
| `ota` | `esp_ota_package_create`, `esp_ota_deploy`, `esp_ota_rollback` |
|
||||||
|
| `production` | `esp_factory_program`, `esp_batch_program`, `esp_quality_control` |
|
||||||
|
| `diagnostics` | `esp_memory_dump`, `esp_performance_profile`, `esp_diagnostic_report` |
|
||||||
|
| `qemu_emulation` | `esp_qemu_start`, `esp_qemu_stop`, `esp_qemu_list`, `esp_qemu_status`, `esp_qemu_flash` |
|
||||||
|
|
||||||
|
The `qemu_emulation` category is only present when QEMU binaries are available. If ESP-IDF is detected, an additional `esp_idf` category appears with IDF-specific tools.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# List all categories
|
||||||
|
result = await client.call_tool("esp_list_tools", {})
|
||||||
|
|
||||||
|
# List tools in a specific category
|
||||||
|
result = await client.call_tool("esp_list_tools", {
|
||||||
|
"category": "flash_operations"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
All categories:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total_categories": 9,
|
||||||
|
"categories": {
|
||||||
|
"chip_control": ["esp_detect_chip", "esp_connect_advanced", "..."],
|
||||||
|
"flash_operations": ["esp_flash_firmware", "esp_flash_read", "..."],
|
||||||
|
"...": "..."
|
||||||
|
},
|
||||||
|
"total_tools": 44
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Filtered by category:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"category": "flash_operations",
|
||||||
|
"tools": ["esp_flash_firmware", "esp_flash_read", "esp_flash_erase", "esp_flash_backup"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Unknown category:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Unknown category: nonexistent",
|
||||||
|
"available_categories": ["chip_control", "flash_operations", "..."]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## esp_health_check
|
||||||
|
|
||||||
|
Perform an environment health check. Verifies esptool availability, ESP-IDF installation, and project directory accessibility. With `detailed: true`, also checks the health of each loaded component.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `detailed` | `bool` | `False` | Include per-component health checks. |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await client.call_tool("esp_health_check", {
|
||||||
|
"detailed": True
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": 1708732800.0,
|
||||||
|
"checks": {
|
||||||
|
"esptool": {
|
||||||
|
"available": true,
|
||||||
|
"version": "esptool.py v4.8"
|
||||||
|
},
|
||||||
|
"esp_idf": {
|
||||||
|
"available": true,
|
||||||
|
"path": "/home/user/esp/esp-idf"
|
||||||
|
},
|
||||||
|
"project_roots": {
|
||||||
|
"configured": 2,
|
||||||
|
"accessible": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"chip_control": {"status": "healthy", "esptool_path": "esptool"},
|
||||||
|
"flash_manager": {"status": "healthy", "note": "Flash manager ready"},
|
||||||
|
"partition_manager": {"status": "healthy", "note": "Partition manager ready"},
|
||||||
|
"security_manager": {"status": "healthy", "note": "Security manager ready"},
|
||||||
|
"firmware_builder": {"status": "healthy", "note": "Firmware builder ready"},
|
||||||
|
"ota_manager": {"status": "healthy", "note": "OTA manager ready"},
|
||||||
|
"production_tools": {"status": "healthy", "note": "Production tools ready"},
|
||||||
|
"diagnostics": {"status": "healthy", "note": "Diagnostics ready"},
|
||||||
|
"qemu_manager": {
|
||||||
|
"status": "healthy",
|
||||||
|
"qemu_xtensa_available": true,
|
||||||
|
"qemu_riscv_available": true,
|
||||||
|
"running_instances": 0,
|
||||||
|
"max_instances": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Status
|
||||||
|
|
||||||
|
| Status | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| `healthy` | All checks passed. |
|
||||||
|
| `degraded` | Some non-critical checks failed (e.g., ESP-IDF not installed). |
|
||||||
|
|
||||||
|
The `components` object is only present when `detailed` is `True`. The `failed_checks` array is only present when `status` is `"degraded"`.
|
||||||
157
docs-site/src/content/docs/tutorials/first-flash.mdx
Normal file
157
docs-site/src/content/docs/tutorials/first-flash.mdx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
title: First Flash
|
||||||
|
description: Flash firmware to an ESP device end-to-end
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside } from "@astrojs/starlight/components";
|
||||||
|
|
||||||
|
This tutorial walks through the complete flash cycle: detect the chip, back up existing firmware, write new firmware, verify the result, and read serial output. Each step uses an mcesptool MCP tool that you invoke through your LLM client.
|
||||||
|
|
||||||
|
These same tools work identically with QEMU virtual devices. Wherever you see a `/dev/ttyUSB0` port, a `socket://localhost:5555` URI works the same way.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Detect the chip**
|
||||||
|
|
||||||
|
Start by confirming the device is connected and identifying what you are working with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_detect_chip(port="/dev/ttyUSB0", detailed=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response includes chip type, MAC address, flash size, and crystal frequency. Note the flash size -- you will need it if you ever restore the backup.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"connection_time_seconds": 0.91,
|
||||||
|
"chip_info": {
|
||||||
|
"chip_type": "ESP32",
|
||||||
|
"mac_address": "24:6f:28:xx:xx:xx",
|
||||||
|
"flash_size": "4MB",
|
||||||
|
"crystal_frequency": "40MHz",
|
||||||
|
"features": ["WiFi", "BT", "Dual Core", "240MHz"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Back up existing flash**
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Always back up before flashing new firmware. If something goes wrong, you can restore the original firmware from this backup file. This reads the entire flash contents, so it takes a minute or two depending on flash size and baud rate.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_flash_backup(
|
||||||
|
backup_path="/tmp/esp32-backup.bin",
|
||||||
|
port="/dev/ttyUSB0"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool reads the full flash (starting from address `0x0` by default, including the bootloader) and writes it to the specified path. You can restore this backup later with `esp_flash_firmware` at address `0x0`.
|
||||||
|
|
||||||
|
3. **Flash new firmware**
|
||||||
|
|
||||||
|
Write your compiled firmware binary to the device:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_flash_firmware(
|
||||||
|
firmware_path="/path/to/my_app.bin",
|
||||||
|
port="/dev/ttyUSB0",
|
||||||
|
address="0x10000",
|
||||||
|
verify=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `address` parameter controls where in flash the binary lands. Common addresses:
|
||||||
|
|
||||||
|
| Address | Contents |
|
||||||
|
|------------|-------------------------|
|
||||||
|
| `0x0` | Full merged image |
|
||||||
|
| `0x1000` | Bootloader (ESP32) |
|
||||||
|
| `0x8000` | Partition table |
|
||||||
|
| `0x10000` | Application firmware |
|
||||||
|
|
||||||
|
With `verify=True` (the default), esptool reads back the written data and compares hashes after the write completes.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
For a complete firmware stack (bootloader + partition table + app), use `esp_flash_multi` to write all binaries in a single connection:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_flash_multi(
|
||||||
|
port="/dev/ttyUSB0",
|
||||||
|
files=[
|
||||||
|
{"address": "0x1000", "path": "bootloader.bin"},
|
||||||
|
{"address": "0x8000", "path": "partitions.bin"},
|
||||||
|
{"address": "0x10000", "path": "my_app.bin"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
4. **Verify the flash**
|
||||||
|
|
||||||
|
If you want an independent verification pass (separate from the write-time check), use `esp_verify_flash`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_verify_flash(
|
||||||
|
firmware_path="/path/to/my_app.bin",
|
||||||
|
port="/dev/ttyUSB0",
|
||||||
|
address="0x10000"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This reads the flash region back and compares it byte-for-byte against your local file. The response indicates whether the contents match or reports the mismatch location.
|
||||||
|
|
||||||
|
5. **Monitor serial output**
|
||||||
|
|
||||||
|
After flashing, observe the device boot and application output:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_serial_monitor(
|
||||||
|
port="/dev/ttyUSB0",
|
||||||
|
baud_rate=115200,
|
||||||
|
duration_seconds=10.0,
|
||||||
|
reset_on_connect=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
With `reset_on_connect=True`, the device is hardware-reset before capturing begins, so you see the full boot sequence from the ROM bootloader through your application startup.
|
||||||
|
|
||||||
|
The tool captures output for the specified duration (up to 30 seconds) and returns all lines as text:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"duration_seconds": 10.03,
|
||||||
|
"reset_performed": true,
|
||||||
|
"line_count": 47,
|
||||||
|
"output": "ets Jul 29 2019 12:21:46\nrst:0x1 (POWERON_RESET)..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Restoring a backup
|
||||||
|
|
||||||
|
If you need to roll back, flash the backup file to address `0x0`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_flash_firmware(
|
||||||
|
firmware_path="/tmp/esp32-backup.bin",
|
||||||
|
port="/dev/ttyUSB0",
|
||||||
|
address="0x0"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This overwrites the entire flash with the previously saved image, restoring the device to its original state.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
- [First QEMU Session](/tutorials/first-qemu-session/) -- practice the same workflow without physical hardware
|
||||||
|
- [Flash Operations Reference](/reference/flash-operations/) -- full documentation for all flash tools
|
||||||
|
- [Partition Management Reference](/reference/partition-management/) -- create and manage partition tables
|
||||||
214
docs-site/src/content/docs/tutorials/first-qemu-session.mdx
Normal file
214
docs-site/src/content/docs/tutorials/first-qemu-session.mdx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
title: First QEMU Session
|
||||||
|
description: Create a virtual ESP32 device without any physical hardware
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside } from "@astrojs/starlight/components";
|
||||||
|
|
||||||
|
QEMU emulation gives you a virtual ESP32 that behaves like a real device over a serial connection. The Espressif QEMU fork emulates the chip's ROM bootloader, flash memory, eFuses, and GPIO strapping -- enough for esptool to connect, flash firmware, and read back results. No USB cable, no devkit, no wiring.
|
||||||
|
|
||||||
|
Every tool in mcesptool that accepts a `port` parameter works with QEMU socket URIs (`socket://localhost:PORT`) exactly as it works with physical serial ports.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
When you start a QEMU instance, mcesptool launches the Espressif QEMU fork with a virtual flash image and exposes a TCP socket that speaks the ESP serial bootloader protocol. The returned `socket://localhost:PORT` URI is a drop-in replacement for `/dev/ttyUSB0` in any tool invocation.
|
||||||
|
|
||||||
|
Two boot modes are available:
|
||||||
|
|
||||||
|
- **download** -- the virtual chip starts in serial bootloader mode (GPIO straps held), ready for esptool commands like flash, read, and chip detect
|
||||||
|
- **normal** -- the virtual chip boots from its flash image and runs whatever firmware is stored there
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
QEMU binaries ship as part of the ESP-IDF toolchain. Install them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 $IDF_PATH/tools/idf_tools.py install qemu-xtensa qemu-riscv32
|
||||||
|
```
|
||||||
|
|
||||||
|
After installation, activate the tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
. $IDF_PATH/export.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
mcesptool detects QEMU availability automatically at startup. If the binaries are on your `PATH`, QEMU tools are enabled with no additional configuration.
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
The Espressif QEMU fork supports ESP32, ESP32-S3, and ESP32-C3. **ESP32-S2 is not supported** -- no machine type exists in the fork for that chip variant.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Walkthrough
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Start a virtual device in download mode**
|
||||||
|
|
||||||
|
Create an ESP32 instance with a blank 4 MB flash image, ready for esptool interaction:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_qemu_start(
|
||||||
|
chip_type="esp32",
|
||||||
|
flash_size_mb=4,
|
||||||
|
boot_mode="download"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The response includes the socket URI and instance ID you will use for subsequent commands:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"chip_type": "esp32",
|
||||||
|
"tcp_port": 5555,
|
||||||
|
"socket_uri": "socket://localhost:5555",
|
||||||
|
"flash_image": "/path/to/flash_esp32_5555.bin",
|
||||||
|
"boot_mode": "download",
|
||||||
|
"pid": 12345
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Detect the virtual chip**
|
||||||
|
|
||||||
|
Use the socket URI with `esp_detect_chip` just as you would a USB port:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_detect_chip(
|
||||||
|
port="socket://localhost:5555",
|
||||||
|
detailed=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The virtual device reports as a real ESP32 with MAC address, flash size, and features. This confirms the QEMU instance is accepting esptool connections.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
Running QEMU instances appear automatically in `esp_scan_ports()` results alongside any physical USB devices. You do not need to remember the port -- just scan.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
3. **Flash firmware to the virtual device**
|
||||||
|
|
||||||
|
Write a firmware binary using the same tool as physical hardware:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_flash_firmware(
|
||||||
|
firmware_path="/path/to/my_app.bin",
|
||||||
|
port="socket://localhost:5555",
|
||||||
|
address="0x10000"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete firmware stack, use `esp_flash_multi`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_flash_multi(
|
||||||
|
port="socket://localhost:5555",
|
||||||
|
files=[
|
||||||
|
{"address": "0x1000", "path": "bootloader.bin"},
|
||||||
|
{"address": "0x8000", "path": "partitions.bin"},
|
||||||
|
{"address": "0x10000", "path": "my_app.bin"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify the flash contents**
|
||||||
|
|
||||||
|
Confirm the write was successful:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_verify_flash(
|
||||||
|
firmware_path="/path/to/my_app.bin",
|
||||||
|
port="socket://localhost:5555",
|
||||||
|
address="0x10000"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Stop the instance**
|
||||||
|
|
||||||
|
Shut down the download-mode instance. The flash image file is preserved on disk:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_qemu_stop(instance_id="qemu-1")
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Restart in normal boot mode**
|
||||||
|
|
||||||
|
Boot the virtual device from flash to run your firmware. Point `flash_image` at the same file that was written to in the previous steps:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_qemu_start(
|
||||||
|
chip_type="esp32",
|
||||||
|
flash_image="/path/to/flash_esp32_5555.bin",
|
||||||
|
boot_mode="normal"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The device now boots from flash and executes the firmware you flashed in step 3.
|
||||||
|
|
||||||
|
7. **Clean up**
|
||||||
|
|
||||||
|
When you are finished, stop all running instances:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_qemu_stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
Calling `esp_qemu_stop` with no `instance_id` stops every running QEMU instance. Flash image files remain on disk for future sessions.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Supported chip types
|
||||||
|
|
||||||
|
| Chip | Architecture | QEMU Machine | Status |
|
||||||
|
|------------|-------------|--------------|-----------|
|
||||||
|
| ESP32 | Xtensa | `esp32` | Supported |
|
||||||
|
| ESP32-S3 | Xtensa | `esp32s3` | Supported |
|
||||||
|
| ESP32-C3 | RISC-V | `esp32c3` | Supported |
|
||||||
|
| ESP32-S2 | Xtensa | -- | Not supported |
|
||||||
|
|
||||||
|
## Managing multiple instances
|
||||||
|
|
||||||
|
mcesptool can run several QEMU instances in parallel, each on its own TCP port. Use `esp_qemu_list` to see all instances and their status:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_qemu_list()
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"instances": [
|
||||||
|
{
|
||||||
|
"instance_id": "qemu-1",
|
||||||
|
"chip_type": "esp32",
|
||||||
|
"tcp_port": 5555,
|
||||||
|
"socket_uri": "socket://localhost:5555",
|
||||||
|
"running": true,
|
||||||
|
"uptime_seconds": 142.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"instance_id": "qemu-2",
|
||||||
|
"chip_type": "esp32c3",
|
||||||
|
"tcp_port": 5556,
|
||||||
|
"socket_uri": "socket://localhost:5556",
|
||||||
|
"running": true,
|
||||||
|
"uptime_seconds": 87.1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
"running": 2,
|
||||||
|
"max_instances": 4
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed status on a specific instance including PID, flash image path, and boot mode:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_qemu_status(instance_id="qemu-1")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
- [First Flash](/tutorials/first-flash/) -- the same workflow applied to physical hardware
|
||||||
|
- [QEMU Manager Reference](/reference/qemu-manager/) -- full documentation for all QEMU tools
|
||||||
|
- [Chip Control Reference](/reference/chip-control/) -- detection, scanning, and reset tools
|
||||||
157
docs-site/src/content/docs/tutorials/getting-started.mdx
Normal file
157
docs-site/src/content/docs/tutorials/getting-started.mdx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
title: Getting Started
|
||||||
|
description: Install mcesptool and connect to your first ESP device
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Tabs, TabItem, Aside } from "@astrojs/starlight/components";
|
||||||
|
|
||||||
|
This guide walks you through installing mcesptool, adding it to Claude Code, and verifying connectivity with an ESP device. The whole process takes about five minutes.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Python 3.10 or later** -- mcesptool uses modern type annotations and async features
|
||||||
|
- **esptool** installed and available on your `PATH` (ships with ESP-IDF, or install standalone via `pip install esptool`)
|
||||||
|
- **An ESP32 or ESP8266 device** connected over USB, _or_ willingness to use [QEMU emulation](/tutorials/first-qemu-session/) instead
|
||||||
|
|
||||||
|
<Aside>
|
||||||
|
If you do not have physical hardware, skip ahead to the [First QEMU Session](/tutorials/first-qemu-session/) tutorial after completing installation. Every tool that works with a USB port also works with a QEMU `socket://` URI.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Install mcesptool**
|
||||||
|
|
||||||
|
The fastest way to run the server is with `uvx`, which downloads and runs the package in an isolated environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx mcesptool
|
||||||
|
```
|
||||||
|
|
||||||
|
To add it as a project dependency instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv add mcesptool
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add to Claude Code**
|
||||||
|
|
||||||
|
Register mcesptool as an MCP server so Claude Code can invoke its tools:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Published (PyPI)">
|
||||||
|
```bash
|
||||||
|
claude mcp add mcesptool -- uvx mcesptool
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Local Development">
|
||||||
|
```bash
|
||||||
|
claude mcp add mcesptool -- uv run --directory /path/to/mcp-esptool mcesptool
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
3. **Verify esptool is available**
|
||||||
|
|
||||||
|
mcesptool shells out to `esptool` for all chip communication. Confirm it is installed:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Linux">
|
||||||
|
```bash
|
||||||
|
which esptool.py && esptool.py version
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="macOS">
|
||||||
|
```bash
|
||||||
|
which esptool.py && esptool.py version
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Windows">
|
||||||
|
```powershell
|
||||||
|
where esptool.py
|
||||||
|
esptool.py version
|
||||||
|
```
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
If esptool is not found, install it with `pip install esptool` or activate your ESP-IDF environment with `. $IDF_PATH/export.sh`.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
4. **Connect your device**
|
||||||
|
|
||||||
|
Plug in your ESP board over USB. On Linux, the device typically appears at `/dev/ttyUSB0` or `/dev/ttyACM0`. On macOS, look for `/dev/cu.usbserial-*` or `/dev/cu.SLAB_USBtoUART`.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Linux">
|
||||||
|
```bash
|
||||||
|
ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
If the port exists but is not accessible, add yourself to the `dialout` group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG dialout $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Log out and back in for the group change to take effect.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="macOS">
|
||||||
|
```bash
|
||||||
|
ls /dev/cu.usb*
|
||||||
|
```
|
||||||
|
|
||||||
|
macOS generally does not require extra permissions for USB serial devices. If you see no devices, install the appropriate USB-to-UART driver for your board (CP210x or CH340).
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Windows">
|
||||||
|
Open Device Manager and look under **Ports (COM & LPT)** for a COM port. Note the port number (e.g., `COM3`). You may need to install the CP210x or CH340 driver from your board manufacturer.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
5. **Detect your chip**
|
||||||
|
|
||||||
|
Use `esp_detect_chip` to confirm mcesptool can communicate with the device:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# MCP tool invocation
|
||||||
|
esp_detect_chip(port="/dev/ttyUSB0")
|
||||||
|
```
|
||||||
|
|
||||||
|
A successful response looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baud_rate": 115200,
|
||||||
|
"connection_time_seconds": 0.84,
|
||||||
|
"chip_info": {
|
||||||
|
"chip_type": "ESP32-S3",
|
||||||
|
"mac_address": "dc:da:0c:xx:xx:xx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more detail, pass `detailed=True` to include flash size, crystal frequency, and feature flags:
|
||||||
|
|
||||||
|
```python
|
||||||
|
esp_detect_chip(port="/dev/ttyUSB0", detailed=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If `esp_detect_chip` fails:
|
||||||
|
|
||||||
|
- **"No ESP devices found"** -- check your USB cable. Some cables are charge-only and do not carry data.
|
||||||
|
- **Permission denied** -- on Linux, ensure your user is in the `dialout` group (see step 4 above).
|
||||||
|
- **Timeout** -- hold the BOOT button on your board while the tool connects, then release. Some boards require manual bootloader entry.
|
||||||
|
|
||||||
|
Run `esp_scan_ports()` to see all detected serial ports and their status.
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
- [First Flash](/tutorials/first-flash/) -- flash firmware to a device end-to-end
|
||||||
|
- [First QEMU Session](/tutorials/first-qemu-session/) -- create a virtual ESP32 without any hardware
|
||||||
32
docs-site/src/styles/global.css
Normal file
32
docs-site/src/styles/global.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Teal accent theme — PCB traces, embedded hardware aesthetic */
|
||||||
|
:root {
|
||||||
|
--sl-color-accent-low: #042f2e;
|
||||||
|
--sl-color-accent: #0d9488;
|
||||||
|
--sl-color-accent-high: #99f6e4;
|
||||||
|
|
||||||
|
--sl-color-white: #f8fafc;
|
||||||
|
--sl-color-gray-1: #e2e8f0;
|
||||||
|
--sl-color-gray-2: #cbd5e1;
|
||||||
|
--sl-color-gray-3: #94a3b8;
|
||||||
|
--sl-color-gray-4: #64748b;
|
||||||
|
--sl-color-gray-5: #334155;
|
||||||
|
--sl-color-gray-6: #1e293b;
|
||||||
|
--sl-color-black: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--sl-color-accent-low: #ccfbf1;
|
||||||
|
--sl-color-accent: #0d9488;
|
||||||
|
--sl-color-accent-high: #134e4a;
|
||||||
|
|
||||||
|
--sl-color-white: #0f172a;
|
||||||
|
--sl-color-gray-1: #1e293b;
|
||||||
|
--sl-color-gray-2: #334155;
|
||||||
|
--sl-color-gray-3: #64748b;
|
||||||
|
--sl-color-gray-4: #94a3b8;
|
||||||
|
--sl-color-gray-5: #e2e8f0;
|
||||||
|
--sl-color-gray-6: #f1f5f9;
|
||||||
|
--sl-color-black: #f8fafc;
|
||||||
|
}
|
||||||
3
docs-site/tsconfig.json
Normal file
3
docs-site/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict"
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mcesptool"
|
name = "mcesptool"
|
||||||
version = "2025.09.28.1"
|
version = "2026.02.23"
|
||||||
description = "FastMCP server for ESP32/ESP8266 development with esptool integration"
|
description = "FastMCP server for ESP32/ESP8266 development with esptool integration"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@ -31,14 +31,13 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastmcp>=2.12.4", # FastMCP framework
|
"fastmcp>=3.0.2,<4", # FastMCP framework (v3)
|
||||||
"pyserial>=3.5", # Serial communication
|
"pyserial>=3.5", # Serial communication
|
||||||
"pyserial-asyncio>=0.6", # Async serial support
|
"pyserial-asyncio>=0.6", # Async serial support
|
||||||
"thefuzz[speedup]>=0.22.1", # Fuzzy string matching
|
"thefuzz[speedup]>=0.22.1", # Fuzzy string matching
|
||||||
"pydantic>=2.0.0", # Data validation
|
"pydantic>=2.11.7", # Data validation
|
||||||
"click>=8.0.0", # CLI framework
|
"click>=8.1.0", # CLI framework
|
||||||
"rich>=13.0.0", # Rich console output
|
"rich>=13.9.4", # Rich console output
|
||||||
"asyncio-mqtt>=0.16.0", # MQTT for coordination
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@ -5,7 +5,7 @@ A comprehensive FastMCP server for ESP32/ESP8266 development with esptool integr
|
|||||||
Provides AI-powered ESP development workflows with production-grade capabilities.
|
Provides AI-powered ESP development workflows with production-grade capabilities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "2025.09.28.1"
|
__version__ = "2026.02.23"
|
||||||
__author__ = "ESP Development Team"
|
__author__ = "ESP Development Team"
|
||||||
__description__ = "FastMCP server for ESP32/ESP8266 development with esptool integration"
|
__description__ = "FastMCP server for ESP32/ESP8266 development with esptool integration"
|
||||||
|
|
||||||
|
|||||||
@ -118,7 +118,7 @@ class ESPToolServer:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"server_name": "MCP ESPTool Server",
|
"server_name": "MCP ESPTool Server",
|
||||||
"version": "2025.09.28.1",
|
"version": "2026.02.23",
|
||||||
"uptime_seconds": round(uptime, 2),
|
"uptime_seconds": round(uptime, 2),
|
||||||
"configuration": self.config.to_dict(),
|
"configuration": self.config.to_dict(),
|
||||||
"components": list(self.components.keys()),
|
"components": list(self.components.keys()),
|
||||||
@ -375,7 +375,7 @@ class ESPToolServer:
|
|||||||
@click.option("--debug", "-d", is_flag=True, help="Enable debug logging")
|
@click.option("--debug", "-d", is_flag=True, help="Enable debug logging")
|
||||||
@click.option("--production", "-p", is_flag=True, help="Run in production mode")
|
@click.option("--production", "-p", is_flag=True, help="Run in production mode")
|
||||||
@click.option("--port", default=8080, help="Server port (for future HTTP interface)")
|
@click.option("--port", default=8080, help="Server port (for future HTTP interface)")
|
||||||
@click.version_option(version="2025.09.28.1")
|
@click.version_option(version="2026.02.23")
|
||||||
def main(config: str | None, debug: bool, production: bool, port: int) -> None:
|
def main(config: str | None, debug: bool, production: bool, port: int) -> None:
|
||||||
"""
|
"""
|
||||||
FastMCP ESP Development Server
|
FastMCP ESP Development Server
|
||||||
@ -403,7 +403,7 @@ def main(config: str | None, debug: bool, production: bool, port: int) -> None:
|
|||||||
# Display startup banner
|
# Display startup banner
|
||||||
console.print("\n[bold blue]🚀 FastMCP ESP Development Server[/bold blue]")
|
console.print("\n[bold blue]🚀 FastMCP ESP Development Server[/bold blue]")
|
||||||
console.print("[dim]AI-powered ESP32/ESP8266 development workflows[/dim]")
|
console.print("[dim]AI-powered ESP32/ESP8266 development workflows[/dim]")
|
||||||
console.print("[dim]Version: 2025.09.28.1[/dim]")
|
console.print("[dim]Version: 2026.02.23[/dim]")
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
# Create and run server
|
# Create and run server
|
||||||
|
|||||||
641
uv.lock
generated
641
uv.lock
generated
@ -2,6 +2,18 @@ version = 1
|
|||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiofile"
|
||||||
|
version = "3.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "caio" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -26,17 +38,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "asyncio-mqtt"
|
|
||||||
version = "0.16.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "paho-mqtt" },
|
|
||||||
]
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/64/c8a8c2ed51f3c1f4b8d2f244424d3bad3fbd4333eb01589c6b00a6dd2c04/asyncio_mqtt-0.16.2-py3-none-any.whl", hash = "sha256:fe70ea2c648b248779a7ff3d9218262cdd739083743dfaa7c0d52ba458a8ad71", size = 17380, upload-time = "2023-06-26T19:46:48.342Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "25.3.0"
|
version = "25.3.0"
|
||||||
@ -48,14 +49,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "authlib"
|
name = "authlib"
|
||||||
version = "1.6.4"
|
version = "1.6.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/bb/73a1f1c64ee527877f64122422dafe5b87a846ccf4ac933fe21bcbb8fee8/authlib-1.6.4.tar.gz", hash = "sha256:104b0442a43061dc8bc23b133d1d06a2b0a9c2e3e33f34c4338929e816287649", size = 164046, upload-time = "2025-09-17T09:59:23.897Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6b/6c/c88eac87468c607f88bc24df1f3b31445ee6fc9ba123b09e666adf687cd9/authlib-1.6.8.tar.gz", hash = "sha256:41ae180a17cf672bc784e4a518e5c82687f1fe1e98b0cafaeda80c8e4ab2d1cb", size = 165074, upload-time = "2026-02-14T04:02:17.941Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/aa/91355b5f539caf1b94f0e66ff1e4ee39373b757fce08204981f7829ede51/authlib-1.6.4-py2.py3-none-any.whl", hash = "sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796", size = 243076, upload-time = "2025-09-17T09:59:22.259Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/73/f7084bf12755113cd535ae586782ff3a6e710bfbe6a0d13d1c2f81ffbbfa/authlib-1.6.8-py2.py3-none-any.whl", hash = "sha256:97286fd7a15e6cfefc32771c8ef9c54f0ed58028f1322de6a2a7c969c3817888", size = 244116, upload-time = "2026-02-14T04:02:15.579Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -67,6 +68,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backports-tarfile"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beartype"
|
||||||
|
version = "0.22.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "25.9.0"
|
version = "25.9.0"
|
||||||
@ -102,6 +121,34 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "7.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "caio"
|
||||||
|
version = "0.9.25"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2025.8.3"
|
version = "2025.8.3"
|
||||||
@ -458,18 +505,19 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cyclopts"
|
name = "cyclopts"
|
||||||
version = "3.24.0"
|
version = "4.6.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "attrs" },
|
{ name = "attrs" },
|
||||||
{ name = "docstring-parser", marker = "python_full_version < '4'" },
|
{ name = "docstring-parser" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
{ name = "rich-rst" },
|
{ name = "rich-rst" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/30/ca/7782da3b03242d5f0a16c20371dff99d4bd1fedafe26bc48ff82e42be8c9/cyclopts-3.24.0.tar.gz", hash = "sha256:de6964a041dfb3c57bf043b41e68c43548227a17de1bad246e3a0bfc5c4b7417", size = 76131, upload-time = "2025-09-08T15:40:57.75Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/49/5c/88a4068c660a096bbe87efc5b7c190080c9e86919c36ec5f092cb08d852f/cyclopts-4.6.0.tar.gz", hash = "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5", size = 162724, upload-time = "2026-02-23T15:44:49.286Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/8b/2c95f0645c6f40211896375e6fa51f504b8ccb29c21f6ae661fe87ab044e/cyclopts-3.24.0-py3-none-any.whl", hash = "sha256:809d04cde9108617106091140c3964ee6fceb33cecdd537f7ffa360bde13ed71", size = 86154, upload-time = "2025-09-08T15:40:56.41Z" },
|
{ url = "https://files.pythonhosted.org/packages/8f/eb/1e8337755a70dc7d7ff10a73dc8f20e9352c9ad6c2256ed863ac95cd3539/cyclopts-4.6.0-py3-none-any.whl", hash = "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", size = 200518, upload-time = "2026-02-23T15:44:47.854Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -568,24 +616,33 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastmcp"
|
name = "fastmcp"
|
||||||
version = "2.12.4"
|
version = "3.0.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "authlib" },
|
{ name = "authlib" },
|
||||||
{ name = "cyclopts" },
|
{ name = "cyclopts" },
|
||||||
{ name = "exceptiongroup" },
|
{ name = "exceptiongroup" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "jsonref" },
|
||||||
|
{ name = "jsonschema-path" },
|
||||||
{ name = "mcp" },
|
{ name = "mcp" },
|
||||||
{ name = "openapi-core" },
|
|
||||||
{ name = "openapi-pydantic" },
|
{ name = "openapi-pydantic" },
|
||||||
|
{ name = "opentelemetry-api" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
{ name = "pyperclip" },
|
{ name = "pyperclip" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
|
{ name = "uvicorn" },
|
||||||
|
{ name = "watchfiles" },
|
||||||
|
{ name = "websockets" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/b2/57845353a9bc63002995a982e66f3d0be4ec761e7bcb89e7d0638518d42a/fastmcp-2.12.4.tar.gz", hash = "sha256:b55fe89537038f19d0f4476544f9ca5ac171033f61811cc8f12bdeadcbea5016", size = 7167745, upload-time = "2025-09-26T16:43:27.71Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/11/6b/1a7ec89727797fb07ec0928e9070fa2f45e7b35718e1fe01633a34c35e45/fastmcp-3.0.2.tar.gz", hash = "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", size = 17239351, upload-time = "2026-02-22T16:32:28.843Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/c7/562ff39f25de27caec01e4c1e88cbb5fcae5160802ba3d90be33165df24f/fastmcp-2.12.4-py3-none-any.whl", hash = "sha256:56188fbbc1a9df58c537063f25958c57b5c4d715f73e395c41b51550b247d140", size = 329090, upload-time = "2025-09-26T16:43:25.314Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/5a/f410a9015cfde71adf646dab4ef2feae49f92f34f6050fcfb265eb126b30/fastmcp-3.0.2-py3-none-any.whl", hash = "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028", size = 606268, upload-time = "2026-02-22T16:32:30.992Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -673,6 +730,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "8.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "zipp" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@ -683,12 +752,57 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "isodate"
|
name = "jaraco-classes"
|
||||||
version = "0.7.2"
|
version = "3.4.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" }
|
dependencies = [
|
||||||
|
{ name = "more-itertools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jaraco-context"
|
||||||
|
version = "6.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "backports-tarfile", marker = "python_full_version < '3.12'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jaraco-functools"
|
||||||
|
version = "4.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "more-itertools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jeepney"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonref"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -743,48 +857,21 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy-object-proxy"
|
name = "keyring"
|
||||||
version = "1.12.0"
|
version = "25.7.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" }
|
dependencies = [
|
||||||
|
{ name = "importlib-metadata", marker = "python_full_version < '3.12'" },
|
||||||
|
{ name = "jaraco-classes" },
|
||||||
|
{ name = "jaraco-context" },
|
||||||
|
{ name = "jaraco-functools" },
|
||||||
|
{ name = "jeepney", marker = "sys_platform == 'linux'" },
|
||||||
|
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -799,97 +886,11 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "markupsafe"
|
|
||||||
version = "3.0.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcesptool"
|
name = "mcesptool"
|
||||||
version = "2025.9.28.1"
|
version = "2026.2.23"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asyncio-mqtt" },
|
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "fastmcp" },
|
{ name = "fastmcp" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
@ -927,17 +928,16 @@ testing = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "asyncio-mqtt", specifier = ">=0.16.0" },
|
|
||||||
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },
|
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },
|
||||||
{ name = "click", specifier = ">=8.0.0" },
|
{ name = "click", specifier = ">=8.1.0" },
|
||||||
{ name = "factory-boy", marker = "extra == 'testing'", specifier = ">=3.3.0" },
|
{ name = "factory-boy", marker = "extra == 'testing'", specifier = ">=3.3.0" },
|
||||||
{ name = "fastmcp", specifier = ">=2.12.4" },
|
{ name = "fastmcp", specifier = ">=3.0.2,<4" },
|
||||||
{ name = "gunicorn", marker = "extra == 'production'", specifier = ">=21.0.0" },
|
{ name = "gunicorn", marker = "extra == 'production'", specifier = ">=21.0.0" },
|
||||||
{ name = "kconfiglib", marker = "extra == 'idf'", specifier = ">=14.1.0" },
|
{ name = "kconfiglib", marker = "extra == 'idf'", specifier = ">=14.1.0" },
|
||||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.0" },
|
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5.0" },
|
||||||
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0.0" },
|
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0.0" },
|
||||||
{ name = "prometheus-client", marker = "extra == 'production'", specifier = ">=0.19.0" },
|
{ name = "prometheus-client", marker = "extra == 'production'", specifier = ">=0.19.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||||
{ name = "pyserial", specifier = ">=3.5" },
|
{ name = "pyserial", specifier = ">=3.5" },
|
||||||
{ name = "pyserial-asyncio", specifier = ">=0.6" },
|
{ name = "pyserial-asyncio", specifier = ">=0.6" },
|
||||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2" },
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.2" },
|
||||||
@ -946,7 +946,7 @@ requires-dist = [
|
|||||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
||||||
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" },
|
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" },
|
||||||
{ name = "pytest-xdist", marker = "extra == 'testing'", specifier = ">=3.0.0" },
|
{ name = "pytest-xdist", marker = "extra == 'testing'", specifier = ">=3.0.0" },
|
||||||
{ name = "rich", specifier = ">=13.0.0" },
|
{ name = "rich", specifier = ">=13.9.4" },
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.13.2" },
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.13.2" },
|
||||||
{ name = "thefuzz", extras = ["speedup"], specifier = ">=0.22.1" },
|
{ name = "thefuzz", extras = ["speedup"], specifier = ">=0.22.1" },
|
||||||
{ name = "uvloop", marker = "extra == 'production'", specifier = ">=0.19.0" },
|
{ name = "uvloop", marker = "extra == 'production'", specifier = ">=0.19.0" },
|
||||||
@ -956,7 +956,7 @@ provides-extras = ["dev", "idf", "testing", "production"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcp"
|
name = "mcp"
|
||||||
version = "1.15.0"
|
version = "1.26.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
@ -965,15 +965,18 @@ dependencies = [
|
|||||||
{ name = "jsonschema" },
|
{ name = "jsonschema" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
|
{ name = "pyjwt", extra = ["crypto"] },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "sse-starlette" },
|
{ name = "sse-starlette" },
|
||||||
{ name = "starlette" },
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/9e/e65114795f359f314d7061f4fcb50dfe60026b01b52ad0b986b4631bf8bb/mcp-1.15.0.tar.gz", hash = "sha256:5bda1f4d383cf539d3c035b3505a3de94b20dbd7e4e8b4bd071e14634eeb2d72", size = 469622, upload-time = "2025-09-25T15:39:51.995Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/82/4d0df23d5ff5bb982a59ad597bc7cb9920f2650278ccefb8e0d85c5ce3d4/mcp-1.15.0-py3-none-any.whl", hash = "sha256:314614c8addc67b663d6c3e4054db0a5c3dedc416c24ef8ce954e203fdc2333d", size = 166963, upload-time = "2025-09-25T15:39:50.538Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1057,26 +1060,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openapi-core"
|
|
||||||
version = "0.19.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "isodate" },
|
|
||||||
{ name = "jsonschema" },
|
|
||||||
{ name = "jsonschema-path" },
|
|
||||||
{ name = "more-itertools" },
|
|
||||||
{ name = "openapi-schema-validator" },
|
|
||||||
{ name = "openapi-spec-validator" },
|
|
||||||
{ name = "parse" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
{ name = "werkzeug" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openapi-pydantic"
|
name = "openapi-pydantic"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -1090,32 +1073,16 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openapi-schema-validator"
|
name = "opentelemetry-api"
|
||||||
version = "0.6.3"
|
version = "1.39.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jsonschema" },
|
{ name = "importlib-metadata" },
|
||||||
{ name = "jsonschema-specifications" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "rfc3339-validator" },
|
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "openapi-spec-validator"
|
|
||||||
version = "0.7.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "jsonschema" },
|
|
||||||
{ name = "jsonschema-path" },
|
|
||||||
{ name = "lazy-object-proxy" },
|
|
||||||
{ name = "openapi-schema-validator" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1127,24 +1094,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paho-mqtt"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parse"
|
|
||||||
version = "1.20.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathable"
|
name = "pathable"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@ -1215,6 +1164,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" },
|
{ url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "py-key-value-aio"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "beartype" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
filetree = [
|
||||||
|
{ name = "aiofile" },
|
||||||
|
{ name = "anyio" },
|
||||||
|
]
|
||||||
|
keyring = [
|
||||||
|
{ name = "keyring" },
|
||||||
|
]
|
||||||
|
memory = [
|
||||||
|
{ name = "cachetools" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.23"
|
version = "2.23"
|
||||||
@ -1354,6 +1328,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyjwt"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
crypto = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyperclip"
|
name = "pyperclip"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@ -1517,6 +1505,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pywin32-ctypes"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
@ -1709,18 +1706,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rfc3339-validator"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "six" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "14.1.0"
|
version = "14.1.0"
|
||||||
@ -1909,12 +1894,16 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "secretstorage"
|
||||||
version = "1.17.0"
|
version = "3.5.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "jeepney" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2135,13 +2124,181 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "werkzeug"
|
name = "watchfiles"
|
||||||
version = "3.1.1"
|
version = "1.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markupsafe" },
|
{ name = "anyio" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websockets"
|
||||||
|
version = "16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user