Skip to content

Commit 9fa6fe2

Browse files
authored
Merge pull request #294 from bennyz/esp32
esp32: introduce esp32 driver
2 parents 112decc + 3677516 commit 9fa6fe2

12 files changed

Lines changed: 759 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../../packages/jumpstarter-driver-esp32/README.md

python/docs/source/reference/package-apis/drivers/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Drivers that handle media streams:
6565

6666
Drivers for debugging and programming devices:
6767

68+
* **[ESP32](esp32.md)** (`jumpstarter-driver-esp32`) - ESP32 flashing and
69+
management via esptool
6870
* **[Flashers](flashers.md)** (`jumpstarter-driver-flashers`) - Flash memory
6971
programming tools
7072
* **[Probe-RS](probe-rs.md)** (`jumpstarter-driver-probe-rs`) - Debugging probe
@@ -91,6 +93,7 @@ can.md
9193
corellium.md
9294
dutlink.md
9395
energenie.md
96+
esp32.md
9497
flashers.md
9598
http.md
9699
http-power.md
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/
2+
.coverage
3+
coverage.xml
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# ESP32 driver
2+
3+
`jumpstarter-driver-esp32` provides functionality for flashing and managing
4+
ESP32 devices using [esptool](https://github.com/espressif/esptool) as a
5+
library. It implements the `FlasherInterface` from `jumpstarter-driver-opendal`.
6+
7+
## Installation
8+
9+
```{code-block} console
10+
:substitutions:
11+
$ pip3 install --extra-index-url {{index_url}} jumpstarter-driver-esp32
12+
```
13+
14+
## Configuration
15+
16+
Example configuration:
17+
18+
```yaml
19+
export:
20+
storage:
21+
type: jumpstarter_driver_esp32.driver.Esp32Flasher
22+
config:
23+
baudrate: 115200
24+
chip: "esp32"
25+
children:
26+
serial:
27+
ref: serial
28+
serial:
29+
type: jumpstarter_driver_pyserial.driver.PySerial
30+
config:
31+
url: "/dev/ttyUSB0"
32+
baudrate: 115200
33+
```
34+
35+
### Config parameters
36+
37+
| Parameter | Description | Type | Required | Default |
38+
| --------- | ------------------------------------ | ---- | -------- | ------------- |
39+
| baudrate | Baud rate for esptool communication | int | no | 115200 |
40+
| chip | Target chip type | str | no | esp32 |
41+
42+
The ESP32 driver requires a `serial` child driver (PySerial) for serial port
43+
access. DTR/RTS control signals and the serial port path are managed through
44+
the child driver. Use a `ref` proxy to share the serial driver with the
45+
top-level composite, enabling both `j serial start-console` and
46+
`j storage flash` to work.
47+
48+
## API Reference
49+
50+
```{eval-rst}
51+
.. autoclass:: jumpstarter_driver_esp32.client.Esp32FlasherClient()
52+
:members: flash, dump, get_chip_info, erase, hard_reset, enter_bootloader
53+
```
54+
55+
### CLI
56+
57+
```text
58+
$ j storage
59+
Usage: j storage [OPTIONS] COMMAND [ARGS]...
60+
61+
Commands:
62+
bootloader Enter download mode
63+
chip-info Get chip info (name, features, MAC)
64+
dump Dump flash content to file
65+
erase Erase entire flash
66+
flash Flash firmware to ESP32
67+
reset Hard reset the chip
68+
69+
$ j serial
70+
Usage: j serial [OPTIONS] COMMAND [ARGS]...
71+
72+
Commands:
73+
start-console Start serial port console
74+
pipe Pipe serial port data to stdout or file
75+
```
76+
77+
## Examples
78+
79+
### CLI usage
80+
81+
```bash
82+
# Flash MicroPython firmware
83+
j storage flash firmware.bin --address 0x1000
84+
85+
# Get chip info
86+
j storage chip-info
87+
88+
# Enter download mode
89+
j storage bootloader
90+
91+
# Erase entire flash
92+
j storage erase
93+
94+
# Hard reset
95+
j storage reset
96+
97+
# Open serial console
98+
j serial start-console
99+
100+
# Read serial output
101+
j serial pipe
102+
```
103+
104+
### Python API
105+
106+
```python
107+
# Get chip information
108+
info = client.storage.get_chip_info()
109+
print(info["chip"]) # e.g. "ESP32-D0WD-V3 (revision v3.1)"
110+
print(info["features"]) # e.g. "Wi-Fi, BT, Dual Core"
111+
print(info["mac"]) # e.g. "5c:01:3b:68:ab:0c"
112+
113+
# Flash firmware
114+
client.storage.flash("/path/to/firmware.bin", target="0x1000")
115+
116+
# Enter download mode
117+
client.storage.enter_bootloader()
118+
119+
# Erase flash
120+
client.storage.erase()
121+
122+
# Hard reset
123+
client.storage.hard_reset()
124+
125+
# Serial console via pexpect
126+
console = client.serial.open()
127+
console.sendline("import machine")
128+
console.expect(">>>")
129+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
metadata:
4+
namespace: default
5+
name: demo
6+
endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082
7+
token: "<token>"
8+
export:
9+
storage:
10+
type: jumpstarter_driver_esp32.driver.Esp32Flasher
11+
config:
12+
baudrate: 115200
13+
chip: "esp32"
14+
children:
15+
serial:
16+
ref: serial
17+
serial:
18+
type: jumpstarter_driver_pyserial.driver.PySerial
19+
config:
20+
url: "/dev/ttyUSB0"
21+
baudrate: 115200

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

Whitespace-only changes.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from dataclasses import dataclass
2+
3+
import click
4+
from jumpstarter_driver_opendal.client import FlasherClient
5+
6+
from jumpstarter.streams.encoding import Compression
7+
8+
9+
@dataclass(kw_only=True)
10+
class Esp32FlasherClient(FlasherClient):
11+
"""Client interface for ESP32 flasher driver."""
12+
13+
def get_chip_info(self) -> dict[str, str]:
14+
"""Get chip information including name, features, and MAC address."""
15+
return self.call("get_chip_info")
16+
17+
def erase(self):
18+
"""Erase the entire flash memory."""
19+
return self.call("erase")
20+
21+
def hard_reset(self):
22+
"""Hard reset the ESP32 chip."""
23+
return self.call("hard_reset")
24+
25+
def enter_bootloader(self):
26+
"""Enter ESP32 download mode via DTR/RTS toggle."""
27+
return self.call("enter_bootloader")
28+
29+
def cli(self):
30+
base = super().cli()
31+
32+
# Override the inherited flash command to add --address
33+
base.commands.pop("flash", None)
34+
35+
@base.command()
36+
@click.argument("file", type=click.Path(exists=True))
37+
@click.option("--address", "-a", default=None, help="Flash address (e.g. 0x1000)")
38+
@click.option("--compression", type=click.Choice(Compression, case_sensitive=False))
39+
def flash(file, address, compression):
40+
"""Flash firmware to ESP32"""
41+
self.flash(file, target=address, compression=compression)
42+
click.echo("done")
43+
44+
@base.command()
45+
def chip_info():
46+
"""Get chip info (name, features, MAC)"""
47+
info = self.get_chip_info()
48+
for key, value in info.items():
49+
click.echo(f"{key}: {value}")
50+
51+
@base.command()
52+
def erase():
53+
"""Erase entire flash"""
54+
self.erase()
55+
click.echo("done")
56+
57+
@base.command()
58+
def reset():
59+
"""Hard reset the chip"""
60+
self.hard_reset()
61+
click.echo("done")
62+
63+
@base.command()
64+
def bootloader():
65+
"""Enter download mode"""
66+
self.enter_bootloader()
67+
click.echo("done")
68+
69+
return base

0 commit comments

Comments
 (0)