Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 1cb401a

Browse files
committed
esp32: introduce ESP32 driver
Signed-off-by: Benny Zlotnik <bzlotnik@protonmail.com>
1 parent 54fb64e commit 1cb401a

8 files changed

Lines changed: 572 additions & 0 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# ESP32 driver
2+
3+
`jumpstarter-driver-esp32` provides functionality for flashing, monitoring, and controlling ESP32 devices using esptool and serial communication.
4+
5+
## Installation
6+
7+
```{code-block} console
8+
:substitutions:
9+
$ pip3 install --extra-index-url {{index_url}} jumpstarter-driver-esp32
10+
```
11+
12+
## Configuration
13+
14+
Example configuration:
15+
16+
```yaml
17+
export:
18+
esp32:
19+
type: jumpstarter_driver_esp32.driver.ESP32
20+
config:
21+
port: "/dev/ttyUSB0"
22+
baudrate: 115200
23+
chip: "esp32"
24+
reset_pin: null
25+
boot_pin: null
26+
```
27+
28+
### Config parameters
29+
30+
| Parameter | Description | Type | Required | Default |
31+
| ------------ | --------------------------------------------------------------------- | ---- | -------- | ----------- |
32+
| port | The serial port to connect to the ESP32 | str | yes | |
33+
| baudrate | The baudrate for serial communication | int | no | 115200 |
34+
| chip | The ESP32 chip type (esp32, esp32s2, esp32s3, esp32c3, etc.) | str | no | esp32 |
35+
| reset_pin | GPIO pin number for hardware reset (if connected) | int | no | null |
36+
| boot_pin | GPIO pin number for boot mode control (if connected) | int | no | null |
37+
38+
## Features
39+
40+
- **Firmware Flashing**: Flash binary files to ESP32 using esptool library
41+
- **Device Control**: Reset, erase, and get device information
42+
- **ESP-IDF Integration**: Support for ESP-IDF project workflows
43+
- **Hardware Control**: Optional GPIO control for reset and boot pins
44+
45+
## API Reference
46+
47+
```{eval-rst}
48+
.. autoclass:: jumpstarter_driver_esp32.client.ESP32Client()
49+
:members:
50+
```
51+
52+
### CLI
53+
54+
The ESP32 driver client comes with a CLI tool that can be used to interact with
55+
the ESP32 device:
56+
57+
```
58+
jumpstarter ⚡ local ➤ j esp32
59+
Usage: j esp32 [OPTIONS] COMMAND [ARGS]...
60+
61+
ESP32 client
62+
63+
Options:
64+
--help Show this message and exit.
65+
66+
Commands:
67+
bootloader Enter bootloader mode
68+
chip-id Get chip ID information
69+
erase Erase the entire flash
70+
flash Flash firmware to the device
71+
flash-multiple Flash multiple files
72+
info Get device information
73+
read-flash Read flash contents
74+
reset Reset the device
75+
```
76+
77+
### Examples
78+
79+
**Flash firmware:**
80+
```{testcode}
81+
esp32client.flash_firmware("/path/to/firmware.bin", address=0x10000)
82+
```
83+
84+
**Get device information:**
85+
```{testcode}
86+
info = esp32client.chip_info()
87+
print(f"Chip: {info['chip_type']}")
88+
print(f"MAC: {info['mac_address']}")
89+
```
90+
91+
**Erase flash:**
92+
```{testcode}
93+
esp32client.erase_flash()
94+
```
95+
96+
## Serial Communication
97+
98+
For serial monitoring and communication, use the separate `jumpstarter-driver-pyserial` driver:
99+
100+
```yaml
101+
export:
102+
esp32:
103+
type: jumpstarter_driver_esp32.driver.ESP32
104+
config:
105+
port: "/dev/ttyUSB0"
106+
baudrate: 115200
107+
chip: "esp32"
108+
109+
serial:
110+
type: jumpstarter_driver_pyserial.driver.PySerial
111+
config:
112+
url: "/dev/ttyUSB0"
113+
baudrate: 115200
114+
```
115+
116+
This separation allows you to:
117+
- Use `j esp32` commands for flashing and device management
118+
- Use `j serial` commands for serial communication and monitoring
119+
120+
```{testsetup} *
121+
from jumpstarter_driver_esp32.driver import ESP32
122+
from jumpstarter.common.utils import serve
123+
124+
instance = serve(ESP32(port="/dev/null"))
125+
esp32client = instance.__enter__()
126+
```
127+
128+
```{testcleanup} *
129+
instance.__exit__(None, None, None)
130+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
metadata:
4+
name: esp32
5+
spec:
6+
export:
7+
esp32:
8+
type: jumpstarter_driver_esp32.driver.ESP32
9+
config:
10+
port: "/dev/ttyUSB0"
11+
baudrate: 115200
12+
chip: "esp32"
13+
# Optional GPIO pins for hardware control
14+
# reset_pin: 2
15+
# boot_pin: 0

packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/__init__.py

Whitespace-only changes.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
from typing import Any, Dict
4+
5+
import click
6+
from jumpstarter_driver_opendal.adapter import OpendalAdapter
7+
from opendal import Operator
8+
9+
from jumpstarter.client import DriverClient
10+
from jumpstarter.common.exceptions import ArgumentError
11+
12+
13+
@dataclass(kw_only=True)
14+
class ESP32Client(DriverClient):
15+
"""
16+
Client interface for ESP32 driver
17+
"""
18+
19+
def chip_info(self) -> Dict[str, Any]:
20+
"""Get ESP32 chip information"""
21+
return self.call("chip_info")
22+
23+
def reset(self) -> str:
24+
"""Reset the ESP32 device"""
25+
return self.call("reset_device")
26+
27+
def erase_flash(self) -> str:
28+
"""Erase the entire flash memory"""
29+
self.logger.info("Erasing flash... this may take a while")
30+
return self.call("erase_flash")
31+
32+
def flash_firmware(self, operator: Operator, path: str, address: int = 0x10000) -> str:
33+
"""Flash firmware to the ESP32
34+
35+
Args:
36+
operator: OpenDAL operator for file access
37+
path: Path to firmware file
38+
address: Flash address (default: 0x10000 for app partition)
39+
"""
40+
if address < 0:
41+
raise ArgumentError("Flash address must be non-negative")
42+
43+
with OpendalAdapter(client=self, operator=operator, path=path) as handle:
44+
return self.call("flash_firmware", handle, address)
45+
46+
def flash_firmware_file(self, filepath: str, address: int = 0x10000) -> str:
47+
"""Flash a local firmware file to the ESP32"""
48+
absolute = Path(filepath).resolve()
49+
if not absolute.exists():
50+
raise ArgumentError(f"File not found: {filepath}")
51+
return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)
52+
53+
def read_flash(self, address: int, size: int) -> bytes:
54+
"""Read flash contents from specified address
55+
56+
Args:
57+
address: Flash address to read from
58+
size: Number of bytes to read
59+
"""
60+
if address < 0:
61+
raise ArgumentError("Flash address must be non-negative")
62+
if size <= 0:
63+
raise ArgumentError("Size must be positive")
64+
65+
return self.call("read_flash", address, size)
66+
67+
def enter_bootloader(self) -> str:
68+
"""Enter bootloader mode"""
69+
return self.call("enter_bootloader")
70+
71+
def cli(self):
72+
@click.group()
73+
def base():
74+
"""ESP32 client"""
75+
pass
76+
77+
@base.command()
78+
def info():
79+
"""Get device information"""
80+
chip_info = self.chip_info()
81+
for key, value in chip_info.items():
82+
print(f"{key}: {value}")
83+
84+
@base.command("chip-id")
85+
def chip_id():
86+
"""Get chip ID information"""
87+
info = self.chip_info()
88+
print(f"Chip Type: {info.get('chip_type', 'Unknown')}")
89+
if 'mac_address' in info:
90+
print(f"MAC Address: {info['mac_address']}")
91+
if 'chip_revision' in info:
92+
print(f"Chip Revision: {info['chip_revision']}")
93+
94+
@base.command()
95+
def reset():
96+
"""Reset the device"""
97+
result = self.reset()
98+
print(result)
99+
100+
@base.command()
101+
def erase():
102+
"""Erase the entire flash"""
103+
print("Erasing flash... this may take a while")
104+
result = self.erase_flash()
105+
print(result)
106+
107+
@base.command()
108+
@click.argument("firmware_file", type=click.Path(exists=True))
109+
@click.option("--address", "-a", default="0x10000", type=str,
110+
help="Flash address (hex or decimal)")
111+
def flash(firmware_file, address):
112+
"""Flash firmware to the device"""
113+
try:
114+
if isinstance(address, str) and address.startswith("0x"):
115+
address = int(address, 16)
116+
else:
117+
address = int(float(address))
118+
except (ValueError, TypeError):
119+
address = 0x10000 # Default fallback
120+
121+
print(f"Flashing {firmware_file} to address 0x{address:x}...")
122+
result = self.flash_firmware_file(firmware_file, address)
123+
print(result)
124+
125+
@base.command("read-flash")
126+
@click.argument("address", type=str)
127+
@click.argument("size", type=str)
128+
@click.option("--output", "-o", type=click.Path(),
129+
help="Output file (default: print hex)")
130+
def read_flash_cmd(address, size, output):
131+
"""Read flash contents"""
132+
# Parse address and size
133+
try:
134+
if address.startswith("0x"):
135+
address = int(address, 16)
136+
else:
137+
address = int(float(address))
138+
except (ValueError, TypeError):
139+
address = 0
140+
141+
try:
142+
if size.startswith("0x"):
143+
size = int(size, 16)
144+
else:
145+
size = int(float(size))
146+
except (ValueError, TypeError):
147+
size = 1024
148+
149+
print(f"Reading {size} bytes from address 0x{address:x}...")
150+
data = self.read_flash(address, size)
151+
152+
if output:
153+
with open(output, 'wb') as f:
154+
f.write(data)
155+
print(f"Data written to {output}")
156+
else:
157+
# Print as hex
158+
hex_data = data.hex()
159+
for i in range(0, len(hex_data), 32):
160+
addr_offset = address + i // 2
161+
line = hex_data[i:i+32]
162+
print(f"0x{addr_offset:08x}: {line}")
163+
164+
@base.command()
165+
def bootloader():
166+
"""Enter bootloader mode"""
167+
result = self.enter_bootloader()
168+
print(result)
169+
170+
return base

0 commit comments

Comments
 (0)