Skip to content

Commit 23b19f5

Browse files
committed
esp32: introduce ESP32 driver
Signed-off-by: Benny Zlotnik <[email protected]>
1 parent 54fb64e commit 23b19f5

File tree

8 files changed

+508
-0
lines changed

8 files changed

+508
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
```
25+
26+
### Config parameters
27+
28+
| Parameter | Description | Type | Required | Default |
29+
| ------------ | --------------------------------------------------------------------- | ---- | -------- | ----------- |
30+
| port | The serial port to connect to the ESP32 | str | yes | |
31+
| baudrate | The baudrate for serial communication | int | no | 115200 |
32+
| chip | The ESP32 chip type (esp32, esp32s2, esp32s3, esp32c3, etc.) | str | no | esp32 |
33+
| reset_pin | GPIO pin number for hardware reset (if connected) | int | no | null |
34+
| boot_pin | GPIO pin number for boot mode control (if connected) | int | no | null |
35+
36+
## API Reference
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: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 _info_command(self):
72+
"""Get device information"""
73+
chip_info = self.chip_info()
74+
for key, value in chip_info.items():
75+
print(f"{key}: {value}")
76+
77+
def _chip_id_command(self):
78+
"""Get chip ID information"""
79+
info = self.chip_info()
80+
print(f"Chip Type: {info.get('chip_type', 'Unknown')}")
81+
if "mac_address" in info:
82+
print(f"MAC Address: {info['mac_address']}")
83+
if "chip_revision" in info:
84+
print(f"Chip Revision: {info['chip_revision']}")
85+
86+
def _reset_command(self):
87+
"""Reset the device"""
88+
result = self.reset()
89+
print(result)
90+
91+
def _erase_command(self):
92+
"""Erase the entire flash"""
93+
print("Erasing flash...")
94+
result = self.erase_flash()
95+
print(result)
96+
97+
def _parse_address(self, address):
98+
"""Parse address string to integer"""
99+
try:
100+
if isinstance(address, str) and address.startswith("0x"):
101+
return int(address, 16)
102+
else:
103+
return int(float(address))
104+
except (ValueError, TypeError):
105+
return 0x10000 # Default fallback
106+
107+
def _parse_size(self, size):
108+
"""Parse size string to integer"""
109+
try:
110+
if size.startswith("0x"):
111+
return int(size, 16)
112+
else:
113+
return int(float(size))
114+
except (ValueError, TypeError):
115+
return 1024 # Default fallback
116+
117+
def _flash_command(self, firmware_file, address):
118+
"""Flash firmware to the device"""
119+
address = self._parse_address(address)
120+
print(f"Flashing {firmware_file} to address 0x{address:x}...")
121+
result = self.flash_firmware_file(firmware_file, address)
122+
print(result)
123+
124+
def _read_flash_command(self, address, size, output):
125+
"""Read flash contents"""
126+
address = self._parse_address(address)
127+
size = self._parse_size(size)
128+
129+
print(f"Reading {size} bytes from address 0x{address:x}...")
130+
data = self.read_flash(address, size)
131+
132+
if output:
133+
with open(output, "wb") as f:
134+
f.write(data)
135+
print(f"Data written to {output}")
136+
else:
137+
# Print as hex
138+
hex_data = data.hex()
139+
for i in range(0, len(hex_data), 32):
140+
addr_offset = address + i // 2
141+
line = hex_data[i : i + 32]
142+
print(f"0x{addr_offset:08x}: {line}")
143+
144+
def _bootloader_command(self):
145+
"""Enter bootloader mode"""
146+
result = self.enter_bootloader()
147+
print(result)
148+
149+
def cli(self):
150+
@click.group()
151+
def base():
152+
"""ESP32 client"""
153+
pass
154+
155+
@base.command()
156+
def info():
157+
"""Get device information"""
158+
self._info_command()
159+
160+
@base.command("chip-id")
161+
def chip_id():
162+
"""Get chip ID information"""
163+
self._chip_id_command()
164+
165+
@base.command()
166+
def reset():
167+
"""Reset the device"""
168+
self._reset_command()
169+
170+
@base.command()
171+
def erase():
172+
"""Erase the entire flash"""
173+
self._erase_command()
174+
175+
@base.command()
176+
@click.argument("firmware_file", type=click.Path(exists=True))
177+
@click.option("--address", "-a", default="0x10000", type=str, help="Flash address (hex or decimal)")
178+
def flash(firmware_file, address):
179+
"""Flash firmware to the device"""
180+
self._flash_command(firmware_file, address)
181+
182+
@base.command("read-flash")
183+
@click.argument("address", type=str)
184+
@click.argument("size", type=str)
185+
@click.option("--output", "-o", type=click.Path(), help="Output file (default: print hex)")
186+
def read_flash_cmd(address, size, output):
187+
"""Read flash contents"""
188+
self._read_flash_command(address, size, output)
189+
190+
@base.command()
191+
def bootloader():
192+
"""Enter bootloader mode"""
193+
self._bootloader_command()
194+
195+
return base

0 commit comments

Comments
 (0)