Compare commits

...

6 Commits

Author SHA1 Message Date
c6a55a5e8f Merge feature/smart-backup: partition-aware flash backup and restore
Adds esp_smart_backup and esp_smart_restore tools with chunked reads,
adaptive sizing, skip-empty detection, gzip compression, SHA256
verification, and an esp_backup_plan prompt for pre-flight planning.

Hardened against 11 code review findings including path traversal
protection, atomic manifest writes, and timeout caps.
2026-02-25 17:01:22 -07:00
ca893f1bbd Add smart backup tools to reference docs
Also fix Flash Operations count (6 tools, not 7).
2026-02-25 16:17:39 -07:00
a07b3a0fd3 Harden smart backup against review findings
- Path traversal protection: validate manifest file paths stay within
  backup directory (prevents ../../etc/passwd in crafted manifests)
- Input validation: reject chunk_mb outside 1-16 range
- Memory: _is_chunk_empty uses count() instead of allocating comparison buffer
- Type safety: accept bytes | bytearray in hash/empty-check helpers
- Timeout cap: _compute_chunk_timeout capped at 240s (MCP limit is 300s)
- Manifest schema: validate required fields before processing
- Disk full: wrap file writes in OSError handling with partial_backup path
- Atomic manifest: write via temp+rename to prevent corrupt state
- Decompression cache: avoid decompressing twice during restore
- Tempfile race: close NamedTemporaryFile before Path.write_bytes
- Unused variable: replace sum(1 for v in ...) with len()
2026-02-25 16:17:09 -07:00
9fa314dae9 Add partition-aware smart backup and restore
Adds SmartBackupManager component that reads the partition table first,
then backs up each partition individually with chunked reads to stay
under the 300s MCP timeout. Solves timeout issues on large flash devices
(e.g. 16 MB ESP32-P4).

Tools: esp_smart_backup, esp_smart_restore
Prompt: esp_backup_plan (estimates backup time per partition)

Features:
- Adaptive chunk sizing based on measured throughput
- Skip-empty detection (0xFF regions)
- Optional gzip compression per partition
- SHA256 manifest for integrity verification
- Dry-run mode for time estimation
- Progress reporting via MCP context
2026-02-25 16:09:10 -07:00
6c71fbc319 Fix flash operations tool count in reference index
Heading said 7 tools but the table correctly listed 6.
2026-02-25 16:06:16 -07:00
f8dfa90fba Add live product catalog to docs site
Espressif product data fetched at build time via Content Layer API
and rendered as browsable Starlight pages. Alpine.js provides
client-side filtering by series, type, connectivity, and status.

- Products content collection with inline API loader + Zod schema
- Filterable browse page at /products/ with card grid
- Individual spec sheet pages at /products/[id]/ (94 products)
- Separate CSS with dark/light theme support
- Sidebar section linking to catalog
- Cross-references to MCP tool reference docs
2026-02-25 16:02:43 -07:00
13 changed files with 2647 additions and 4 deletions

View File

@ -1,5 +1,6 @@
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import alpinejs from "@astrojs/alpinejs";
import tailwindcss from "@tailwindcss/vite";
import sitemap from "@astrojs/sitemap";
@ -8,6 +9,7 @@ export default defineConfig({
telemetry: false,
devToolbar: { enabled: false },
integrations: [
alpinejs(),
starlight({
title: "mcesptool",
description:
@ -20,7 +22,7 @@ export default defineConfig({
href: "https://git.supported.systems/MCP/mcesptool",
},
],
customCss: ["./src/styles/global.css"],
customCss: ["./src/styles/global.css", "./src/styles/products.css"],
sidebar: [
{
label: "Tutorials",
@ -82,12 +84,22 @@ export default defineConfig({
label: "Server Tools",
slug: "reference/server-tools",
},
{
label: "Product Catalog",
slug: "reference/product-catalog",
},
],
},
{ label: "Resources", slug: "reference/resources" },
{ label: "Configuration", slug: "reference/configuration" },
],
},
{
label: "Product Catalog",
items: [
{ label: "Browse Products", link: "/products/" },
],
},
{
label: "Explanation",
autogenerate: { directory: "explanation" },

View File

@ -8,14 +8,26 @@
"name": "mcesptool-docs",
"version": "0.0.1",
"dependencies": {
"@astrojs/alpinejs": "^0.4.9",
"@astrojs/sitemap": "^3.3.1",
"@astrojs/starlight": "^0.37.6",
"@tailwindcss/vite": "^4.1.3",
"alpinejs": "^3.15.8",
"astro": "^5.7.10",
"sharp": "^0.33.0",
"tailwindcss": "^4.1.3"
}
},
"node_modules/@astrojs/alpinejs": {
"version": "0.4.9",
"resolved": "https://registry.npmjs.org/@astrojs/alpinejs/-/alpinejs-0.4.9.tgz",
"integrity": "sha512-fvKBAugn7yIngEKfdk6vL3ZlcVKtQvFXCZznG28OikGanKN5W+PkRPIdKaW/0gThRU2FyCemgzyHgyFjsH8dTA==",
"license": "MIT",
"peerDependencies": {
"@types/alpinejs": "^3.0.0",
"alpinejs": "^3.0.0"
}
},
"node_modules/@astrojs/compiler": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz",
@ -2026,6 +2038,13 @@
"vite": "^5.2.0 || ^6 || ^7"
}
},
"node_modules/@types/alpinejs": {
"version": "3.13.11",
"resolved": "https://registry.npmjs.org/@types/alpinejs/-/alpinejs-3.13.11.tgz",
"integrity": "sha512-3KhGkDixCPiLdL3Z/ok1GxHwLxEWqQOKJccgaQL01wc0EVM2tCTaqlC3NIedmxAXkVzt/V6VTM8qPgnOHKJ1MA==",
"license": "MIT",
"peer": true
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@ -2125,6 +2144,21 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@vue/reactivity": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.1.5"
}
},
"node_modules/@vue/shared": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@ -2146,6 +2180,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/alpinejs": {
"version": "3.15.8",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz",
"integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",

View File

@ -9,9 +9,11 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/alpinejs": "^0.4.9",
"@astrojs/sitemap": "^3.3.1",
"@astrojs/starlight": "^0.37.6",
"@tailwindcss/vite": "^4.1.3",
"alpinejs": "^3.15.8",
"astro": "^5.7.10",
"sharp": "^0.33.0",
"tailwindcss": "^4.1.3"

View File

@ -1,7 +1,140 @@
import { defineCollection } from "astro:content";
import { defineCollection, z } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
function parseMemoryField(value: string | number | null | undefined): [number, string] {
if (typeof value === "number") return [value, ""];
if (!value || ["N/A", "NA", "-", "\u2014"].includes(value)) return [0, ""];
const parts = value.split(",").map((s) => s.trim());
const size = parseInt(parts[0], 10);
return [isNaN(size) ? 0 : size, parts[1] || ""];
}
function parseSramKb(value: string | number | null | undefined): number {
if (typeof value === "number") return value;
if (!value || ["N/A", "NA", "-", "\u2014"].includes(value)) return 0;
const n = parseInt(value.split(",")[0].trim(), 10);
return isNaN(n) ? 0 : n;
}
function hasCapability(value: string | null | undefined): boolean {
if (!value) return false;
const v = value.trim().toLowerCase();
return !["", "n/a", "na", "-", "\u2014", "no", "0"].includes(v);
}
function normalizeStatus(value: string): string {
const mapping: Record<string, string> = {
"mass production": "Mass Production",
nrnd: "NRND",
eol: "EOL",
replaced: "Replaced",
sample: "Sample",
};
return mapping[value.trim().toLowerCase()] || value.trim();
}
const products = defineCollection({
loader: async () => {
const resp = await fetch(
"https://products.espressif.com/api/user/products?language=en",
);
if (!resp.ok) {
throw new Error(`Espressif API returned ${resp.status}`);
}
const data = await resp.json();
const results: any[] = data.results || [];
return results.map((p: any) => {
const [flashMb, flashType] = parseMemoryField(p.flash);
const [psramMb, psramType] = parseMemoryField(p.psram);
const id = (p.name || "unknown")
.toLowerCase()
.replace(/[^a-z0-9-]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
return {
id,
name: p.name || "",
name_new: p.nameNew || "",
type: p.type || "",
series: p.seriesName || "",
status: normalizeStatus(p.status || ""),
mpn: p.mpn || "",
dimensions: p.dimensions || "",
wifi: p.wifi || "",
wifi6: p.wifi6 || "",
bluetooth: p.bluetooth || "",
thread_zigbee: p.threadZigbee || "",
pins: String(p.pins || ""),
frequency_mhz: String(p.freq || ""),
sram_kb: parseSramKb(p.sram),
rom_kb: String(p.rom || ""),
flash_mb: flashMb,
flash_type: flashType,
psram_mb: psramMb,
psram_type: psramType,
gpio: parseInt(String(p.gpio || 0), 10) || 0,
operating_temp: p.operatingTemp || "",
voltage_range: p.voltageRange || "",
antenna: p.antenna || "",
size_type: p.sizeType || "",
release_time: p.releaseTime || "",
idf_supports: p.idfSupports || "",
spq: String(p.spq || ""),
moq: String(p.moq || ""),
lead_time: p.leadTime || "",
eccn_code: p.eccnCode || "",
hs_code: p.hsCode || "",
replaced_by: p.replacedByName || "",
has_wifi: hasCapability(p.wifi),
has_bluetooth: hasCapability(p.bluetooth),
has_thread_zigbee: hasCapability(p.threadZigbee),
};
});
},
schema: z.object({
name: z.string(),
name_new: z.string(),
type: z.string(),
series: z.string(),
status: z.string(),
mpn: z.string(),
dimensions: z.string(),
wifi: z.string(),
wifi6: z.string(),
bluetooth: z.string(),
thread_zigbee: z.string(),
pins: z.string(),
frequency_mhz: z.string(),
sram_kb: z.number(),
rom_kb: z.string(),
flash_mb: z.number(),
flash_type: z.string(),
psram_mb: z.number(),
psram_type: z.string(),
gpio: z.number(),
operating_temp: z.string(),
voltage_range: z.string(),
antenna: z.string(),
size_type: z.string(),
release_time: z.string(),
idf_supports: z.string(),
spq: z.string(),
moq: z.string(),
lead_time: z.string(),
eccn_code: z.string(),
hs_code: z.string(),
replaced_by: z.string(),
has_wifi: z.boolean(),
has_bluetooth: z.boolean(),
has_thread_zigbee: z.boolean(),
}),
});
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
products,
};

View File

@ -25,7 +25,7 @@ All tools follow a consistent pattern: they return a JSON object with a `success
| `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)
### Flash Operations (6 tools)
| Tool | Description |
|------|-------------|
@ -103,6 +103,24 @@ All tools follow a consistent pattern: they return a JSON object with a `success
| `esp_list_tools` | List all tools by category |
| `esp_health_check` | Environment health check |
### Smart Backup (2 tools + 1 prompt)
| Tool | Description |
|------|-------------|
| `esp_smart_backup` | Partition-aware flash backup with chunked reads, skip-empty, and compression |
| `esp_smart_restore` | Restore a partition-aware backup with SHA256 verification before flashing |
| `esp_backup_plan` *(prompt)* | Generate an informed backup plan based on device partition layout |
### Product Catalog (5 tools)
| Tool | Description |
|------|-------------|
| `esp_product_search` | Search products by series, type, connectivity, memory, GPIO, temp, status |
| `esp_product_info` | Full spec sheet for a product (fuzzy-matched by name or MPN) |
| `esp_chip_compare` | Side-by-side comparison of 2-4 products |
| `esp_product_recommend` | Recommendations based on use case description |
| `esp_product_availability` | Filter by production status, lead time, and MOQ |
## Additional References
<CardGrid>

View File

@ -0,0 +1,326 @@
---
title: Product Catalog
description: Chip and module discovery, comparison, and procurement planning
---
import { Aside } from '@astrojs/starlight/components';
The Product Catalog component provides 5 tools for discovering Espressif chips and modules, comparing specifications, getting recommendations, and checking availability. Data is fetched from [Espressif's public product API](https://products.espressif.com/) and cached in-memory.
<Aside type="note">
The catalog is **lazy-loaded** on first use and cached with a 24-hour TTL. The initial call may take a few seconds while the catalog is fetched. Subsequent calls within the same server session are instant.
</Aside>
<Aside type="tip">
Product lookups use **fuzzy matching** on name and MPN (Manufacturer Part Number). You don't need the exact product name -- partial matches and close spellings work. For example, `"S3-WROOM"` will match `"ESP32-S3-WROOM-1-N16R8"`.
</Aside>
---
## esp_product_search
Search the Espressif product catalog by specs, capabilities, or keyword. All filters are optional and combine with AND logic. Returns matching products with key specs.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `series` | `str \| None` | `None` | Chip family filter, e.g. `"ESP32-S3"`, `"ESP32-C6"`. |
| `product_type` | `str \| None` | `None` | `"SoC"` or `"Module"`. |
| `wifi` | `bool \| None` | `None` | Filter to products with (or without) Wi-Fi. |
| `bluetooth` | `bool \| None` | `None` | Filter to products with (or without) Bluetooth. |
| `thread_zigbee` | `bool \| None` | `None` | Filter to products with (or without) Thread/Zigbee (802.15.4). |
| `min_flash_mb` | `int \| None` | `None` | Minimum flash size in MB. |
| `min_psram_mb` | `int \| None` | `None` | Minimum PSRAM size in MB. |
| `min_sram_kb` | `int \| None` | `None` | Minimum SRAM size in KB. |
| `min_gpio` | `int \| None` | `None` | Minimum GPIO pin count. |
| `max_temp_c` | `int \| None` | `None` | Required upper operating temperature (e.g. `105` for industrial). |
| `status` | `str \| None` | `None` | Production status: `"Mass Production"`, `"NRND"`, `"EOL"`, `"Sample"`. |
| `antenna` | `str \| None` | `None` | Antenna type: `"PCB"`, `"IPEX"`, etc. |
| `keyword` | `str \| None` | `None` | Fuzzy search on product name or MPN. |
### Example
```python
result = await client.call_tool("esp_product_search", {
"series": "ESP32-S3",
"product_type": "Module",
"wifi": True,
"min_flash_mb": 8,
"status": "Mass Production"
})
```
### Return Value
```json
{
"count": 5,
"filters_applied": {
"series": "ESP32-S3",
"product_type": "Module",
"wifi": true,
"min_flash_mb": 8,
"status": "Mass Production"
},
"products": [
{
"name": "ESP32-S3-WROOM-1-N8",
"type": "Module",
"series": "ESP32-S3",
"status": "Mass Production",
"wifi": true,
"bluetooth": true,
"thread_zigbee": false,
"flash_mb": 8,
"psram_mb": 0,
"sram_kb": 512,
"gpio": 36,
"antenna": "PCB"
}
]
}
```
The `filters_applied` object echoes back only the filters that were actually provided, making it easy to confirm what was searched.
---
## esp_product_info
Get the full specification sheet for a specific Espressif product. The product is looked up by name or MPN using fuzzy matching.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `str` | -- | Product name or MPN, e.g. `"ESP32-S3-WROOM-1"`. **Required**. Fuzzy-matched. |
### Example
```python
result = await client.call_tool("esp_product_info", {
"name": "ESP32-S3-WROOM-1"
})
```
### Return Value
```json
{
"name": "ESP32-S3-WROOM-1-N16R8",
"name_new": "",
"type": "Module",
"series": "ESP32-S3",
"status": "Mass Production",
"mpn": "ESP32-S3-WROOM-1-N16R8",
"dimensions": "18x25.5x3.1",
"wifi": "802.11 b/g/n",
"wifi6": "",
"bluetooth": "BLE 5.0",
"thread_zigbee": "",
"pins": "38",
"frequency_mhz": "240",
"sram_kb": 512,
"rom_kb": "384",
"flash_mb": 16,
"flash_type": "Quad",
"psram_mb": 8,
"psram_type": "Octal",
"gpio": 36,
"operating_temp": "-40~85C",
"voltage_range": "3.0~3.6V",
"antenna": "PCB",
"size_type": "18x25.5",
"release_time": "2021-12",
"idf_supports": ">=v4.4",
"spq": "1400",
"moq": "1400",
"lead_time": "8-10 weeks",
"eccn_code": "5A992.c",
"ccatsz_code": "",
"hs_code": "8542390000",
"pre_firmware": "",
"replaced_by": ""
}
```
If no product matches, the response includes an `error` field and a `suggestion` to try `esp_product_search` with a keyword.
---
## esp_chip_compare
Compare 2-4 Espressif products side-by-side. The response highlights fields that differ between the products and groups fields that are identical, making it easy to spot the key differences between chip families or module variants.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `products` | `list[str]` | -- | List of 2-4 product names to compare. **Required**. Each name is fuzzy-matched. |
### Example
```python
result = await client.call_tool("esp_chip_compare", {
"products": ["ESP32-S3-WROOM-1-N16R8", "ESP32-C6-WROOM-1-N8"]
})
```
### Return Value
```json
{
"products_compared": ["ESP32-S3-WROOM-1-N16R8", "ESP32-C6-WROOM-1-N8"],
"differences": {
"series": {
"ESP32-S3-WROOM-1-N16R8": "ESP32-S3",
"ESP32-C6-WROOM-1-N8": "ESP32-C6"
},
"flash_mb": {
"ESP32-S3-WROOM-1-N16R8": 16,
"ESP32-C6-WROOM-1-N8": 8
},
"thread_zigbee": {
"ESP32-S3-WROOM-1-N16R8": "",
"ESP32-C6-WROOM-1-N8": "802.15.4"
}
},
"common": {
"type": "Module",
"status": "Mass Production",
"antenna": "PCB"
},
"full_specs": { }
}
```
The `differences` object maps each differing field to a dict of `{product_name: value}`. The `common` object contains fields where all compared products have the same value. `full_specs` includes the complete detail record for each product.
---
## esp_product_recommend
Get chip or module recommendations for a use case. Describe what you're building and any constraints, and this tool filters the catalog to matching products ranked by fit.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `use_case` | `str` | -- | What you're building, e.g. `"battery-powered BLE sensor"`. **Required**. |
| `constraints` | `str \| None` | `None` | Technical constraints, e.g. `"needs PSRAM, temp to 105C"`. |
| `prefer_module` | `bool` | `True` | Prefer modules over bare SoCs. Falls back to SoCs if no modules match. |
### Example
```python
result = await client.call_tool("esp_product_recommend", {
"use_case": "battery-powered BLE sensor with 8MB flash",
"constraints": "needs PSRAM, industrial temperature"
})
```
### Return Value
```json
{
"use_case": "battery-powered BLE sensor with 8MB flash",
"constraints_parsed": {
"wifi": false,
"bluetooth": true,
"thread_zigbee": false,
"psram_required": true,
"industrial_temp": true,
"min_flash_mb": 8,
"min_psram_mb": null,
"prefer_module": true
},
"count": 3,
"recommendations": [
{
"name": "ESP32-S3-WROOM-1-N8R8",
"type": "Module",
"series": "ESP32-S3",
"status": "Mass Production",
"wifi": true,
"bluetooth": true,
"thread_zigbee": false,
"flash_mb": 8,
"psram_mb": 8,
"sram_kb": 512,
"gpio": 36,
"antenna": "PCB"
}
]
}
```
The `constraints_parsed` object shows how the natural-language inputs were interpreted into filter criteria. Up to 15 recommendations are returned.
<Aside type="caution">
Recommendations only include products in **Mass Production** status. NRND, EOL, and Sample parts are excluded to avoid suggesting products that may be difficult to source.
</Aside>
---
## esp_product_availability
Check product availability and procurement details. Filter by production status, lead time, and minimum order quantity to find products you can actually source.
### Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `series` | `str \| None` | `None` | Chip family filter, e.g. `"ESP32-S3"`. |
| `status` | `str` | `"Mass Production"` | Production status filter. |
| `max_lead_weeks` | `int \| None` | `None` | Maximum acceptable lead time in weeks. |
| `max_moq` | `int \| None` | `None` | Maximum acceptable minimum order quantity. |
### Example
```python
result = await client.call_tool("esp_product_availability", {
"series": "ESP32-C3",
"max_lead_weeks": 12,
"max_moq": 500
})
```
### Return Value
```json
{
"count": 4,
"filters": {
"series": "ESP32-C3",
"status": "Mass Production",
"max_lead_weeks": 12,
"max_moq": 500
},
"products": [
{
"name": "ESP32-C3-WROOM-02-N4",
"type": "Module",
"series": "ESP32-C3",
"status": "Mass Production",
"wifi": true,
"bluetooth": true,
"thread_zigbee": false,
"flash_mb": 4,
"psram_mb": 0,
"sram_kb": 400,
"gpio": 22,
"antenna": "PCB",
"lead_time": "8-10 weeks",
"moq": "200",
"spq": "200",
"mpn": "ESP32-C3-WROOM-02-N4",
"replaced_by": ""
}
]
}
```
<Aside type="tip">
Use this tool to identify alternatives for EOL or NRND parts. Set `status` to `"NRND"` or `"EOL"` and check the `replaced_by` field for Espressif's suggested replacements.
</Aside>

View File

@ -5,7 +5,7 @@ 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.
mcesptool exposes 4 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.
@ -158,3 +158,65 @@ result = await client.read_resource("esp://capabilities")
<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>
---
## esp://products/catalog
Complete Espressif product catalog with summarized specs for every SoC and module. Data is fetched from [Espressif's public product API](https://products.espressif.com/) and cached in-memory with a 24-hour TTL.
For filtered queries, use the [Product Catalog tools](/reference/product-catalog/) instead.
### URI
```
esp://products/catalog
```
### Example
```python
result = await client.read_resource("esp://products/catalog")
```
### Response
```json
[
{
"name": "ESP32-S3-WROOM-1-N16R8",
"type": "Module",
"series": "ESP32-S3",
"status": "Mass Production",
"wifi": true,
"bluetooth": true,
"thread_zigbee": false,
"flash_mb": 16,
"psram_mb": 8,
"sram_kb": 512,
"gpio": 36,
"antenna": "PCB"
}
]
```
### Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | `str` | Product name as listed in Espressif's catalog. |
| `type` | `str` | `"SoC"` or `"Module"`. |
| `series` | `str` | Chip family, e.g. `"ESP32-S3"`, `"ESP32-C6"`. |
| `status` | `str` | Production status: `"Mass Production"`, `"NRND"`, `"EOL"`, `"Sample"`. |
| `wifi` | `bool` | Whether the product supports Wi-Fi. |
| `bluetooth` | `bool` | Whether the product supports Bluetooth/BLE. |
| `thread_zigbee` | `bool` | Whether the product supports Thread/Zigbee (802.15.4). |
| `flash_mb` | `int` | Integrated flash size in MB (0 if none). |
| `psram_mb` | `int` | Integrated PSRAM size in MB (0 if none). |
| `sram_kb` | `int` | Internal SRAM size in KB. |
| `gpio` | `int` | Number of GPIO pins. |
| `antenna` | `str` | Antenna type (e.g. `"PCB"`, `"IPEX"`, or empty for SoCs). |
<Aside type="note">
The catalog resource returns the full, unfiltered product list. For large catalogs (200+ products) this can be a significant payload. Use the [search](/reference/product-catalog/#esp_product_search) and [availability](/reference/product-catalog/#esp_product_availability) tools for targeted queries.
</Aside>

View File

@ -0,0 +1,238 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
export const getStaticPaths: GetStaticPaths = async () => {
const products = await getCollection("products");
return products.map((product) => ({
params: { id: product.id },
props: { product },
}));
};
const { product } = Astro.props;
const d = product.data;
function statusClass(status: string): string {
const map: Record<string, string> = {
"Mass Production": "badge-mass-production",
Sample: "badge-sample",
NRND: "badge-nrnd",
EOL: "badge-eol",
Replaced: "badge-replaced",
};
return map[status] || "";
}
function displayValue(val: string | number | undefined | null): string {
if (val === undefined || val === null || val === "" || val === 0) return "\u2014";
return String(val);
}
function memoryDisplay(mb: number, type: string): string {
if (mb === 0) return "\u2014";
return `${mb} MB${type ? ` (${type})` : ""}`;
}
---
<StarlightPage
frontmatter={{
title: d.name,
description: `${d.name} — ${d.type} from the ${d.series} family. Full specifications, memory, connectivity, and procurement details.`,
}}
>
<a href="/products/" class="product-back-link">&larr; Back to catalog</a>
<div class="product-detail-header">
<div class="product-detail-badges">
{d.series && <span class="badge badge-series">{d.series}</span>}
{d.type && (
<span class={`badge ${d.type === "SoC" ? "badge-soc" : "badge-module"}`}>
{d.type}
</span>
)}
{d.status && <span class={`badge ${statusClass(d.status)}`}>{d.status}</span>}
{d.antenna && <span class="conn-badge">{d.antenna}</span>}
</div>
</div>
{d.name_new && d.name_new !== d.name && (
<p style="font-size: 0.875rem; color: var(--sl-color-gray-3); margin-block-end: 1.5rem;">
Also known as: <strong style="color: var(--sl-color-white);">{d.name_new}</strong>
</p>
)}
<div class="product-spec-grid">
<div class="spec-section">
<h3>Memory</h3>
<table class="spec-table">
<tbody>
<tr>
<th>SRAM</th>
<td>{d.sram_kb > 0 ? `${d.sram_kb} KB` : "\u2014"}</td>
</tr>
<tr>
<th>ROM</th>
<td>{displayValue(d.rom_kb) !== "\u2014" ? `${d.rom_kb} KB` : "\u2014"}</td>
</tr>
<tr>
<th>Flash</th>
<td>{memoryDisplay(d.flash_mb, d.flash_type)}</td>
</tr>
<tr>
<th>PSRAM</th>
<td>{memoryDisplay(d.psram_mb, d.psram_type)}</td>
</tr>
</tbody>
</table>
</div>
<div class="spec-section">
<h3>Connectivity</h3>
<table class="spec-table">
<tbody>
<tr>
<th>Wi-Fi</th>
<td>
{d.has_wifi ? (
<span class="conn-badge conn-badge-wifi">{d.wifi}</span>
) : "\u2014"}
</td>
</tr>
{d.wifi6 && (
<tr>
<th>Wi-Fi 6</th>
<td><span class="conn-badge conn-badge-wifi">{d.wifi6}</span></td>
</tr>
)}
<tr>
<th>Bluetooth</th>
<td>
{d.has_bluetooth ? (
<span class="conn-badge conn-badge-bt">{d.bluetooth}</span>
) : "\u2014"}
</td>
</tr>
<tr>
<th>Thread / Zigbee</th>
<td>
{d.has_thread_zigbee ? (
<span class="conn-badge conn-badge-thread">{d.thread_zigbee}</span>
) : "\u2014"}
</td>
</tr>
</tbody>
</table>
</div>
<div class="spec-section">
<h3>Physical</h3>
<table class="spec-table">
<tbody>
<tr>
<th>Pins</th>
<td>{displayValue(d.pins)}</td>
</tr>
<tr>
<th>GPIO</th>
<td>{d.gpio > 0 ? d.gpio : "\u2014"}</td>
</tr>
<tr>
<th>Frequency</th>
<td>{displayValue(d.frequency_mhz) !== "\u2014" ? `${d.frequency_mhz} MHz` : "\u2014"}</td>
</tr>
<tr>
<th>Dimensions</th>
<td>{displayValue(d.dimensions)}</td>
</tr>
<tr>
<th>Antenna</th>
<td>{displayValue(d.antenna)}</td>
</tr>
<tr>
<th>Package</th>
<td>{displayValue(d.size_type)}</td>
</tr>
<tr>
<th>Operating Temp</th>
<td>{displayValue(d.operating_temp)}</td>
</tr>
<tr>
<th>Voltage Range</th>
<td>{displayValue(d.voltage_range)}</td>
</tr>
</tbody>
</table>
</div>
<div class="spec-section">
<h3>Procurement</h3>
<table class="spec-table">
<tbody>
<tr>
<th>MPN</th>
<td>{displayValue(d.mpn)}</td>
</tr>
<tr>
<th>MOQ</th>
<td>{displayValue(d.moq)}</td>
</tr>
<tr>
<th>SPQ</th>
<td>{displayValue(d.spq)}</td>
</tr>
<tr>
<th>Lead Time</th>
<td>{displayValue(d.lead_time)}</td>
</tr>
<tr>
<th>ECCN</th>
<td>{displayValue(d.eccn_code)}</td>
</tr>
<tr>
<th>HS Code</th>
<td>{displayValue(d.hs_code)}</td>
</tr>
</tbody>
</table>
</div>
</div>
{(d.release_time || d.idf_supports || d.replaced_by) && (
<div class="spec-section" style="margin-block-end: 2rem;">
<h3>Lifecycle</h3>
<table class="spec-table">
<tbody>
{d.release_time && (
<tr>
<th>Release Date</th>
<td>{d.release_time}</td>
</tr>
)}
{d.idf_supports && (
<tr>
<th>IDF Support</th>
<td>{d.idf_supports}</td>
</tr>
)}
{d.replaced_by && (
<tr>
<th>Replaced By</th>
<td>
<a href={`/products/${d.replaced_by.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}/`}>
{d.replaced_by}
</a>
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
<hr style="border-color: var(--sl-color-gray-5); margin-block: 2rem;" />
<p style="font-size: 0.875rem; color: var(--sl-color-gray-3);">
Query this data programmatically via the <a href="/reference/product-catalog/">Product Catalog MCP tools</a>.
</p>
</StarlightPage>

View File

@ -0,0 +1,246 @@
---
import { getCollection } from "astro:content";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
const allProducts = await getCollection("products");
// Extract unique series for filter chips
const allSeries = [...new Set(allProducts.map((p) => p.data.series).filter(Boolean))].sort();
const allStatuses = [...new Set(allProducts.map((p) => p.data.status).filter(Boolean))].sort();
// Serialize products for Alpine.js
const productsJson = JSON.stringify(
allProducts.map((p) => ({
id: p.id,
...p.data,
})),
);
---
<StarlightPage
frontmatter={{
title: "Product Catalog",
description: "Browse the complete Espressif ESP32 and ESP8266 product catalog — SoCs, modules, and dev kits with full specifications.",
template: "splash",
hero: {
title: "Espressif Product Catalog",
tagline: "Browse all ESP32 and ESP8266 chips and modules. Data sourced live from Espressif at build time.",
},
}}
>
<div x-data={`catalog(${productsJson})`}>
<div class="product-filters">
<div class="filter-row">
<input
type="text"
class="filter-search"
placeholder="Search by name, MPN, or series..."
x-model="search"
/>
</div>
<div class="filter-row">
<span class="filter-label">Type</span>
<div class="filter-group">
<button
class="filter-chip"
:class="{ active: typeFilter === '' }"
@click="typeFilter = ''"
>All</button>
<button
class="filter-chip"
:class="{ active: typeFilter === 'SoC' }"
@click="typeFilter = typeFilter === 'SoC' ? '' : 'SoC'"
>SoC</button>
<button
class="filter-chip"
:class="{ active: typeFilter === 'Module' }"
@click="typeFilter = typeFilter === 'Module' ? '' : 'Module'"
>Module</button>
</div>
<span class="filter-sep"></span>
<span class="filter-label">Connectivity</span>
<div class="filter-group">
<button
class="filter-chip"
:class="{ active: wifiFilter }"
@click="wifiFilter = !wifiFilter"
>Wi-Fi</button>
<button
class="filter-chip"
:class="{ active: btFilter }"
@click="btFilter = !btFilter"
>Bluetooth</button>
<button
class="filter-chip"
:class="{ active: threadFilter }"
@click="threadFilter = !threadFilter"
>Thread/Zigbee</button>
</div>
</div>
<div class="filter-row">
<span class="filter-label">Series</span>
<div class="filter-group">
<button
class="filter-chip"
:class="{ active: seriesFilter === '' }"
@click="seriesFilter = ''"
>All</button>
{allSeries.map((s) => (
<button
class="filter-chip"
x-bind:class={`{ active: seriesFilter === '${s}' }`}
x-on:click={`seriesFilter = seriesFilter === '${s}' ? '' : '${s}'`}
>{s}</button>
))}
</div>
</div>
<div class="filter-row">
<span class="filter-label">Status</span>
<div class="filter-group">
<button
class="filter-chip"
:class="{ active: statusFilter === '' }"
@click="statusFilter = ''"
>All</button>
{allStatuses.map((s) => (
<button
class="filter-chip"
x-bind:class={`{ active: statusFilter === '${s}' }`}
x-on:click={`statusFilter = statusFilter === '${s}' ? '' : '${s}'`}
>{s}</button>
))}
</div>
</div>
</div>
<div class="product-count">
Showing <strong x-text="filtered.length"></strong> of <strong>{allProducts.length}</strong> products
</div>
<div class="product-grid">
<template x-for="p in filtered" :key="p.id">
<a :href="`/products/${p.id}/`" class="product-card">
<div class="product-card-header">
<span class="product-card-name" x-text="p.name"></span>
</div>
<div class="product-card-badges">
<span class="badge badge-series" x-text="p.series" x-show="p.series"></span>
<span
class="badge"
:class="p.type === 'SoC' ? 'badge-soc' : 'badge-module'"
x-text="p.type"
x-show="p.type"
></span>
<span
class="badge"
:class="statusClass(p.status)"
x-text="p.status"
x-show="p.status"
></span>
</div>
<dl class="product-card-specs">
<template x-if="p.flash_mb > 0">
<div>
<dt>Flash</dt>
<dd x-text="p.flash_mb + ' MB' + (p.flash_type ? ' ' + p.flash_type : '')"></dd>
</div>
</template>
<template x-if="p.psram_mb > 0">
<div>
<dt>PSRAM</dt>
<dd x-text="p.psram_mb + ' MB' + (p.psram_type ? ' ' + p.psram_type : '')"></dd>
</div>
</template>
<template x-if="p.sram_kb > 0">
<div>
<dt>SRAM</dt>
<dd x-text="p.sram_kb + ' KB'"></dd>
</div>
</template>
<template x-if="p.gpio > 0">
<div>
<dt>GPIO</dt>
<dd x-text="p.gpio"></dd>
</div>
</template>
</dl>
<div class="product-card-connectivity">
<span class="conn-badge conn-badge-wifi" x-show="p.has_wifi" x-text="'Wi-Fi'"></span>
<span class="conn-badge conn-badge-bt" x-show="p.has_bluetooth" x-text="'BT'"></span>
<span class="conn-badge conn-badge-thread" x-show="p.has_thread_zigbee" x-text="'Thread/Zigbee'"></span>
<span class="conn-badge" x-show="p.antenna" x-text="p.antenna"></span>
</div>
</a>
</template>
</div>
<div class="product-empty" x-show="filtered.length === 0">
<p>No products match your current filters. Try broadening your search.</p>
</div>
</div>
<script>
document.addEventListener("alpine:init", () => {
// @ts-ignore
window.Alpine.data("catalog", (products: any[]) => ({
products,
search: "",
typeFilter: "",
seriesFilter: "",
statusFilter: "",
wifiFilter: false,
btFilter: false,
threadFilter: false,
get filtered() {
let result = this.products;
if (this.search) {
const q = this.search.toLowerCase();
result = result.filter(
(p: any) =>
p.name.toLowerCase().includes(q) ||
p.mpn.toLowerCase().includes(q) ||
p.series.toLowerCase().includes(q),
);
}
if (this.typeFilter) {
result = result.filter((p: any) => p.type === this.typeFilter);
}
if (this.seriesFilter) {
result = result.filter((p: any) => p.series === this.seriesFilter);
}
if (this.statusFilter) {
result = result.filter((p: any) => p.status === this.statusFilter);
}
if (this.wifiFilter) {
result = result.filter((p: any) => p.has_wifi);
}
if (this.btFilter) {
result = result.filter((p: any) => p.has_bluetooth);
}
if (this.threadFilter) {
result = result.filter((p: any) => p.has_thread_zigbee);
}
return result;
},
statusClass(status: string) {
const map: Record<string, string> = {
"Mass Production": "badge-mass-production",
Sample: "badge-sample",
NRND: "badge-nrnd",
EOL: "badge-eol",
Replaced: "badge-replaced",
};
return map[status] || "";
},
}));
});
</script>
</StarlightPage>

View File

@ -0,0 +1,438 @@
/* Product Catalog — filter bar, card grid, detail pages */
/* ── Filter bar ─────────────────────────────────────────── */
.product-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
margin-block-end: 1.5rem;
padding: 1rem;
background: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.75rem;
}
.product-filters .filter-group {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
align-items: center;
}
.product-filters .filter-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--sl-color-gray-3);
margin-inline-end: 0.25rem;
white-space: nowrap;
}
.product-filters .filter-sep {
width: 1px;
height: 1.5rem;
background: var(--sl-color-gray-5);
margin-inline: 0.375rem;
}
.filter-chip {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.625rem;
font-size: 0.8125rem;
line-height: 1.4;
border: 1px solid var(--sl-color-gray-5);
border-radius: 9999px;
background: transparent;
color: var(--sl-color-gray-2);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
user-select: none;
}
.filter-chip:hover {
border-color: var(--sl-color-accent);
color: var(--sl-color-accent-high);
}
.filter-chip.active {
background: var(--sl-color-accent);
border-color: var(--sl-color-accent);
color: var(--sl-color-black);
font-weight: 600;
}
.filter-search {
flex: 1 1 12rem;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.5rem;
background: var(--sl-color-black);
color: var(--sl-color-white);
outline: none;
transition: border-color 0.15s ease;
}
.filter-search::placeholder {
color: var(--sl-color-gray-4);
}
.filter-search:focus {
border-color: var(--sl-color-accent);
}
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
width: 100%;
}
.filter-row + .filter-row {
margin-block-start: 0.5rem;
}
/* ── Result count ───────────────────────────────────────── */
.product-count {
font-size: 0.8125rem;
color: var(--sl-color-gray-3);
margin-block-end: 1rem;
}
.product-count strong {
color: var(--sl-color-accent-high);
}
/* ── Card grid ──────────────────────────────────────────── */
.product-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 640px) {
.product-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.product-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.product-card {
display: flex;
flex-direction: column;
padding: 1rem;
background: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.75rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
text-decoration: none;
color: inherit;
}
.product-card:hover {
border-color: var(--sl-color-accent);
box-shadow: 0 0 0 1px var(--sl-color-accent);
}
.product-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
margin-block-end: 0.75rem;
}
.product-card-name {
font-size: 0.9375rem;
font-weight: 600;
color: var(--sl-color-white);
line-height: 1.3;
word-break: break-word;
}
.product-card-badges {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-block-end: 0.625rem;
}
.product-card-specs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.25rem 1rem;
font-size: 0.8125rem;
color: var(--sl-color-gray-2);
margin-block-end: 0.625rem;
}
.product-card-specs dt {
color: var(--sl-color-gray-3);
font-size: 0.75rem;
}
.product-card-specs dd {
margin: 0;
font-weight: 500;
}
.product-card-connectivity {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
margin-block-start: auto;
padding-block-start: 0.5rem;
border-block-start: 1px solid var(--sl-color-gray-5);
}
/* ── Badges ─────────────────────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.03em;
border-radius: 9999px;
white-space: nowrap;
line-height: 1.5;
}
/* Series badges */
.badge-series {
background: color-mix(in srgb, var(--sl-color-accent) 15%, transparent);
color: var(--sl-color-accent-high);
border: 1px solid color-mix(in srgb, var(--sl-color-accent) 30%, transparent);
}
/* Type badges */
.badge-soc {
background: color-mix(in srgb, #3b82f6 15%, transparent);
color: #93c5fd;
border: 1px solid color-mix(in srgb, #3b82f6 30%, transparent);
}
.badge-module {
background: color-mix(in srgb, #8b5cf6 15%, transparent);
color: #c4b5fd;
border: 1px solid color-mix(in srgb, #8b5cf6 30%, transparent);
}
/* Status badges */
.badge-mass-production {
background: color-mix(in srgb, var(--sl-color-accent) 15%, transparent);
color: var(--sl-color-accent-high);
border: 1px solid color-mix(in srgb, var(--sl-color-accent) 30%, transparent);
}
.badge-sample {
background: color-mix(in srgb, #3b82f6 15%, transparent);
color: #93c5fd;
border: 1px solid color-mix(in srgb, #3b82f6 30%, transparent);
}
.badge-nrnd {
background: color-mix(in srgb, #f59e0b 15%, transparent);
color: #fcd34d;
border: 1px solid color-mix(in srgb, #f59e0b 30%, transparent);
}
.badge-eol {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #fca5a5;
border: 1px solid color-mix(in srgb, #ef4444 30%, transparent);
}
.badge-replaced {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #fca5a5;
border: 1px solid color-mix(in srgb, #ef4444 30%, transparent);
}
/* Connectivity badges */
.conn-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
font-size: 0.6875rem;
font-weight: 500;
border-radius: 9999px;
background: color-mix(in srgb, var(--sl-color-gray-4) 12%, transparent);
color: var(--sl-color-gray-2);
border: 1px solid var(--sl-color-gray-5);
}
.conn-badge-wifi {
background: color-mix(in srgb, #06b6d4 12%, transparent);
color: #67e8f9;
border-color: color-mix(in srgb, #06b6d4 25%, transparent);
}
.conn-badge-bt {
background: color-mix(in srgb, #3b82f6 12%, transparent);
color: #93c5fd;
border-color: color-mix(in srgb, #3b82f6 25%, transparent);
}
.conn-badge-thread {
background: color-mix(in srgb, #22c55e 12%, transparent);
color: #86efac;
border-color: color-mix(in srgb, #22c55e 25%, transparent);
}
/* ── Detail page ────────────────────────────────────────── */
.product-detail-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-block-end: 1.5rem;
}
.product-detail-badges {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.product-spec-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
margin-block-end: 2rem;
}
@media (min-width: 768px) {
.product-spec-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.spec-section {
padding: 1rem;
background: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
border-radius: 0.75rem;
}
.spec-section h3 {
margin: 0 0 0.75rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--sl-color-accent-high);
}
.spec-table {
width: 100%;
border-collapse: collapse;
}
.spec-table tr + tr {
border-block-start: 1px solid var(--sl-color-gray-5);
}
.spec-table th {
text-align: start;
padding: 0.375rem 0.75rem 0.375rem 0;
font-size: 0.8125rem;
font-weight: 500;
color: var(--sl-color-gray-3);
white-space: nowrap;
width: 40%;
}
.spec-table td {
padding: 0.375rem 0;
font-size: 0.8125rem;
color: var(--sl-color-white);
}
/* ── Back link ──────────────────────────────────────────── */
.product-back-link {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
color: var(--sl-color-accent-high);
text-decoration: none;
margin-block-end: 1.5rem;
}
.product-back-link:hover {
text-decoration: underline;
}
/* ── Empty state ────────────────────────────────────────── */
.product-empty {
text-align: center;
padding: 3rem 1rem;
color: var(--sl-color-gray-3);
}
.product-empty p {
margin: 0;
font-size: 0.9375rem;
}
/* ── Light theme overrides ──────────────────────────────── */
:root[data-theme="light"] .badge-soc {
background: color-mix(in srgb, #3b82f6 10%, transparent);
color: #1d4ed8;
border-color: color-mix(in srgb, #3b82f6 25%, transparent);
}
:root[data-theme="light"] .badge-module {
background: color-mix(in srgb, #8b5cf6 10%, transparent);
color: #6d28d9;
border-color: color-mix(in srgb, #8b5cf6 25%, transparent);
}
:root[data-theme="light"] .badge-nrnd {
background: color-mix(in srgb, #f59e0b 10%, transparent);
color: #b45309;
border-color: color-mix(in srgb, #f59e0b 25%, transparent);
}
:root[data-theme="light"] .badge-eol,
:root[data-theme="light"] .badge-replaced {
background: color-mix(in srgb, #ef4444 10%, transparent);
color: #b91c1c;
border-color: color-mix(in srgb, #ef4444 25%, transparent);
}
:root[data-theme="light"] .badge-sample {
background: color-mix(in srgb, #3b82f6 10%, transparent);
color: #1d4ed8;
border-color: color-mix(in srgb, #3b82f6 25%, transparent);
}
:root[data-theme="light"] .conn-badge-wifi {
background: color-mix(in srgb, #06b6d4 10%, transparent);
color: #0e7490;
border-color: color-mix(in srgb, #06b6d4 20%, transparent);
}
:root[data-theme="light"] .conn-badge-bt {
background: color-mix(in srgb, #3b82f6 10%, transparent);
color: #1d4ed8;
border-color: color-mix(in srgb, #3b82f6 20%, transparent);
}
:root[data-theme="light"] .conn-badge-thread {
background: color-mix(in srgb, #22c55e 10%, transparent);
color: #15803d;
border-color: color-mix(in srgb, #22c55e 20%, transparent);
}

View File

@ -15,6 +15,7 @@ from .product_catalog import ProductCatalog
from .production_tools import ProductionTools
from .qemu_manager import QemuManager
from .security_manager import SecurityManager
from .smart_backup import SmartBackupManager
# Component registry for dynamic loading
COMPONENT_REGISTRY = {
@ -28,6 +29,7 @@ COMPONENT_REGISTRY = {
"diagnostics": Diagnostics,
"qemu_manager": QemuManager,
"product_catalog": ProductCatalog,
"smart_backup": SmartBackupManager,
}
__all__ = [
@ -41,5 +43,6 @@ __all__ = [
"Diagnostics",
"QemuManager",
"ProductCatalog",
"SmartBackupManager",
"COMPONENT_REGISTRY",
]

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ from .components import (
ProductionTools,
QemuManager,
SecurityManager,
SmartBackupManager,
)
from .config import ESPToolServerConfig, get_config, set_config
@ -72,6 +73,7 @@ class ESPToolServer:
self.components["chip_control"] = ChipControl(self.app, self.config)
self.components["flash_manager"] = FlashManager(self.app, self.config)
self.components["partition_manager"] = PartitionManager(self.app, self.config)
self.components["smart_backup"] = SmartBackupManager(self.app, self.config)
# Advanced features
self.components["security_manager"] = SecurityManager(self.app, self.config)
@ -179,6 +181,10 @@ class ESPToolServer:
"esp_performance_profile",
"esp_diagnostic_report",
],
"smart_backup": [
"esp_smart_backup",
"esp_smart_restore",
],
"product_catalog": [
"esp_product_search",
"esp_product_info",