Skip to content

Conversation

@bennyz
Copy link
Member

@bennyz bennyz commented Sep 12, 2025

Summary by CodeRabbit

  • New Features

    • ESP32 driver added with device info, reset, erase, read-flash, firmware flashing, and bootloader support; includes a programmatic client and a CLI.
  • Documentation

    • New README and docs page with installation, configuration schema, API surface, CLI usage, examples, and usage snippets.
  • Chores

    • New package added to workspace and aggregated distribution; packaging, test, and dev tooling configured.
  • Examples

    • Example exporter YAML and a Python example demonstrating basic usage.

@netlify
Copy link

netlify bot commented Sep 12, 2025

Deploy Preview for jumpstarter-docs ready!

Name Link
🔨 Latest commit e204945
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/68c68ab3a777f800087a3128
😎 Deploy Preview https://deploy-preview-613--jumpstarter-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 12, 2025

Walkthrough

Adds a new jumpstarter-driver-esp32 package: driver and client implementations (with Click CLI), README, examples, Sphinx docs, packaging (pyproject), workspace registration, and inclusion in jumpstarter-all; no existing public APIs were changed.

Changes

Cohort / File(s) Summary of changes
Documentation & README
packages/jumpstarter-driver-esp32/README.md, docs/source/reference/package-apis/drivers/esp32.md, docs/source/reference/package-apis/drivers/index.md
New README and Sphinx docs describing installation, config parameters, API/CLI surface, examples, and adding esp32.md to drivers index/toctree.
Examples & configs
packages/jumpstarter-driver-esp32/examples/exporter.yaml, packages/jumpstarter-driver-esp32/examples/example.py
New exporter YAML and a Python example demonstrating connecting to ESP32 and printing chip info/MAC.
Client API & CLI
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py
Adds ESP32Client dataclass implementing DriverClient with methods: chip_info, reset, erase_flash, flash_firmware, flash_firmware_file, read_flash, enter_bootloader, plus a Click-based CLI exposing those commands and input validation/OpenDAL file handling.
Driver implementation
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py
Adds ESP32 Driver dataclass (port, baudrate, chip, reset_pin, boot_pin, check_present) and exported methods: client (classmethod), chip_info, reset_device, erase_flash, flash_firmware (async with temp file + thread offload), read_flash, enter_bootloader; includes connection/reset helpers and a TemporaryFilename helper.
Packaging (package)
packages/jumpstarter-driver-esp32/pyproject.toml
New package pyproject with metadata, Python >=3.11, dependencies (anyio, click, esptool, jumpstarter, jumpstarter-driver-opendal), Hatch VCS/versioning, a pin_jumpstarter build hook, and pytest/dev test config.
Workspace & aggregate packaging
pyproject.toml, packages/jumpstarter-all/pyproject.toml
Root workspace configured to include jumpstarter-driver-esp32; packages/jumpstarter-all dependencies updated to add jumpstarter-driver-esp32.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as "j esp32 (CLI)"
  participant Client as "ESP32Client"
  participant Driver as "ESP32 Driver (RPC)"
  participant ESP as "ESP32 Device"

  User->>CLI: j esp32 flash --address 0x1000 firmware.bin
  CLI->>Client: flash_firmware_file(filepath, address)
  Client->>Client: validate path & address
  Client->>Driver: call("flash_firmware", handle, address)
  activate Driver
  Driver->>Driver: stream src → TemporaryFilename (async/thread)
  Driver->>ESP: esptool write_flash(address, tempFile)
  ESP-->>Driver: status
  Driver->>ESP: hard reset
  deactivate Driver
  Driver-->>Client: "Flashed at 0x1000"
  Client-->>CLI: result
  CLI-->>User: print status
Loading
sequenceDiagram
  autonumber
  actor User
  participant CLI as "j esp32 (CLI)"
  participant Client as "ESP32Client"
  participant Driver as "ESP32 Driver (RPC)"
  participant ESP as "ESP32 Device"

  User->>CLI: j esp32 info
  CLI->>Client: chip_info()
  Client->>Driver: call("chip_info")
  Driver->>ESP: connect + read efuse/MAC
  ESP-->>Driver: chip data
  Driver-->>Client: dict(chip_type, mac, revision, ...)
  Client-->>CLI: info dict
  CLI-->>User: print key=value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo
  • NickCao

Poem

I twitch my ears at flashing light,
Baud hops steady at one-one-five,
I nudge the boot, I click the reset,
Bytes like carrots, neatly set,
A happy hop — your board’s alive! 🥕✨

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.49% which is sufficient. The required threshold is 80.00%.
Title Check ✅ Passed The title "esp32: introduce ESP32 driver" succinctly and accurately summarizes the primary change — adding an ESP32 driver and related package artifacts. It is concise, focused, and free of noise or irrelevant details. A reviewer scanning PR history will immediately understand the main intent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (14)
pyproject.toml (1)

15-15: Workspace source entry looks good. Consider source list ordering for churn minimization.

No issues with adding the ESP32 package. To reduce future diff noise, consider keeping [tool.uv.sources] keys alphabetized.

packages/jumpstarter-driver-esp32/pyproject.toml (1)

25-30: pytest config: consider surfacing failures early.

Optional: add -rA -q or --maxfail=1 for faster feedback locally.

packages/jumpstarter-driver-esp32/examples/exporter.yaml (1)

13-15: Clarify hardware pin support status.

Docs imply hardware reset/boot are available, but driver methods are TODOs. Add a short comment noting current no-op status to avoid confusion.

Apply this diff:

-        # Optional GPIO pins for hardware control
+        # Optional GPIO pins for hardware control (currently no-op/TODO in driver)
packages/jumpstarter-driver-esp32/README.md (2)

57-75: Fix markdown lint: add language to fenced block.

Add a language identifier to satisfy MD040 and improve rendering.

Apply this diff:

-```
+```text
 ...
-```
+```

40-44: Set expectations on hardware control.

Since _hardware_reset/_hardware_bootloader are TODOs, add a brief note that GPIO-based control is not implemented yet.

packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (5)

29-33: Remove shadow import.

import os is already at module top. Avoid re-importing inside __post_init__.

Apply this diff:

         if self.check_present and not self.port.startswith(("/dev/null", "loop://")):
-            import os
             if not os.path.exists(self.port):
                 self.logger.warning(f"Serial port {self.port} does not exist")

144-148: Avoid KeyError and improve message clarity.

Use .get() and prefer chip description/type for the message.

Apply this diff:

-                return f"Entered bootloader mode successfully. Connected to {info['chip_revision']}"
+                return (
+                    "Entered bootloader mode successfully. Connected to "
+                    f"{info.get('chip_revision') or info.get('chip_type')}"
+                )

181-191: Hardware control methods are stubs.

Consider returning NotImplementedError until GPIO control is implemented, or clearly document as no-op. Also, decide on a GPIO backend (gpiod vs. RPi vs. sysfs) and add it as an optional extra.

I can wire a minimal gpiod-based implementation behind an optional dependency (e.g., jumpstarter-driver-esp32[gpio]).

Also applies to: 192-201


43-54: Avoid non‑portable efuse access; normalize fields

File: packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py Lines: 43-54

Replace esp.read_efuse(0) with a safe lookup and make chip_id optional (only format as hex when int); use esp.get_chip_description() directly for chip_revision.

-            chip_id = esp.read_efuse(0)
+            # Avoid non-portable efuse access across ESP variants
+            chip_id = getattr(esp, "chip_id", None)
             mac = esp.read_mac()
             mac_str = ":".join([f"{b:02x}" for b in mac])

             return {
                 "chip_type": self.chip,
                 "port": self.port,
                 "baudrate": self.baudrate,
                 "mac_address": mac_str,
-                "chip_id": f"0x{chip_id:08x}",
-                "chip_revision": f"Chip is {esp.get_chip_description()}"
+                "chip_id": f"0x{chip_id:08x}" if isinstance(chip_id, int) else None,
+                # Keep key name for API stability; value is the description
+                "chip_revision": esp.get_chip_description(),
             }

96-106: Pass filename (path) to esptool.cmds.write_flash instead of an open file handle.

esptool v5+ examples use path tuples and the v5 public API targets filename/bytes/file-like inputs — passing the filename string is more portable across esptool versions (repo depends on esptool>=5.0.2). (developer.espressif.com)

-                    with open(filename, 'rb') as f:
-                        esptool.cmds.write_flash(
-                            esp,
-                            [(address, f)],
-                            flash_freq="keep",
-                            flash_mode="keep",
-                            flash_size="detect",
-                            compress=True,
-                            no_progress=False,
-                            erase_all=False
-                        )
+                    esptool.cmds.write_flash(
+                        esp,
+                        [(address, filename)],
+                        flash_freq="keep",
+                        flash_mode="keep",
+                        flash_size="detect",
+                        compress=True,
+                        no_progress=False,
+                        erase_all=False,
+                    )
  • Add a small test that mocks esptool.cmds.write_flash and verifies it's called with the filename string.
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (4)

82-82: Prefer click.echo over print for consistent CLI behavior.

Switching to click.echo handles encoding, piping, and testing better.

Apply:

-                print(f"{key}: {value}")
+                click.echo(f"{key}: {value}")
@@
-            print(f"Chip Type: {info.get('chip_type', 'Unknown')}")
+            click.echo(f"Chip Type: {info.get('chip_type', 'Unknown')}")
+            if 'chip_id' in info:
+                click.echo(f"Chip ID: {info['chip_id']}")
             if 'mac_address' in info:
-                print(f"MAC Address: {info['mac_address']}")
+                click.echo(f"MAC Address: {info['mac_address']}")
             if 'chip_revision' in info:
-                print(f"Chip Revision: {info['chip_revision']}")
+                click.echo(f"Chip Revision: {info['chip_revision']}")
@@
-            result = self.reset()
-            print(result)
+            result = self.reset()
+            click.echo(result)
@@
-            print("Erasing flash... this may take a while")
-            result = self.erase_flash()
-            print(result)
+            click.echo("Erasing flash... this may take a while")
+            result = self.erase_flash()
+            click.echo(result)
@@
-            print(f"Reading {size} bytes from address 0x{address:x}...")
+            click.echo(f"Reading {size} bytes from address 0x{address:x}...")
@@
-                with open(output, 'wb') as f:
-                    f.write(data)
-                print(f"Data written to {output}")
+                with open(output, 'wb') as f:
+                    f.write(data)
+                click.echo(f"Data written to {output}")
@@
-                    print(f"0x{addr_offset:08x}: {line}")
+                    click.echo(f"0x{addr_offset:08x}: {line}")
@@
-            result = self.enter_bootloader()
-            print(result)
+            result = self.enter_bootloader()
+            click.echo(result)

Also applies to: 88-93, 97-98, 103-105, 121-124, 149-156, 160-163, 167-168


84-93: chip-id command omits the actual chip_id field.

The command name suggests showing the numeric ID. Include chip_id.

Included in the echo diff above:

+            if 'chip_id' in info:
+                click.echo(f"Chip ID: {info['chip_id']}")

29-30: Guard logger usage on client.

DriverClient may not expose .logger. Avoid AttributeError.

Apply:

-        self.logger.info("Erasing flash... this may take a while")
+        if getattr(self, "logger", None):
+            self.logger.info("Erasing flash... this may take a while")

53-65: LGTM with sane validation.

Non-negative address and positive size checks are appropriate. Consider an upper bound (e.g., against detected flash size) later.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54fb64e and 1cb401a.

📒 Files selected for processing (6)
  • packages/jumpstarter-driver-esp32/README.md (1 hunks)
  • packages/jumpstarter-driver-esp32/examples/exporter.yaml (1 hunks)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (1 hunks)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (1 hunks)
  • packages/jumpstarter-driver-esp32/pyproject.toml (1 hunks)
  • pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (2)
packages/jumpstarter/jumpstarter/driver/base.py (2)
  • Driver (56-269)
  • resource (225-252)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (7)
  • chip_info (19-21)
  • chip_id (85-92)
  • erase_flash (27-30)
  • info (78-82)
  • flash_firmware (32-44)
  • read_flash (53-65)
  • enter_bootloader (67-69)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (4)
packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)
  • OpendalAdapter (60-86)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (6)
  • client (35-36)
  • chip_info (39-57)
  • erase_flash (68-77)
  • flash_firmware (80-123)
  • read_flash (126-136)
  • enter_bootloader (139-148)
packages/jumpstarter/jumpstarter/client/base.py (2)
  • DriverClient (19-99)
  • call (36-46)
packages/jumpstarter/jumpstarter/common/exceptions.py (1)
  • ArgumentError (54-57)
🪛 markdownlint-cli2 (0.17.2)
packages/jumpstarter-driver-esp32/README.md

57-57: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 GitHub Check: ruff
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py

[failure] 176-176: Ruff (B904)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py:176:13: B904 Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling


[failure] 167-167: Ruff (B904)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py:167:13: B904 Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling


[failure] 120-120: Ruff (E722)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py:120:21: E722 Do not use bare except

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

[failure] 71-71: Ruff (C901)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py:71:9: C901 cli is too complex (20 > 10)

🪛 GitHub Actions: Lint
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py

[error] 71-71: Ruff: C901 'cli' is too complex (20 > 10).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Redirect rules - jumpstarter-docs
  • GitHub Check: Header rules - jumpstarter-docs
  • GitHub Check: Pages changed - jumpstarter-docs
  • GitHub Check: e2e
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)
  • GitHub Check: pytest-matrix (macos-15, 3.11)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)
  • GitHub Check: pytest-matrix (macos-15, 3.12)
  • GitHub Check: pytest-matrix (macos-15, 3.13)
🔇 Additional comments (6)
packages/jumpstarter-driver-esp32/pyproject.toml (2)

21-24: Verify source_archive URL template.

...github.com/jumpstarter-dev/repo/... looks placeholder-ish. Confirm the correct repo slug (likely jumpstarter).

Do you want me to open a follow-up to align this with the actual repository name?


31-34: Build backend/tooling alignment check.

You require hatch-pin-jumpstarter. Confirm this hook is present in the workspace (root lists hatch-pin-jumpstarter). LGTM otherwise.

packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (1)

150-159: esptool API usage—double-check kwargs across versions.

esptool.cmds.detect_chip(..., connect_mode='default_reset', connect_attempts=7, trace_enabled=False) may vary by version. Verify these kwargs exist for your pinned range (esptool>=5.0.2).

If needed, we can branch on hasattr(esptool.cmds, "detect_chip") and fall back to esptool.ESPLoader.detect_chip(...).

packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (3)

19-21: LGTM: thin wrapper over driver call.

The chip_info client method is straightforward and matches the driver API.


23-26: LGTM: reset proxy.

Matches the exported driver method name.


43-45: Incorrect — no change needed: OpendalAdapter is already wrapped for sync use.

OpendalAdapter is decorated with @Blocking and @asynccontextmanager (packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py), so using with OpendalAdapter(...) is valid; DriverClient.call_async exists at packages/jumpstarter/jumpstarter/client/core.py:79.

Likely an incorrect or invalid review comment.

Comment on lines +48 to +51
absolute = Path(filepath).resolve()
if not absolute.exists():
raise ArgumentError(f"File not found: {filepath}")
return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden local file handling and FS operator pathing.

  • Accepts directories; should enforce regular file.
  • Using root="/" with an absolute path can be brittle across backends. Prefer root=parent, path=name.

Apply:

-        absolute = Path(filepath).resolve()
-        if not absolute.exists():
-            raise ArgumentError(f"File not found: {filepath}")
-        return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)
+        absolute = Path(filepath).resolve()
+        if not absolute.exists() or not absolute.is_file():
+            raise ArgumentError(f"File not found or not a file: {filepath}")
+        op = Operator("fs", root=str(absolute.parent))
+        return self.flash_firmware(operator=op, path=absolute.name, address=address)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
absolute = Path(filepath).resolve()
if not absolute.exists():
raise ArgumentError(f"File not found: {filepath}")
return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)
absolute = Path(filepath).resolve()
if not absolute.exists() or not absolute.is_file():
raise ArgumentError(f"File not found or not a file: {filepath}")
op = Operator("fs", root=str(absolute.parent))
return self.flash_firmware(operator=op, path=absolute.name, address=address)
🤖 Prompt for AI Agents
In packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py around
lines 48 to 51, the code currently accepts directories and passes an absolute
path with Operator(root="/"), which is brittle; change it to verify the resolved
path is a regular file (not a directory or symlink to a directory) and raise
ArgumentError if not, then build the FS operator using the file's parent
directory as root and pass only the file name (or relative path) as the path
argument to flash_firmware so backends get a portable operator/root + relative
path pairing.

Comment on lines +80 to +84
async def flash_firmware(self, src: str, address: int = 0x1000) -> str:
"""Flash firmware to the ESP32"""
address = int(address)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix default flash address and validate input.

Default should be 0x10000 (matches README/client and common ESP32 app offset). Add non-negative check.

Apply this diff:

-    async def flash_firmware(self, src: str, address: int = 0x1000) -> str:
+    async def flash_firmware(self, src: str, address: int = 0x10000) -> str:
         """Flash firmware to the ESP32"""
         address = int(address)
+        if address < 0:
+            raise ValueError("Flash address must be non-negative")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def flash_firmware(self, src: str, address: int = 0x1000) -> str:
"""Flash firmware to the ESP32"""
address = int(address)
async def flash_firmware(self, src: str, address: int = 0x10000) -> str:
"""Flash firmware to the ESP32"""
address = int(address)
if address < 0:
raise ValueError("Flash address must be non-negative")
🤖 Prompt for AI Agents
In packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py around
lines 80 to 83, the flash_firmware default address is incorrect and lacks
validation: change the function signature default from 0x1000 to 0x10000, ensure
address is converted to int, and add a non-negative check that raises a
ValueError (with a clear message) if the provided address is negative before
proceeding.

Comment on lines 79 to 83
**Flash firmware:**
```{testcode}
esp32client.flash_firmware("/path/to/firmware.bin", address=0x10000)
```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Default flash address inconsistent with driver.

README (0x10000) conflicts with driver default (0x1000). Align to 0x10000 to match typical ESP32 app offset and your client docs.

I’ve proposed a driver fix below; once merged, README will be consistent.

🤖 Prompt for AI Agents
In packages/jumpstarter-driver-esp32/README.md around lines 79 to 83, the
example flash address must match the driver's default; update the README example
so the address used is 0x10000 (the standard ESP32 app offset) and ensure any
surrounding text or examples refer to 0x10000 as the default; after the driver
fix is merged, verify the README matches the driver defaults and adjust any
other docs or examples that reference the old 0x1000 value.

Signed-off-by: Benny Zlotnik <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (5)

149-195: Ruff C901: cli is too complex — quick unblock and longer-term extraction

Add noqa to unblock. Prefer extracting subcommands to module-level fns to actually reduce complexity.

Apply quick fix:

-    def cli(self):
+    def cli(self):  # noqa: C901

Longer-term:

  • Move each command callback to module-level def cmd_xxx(client: ESP32Client, ...).
  • In cli(), set ctx.obj = self and register commands.
  • Reuse _parse_int to eliminate branching in callbacks.

32-39: Unify default flash address with driver (client 0x10000 vs driver 0x1000)

Client and README default to 0x10000 for app partition; driver.flash_firmware currently defaults to 0x1000. This mismatch risks flashing the wrong partition.

Apply in driver.py:

--- a/packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py
+++ b/packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py
@@
-async def flash_firmware(self, src: str, address: int = 0x1000) -> str:
+async def flash_firmware(self, src: str, address: int = 0x10000) -> str:
     """Flash firmware to the ESP32"""

46-51: Harden local file handling and FS operator pathing

Reject directories and use the file’s parent as the FS root to avoid brittle absolute-path usage across backends.

Apply:

 def flash_firmware_file(self, filepath: str, address: int = 0x10000) -> str:
     """Flash a local firmware file to the ESP32"""
     absolute = Path(filepath).resolve()
-    if not absolute.exists():
-        raise ArgumentError(f"File not found: {filepath}")
-    return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)
+    if not absolute.exists() or not absolute.is_file():
+        raise ArgumentError(f"File not found or not a file: {filepath}")
+    op = Operator("fs", root=str(absolute.parent))
+    return self.flash_firmware(operator=op, path=absolute.name, address=address)

97-116: Fail fast on invalid integers; avoid int(float(...)) and silent defaults

Current parsing accepts “1e5” and silently falls back (dangerous for flash ops). Parse with base-0 ints and raise Click errors.

Apply:

     def _parse_address(self, address):
         """Parse address string to integer"""
-        try:
-            if isinstance(address, str) and address.startswith("0x"):
-                return int(address, 16)
-            else:
-                return int(float(address))
-        except (ValueError, TypeError):
-            return 0x10000  # Default fallback
+        return _parse_int(address, param_hint="--address")
@@
     def _parse_size(self, size):
         """Parse size string to integer"""
-        try:
-            if size.startswith("0x"):
-                return int(size, 16)
-            else:
-                return int(float(size))
-        except (ValueError, TypeError):
-            return 1024  # Default fallback
+        return _parse_int(size, param_hint="SIZE")

Add helper near imports:

def _parse_int(value: str | int, *, param_hint: str) -> int:
    try:
        if isinstance(value, str):
            return int(value, 0)  # auto-detect 0x/0o/0b/decimal
        return int(value)
    except Exception:
        raise click.BadParameter(f"invalid integer: {value}", param_hint=param_hint)

124-131: read-flash: fail fast on invalid/negative size

Surface errors early and avoid defaulting silently.

Apply:

     def _read_flash_command(self, address, size, output):
         """Read flash contents"""
         address = self._parse_address(address)
-        size = self._parse_size(size)
+        size = self._parse_size(size)
+        if size <= 0:
+            raise click.BadParameter("size must be positive", param_hint="SIZE")
 
-        print(f"Reading {size} bytes from address 0x{address:x}...")
+        click.echo(f"Reading {size} bytes from address 0x{address:x}...")
🧹 Nitpick comments (6)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (6)

71-96: Prefer click.echo over print for CLI output

Use click.echo for consistent CLI I/O, proper encoding, and testability.

Apply:

-        for key, value in chip_info.items():
-            print(f"{key}: {value}")
+        for key, value in chip_info.items():
+            click.echo(f"{key}: {value}")
@@
-        print(f"Chip Type: {info.get('chip_type', 'Unknown')}")
+        click.echo(f"Chip Type: {info.get('chip_type', 'Unknown')}")
@@
-            print(f"MAC Address: {info['mac_address']}")
+            click.echo(f"MAC Address: {info['mac_address']}")
@@
-            print(f"Chip Revision: {info['chip_revision']}")
+            click.echo(f"Chip Revision: {info['chip_revision']}")
@@
-        result = self.reset()
-        print(result)
+        result = self.reset()
+        click.echo(result)
@@
-        print("Erasing flash...")
+        click.echo("Erasing flash...")
         result = self.erase_flash()
-        print(result)
+        click.echo(result)

77-85: chip-id command: include chip_id in output

The command omits chip_id despite being available in chip_info.

Apply:

         info = self.chip_info()
-        click.echo(f"Chip Type: {info.get('chip_type', 'Unknown')}")
+        click.echo(f"Chip Type: {info.get('chip_type', 'Unknown')}")
+        if "chip_id" in info:
+            click.echo(f"Chip ID: {info['chip_id']}")

117-123: flash: echo status and rely on strict parsing

Use click.echo and rely on the stricter parser to avoid silent defaults.

Apply:

     def _flash_command(self, firmware_file, address):
         """Flash firmware to the device"""
         address = self._parse_address(address)
-        print(f"Flashing {firmware_file} to address 0x{address:x}...")
+        click.echo(f"Flashing {firmware_file} to address 0x{address:x}...")
         result = self.flash_firmware_file(firmware_file, address)
-        print(result)
+        click.echo(result)

132-143: Prefer click.echo and minor hexdump polish

Use click.echo; logic for address offset is fine.

Apply:

         if output:
             with open(output, "wb") as f:
                 f.write(data)
-            print(f"Data written to {output}")
+            click.echo(f"Data written to {output}")
         else:
             # Print as hex
             hex_data = data.hex()
             for i in range(0, len(hex_data), 32):
                 addr_offset = address + i // 2
                 line = hex_data[i : i + 32]
-                print(f"0x{addr_offset:08x}: {line}")
+                click.echo(f"0x{addr_offset:08x}: {line}")

144-148: Use click.echo for consistency

Aligns with other CLI outputs.

Apply:

     def _bootloader_command(self):
         """Enter bootloader mode"""
         result = self.enter_bootloader()
-        print(result)
+        click.echo(result)

175-178: CLI arg: disallow directories for firmware_file

Tighten Click param to fail on directories at parse time.

Apply:

-        @click.argument("firmware_file", type=click.Path(exists=True))
+        @click.argument("firmware_file", type=click.Path(exists=True, dir_okay=False))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb401a and 23b19f5.

📒 Files selected for processing (6)
  • packages/jumpstarter-driver-esp32/README.md (1 hunks)
  • packages/jumpstarter-driver-esp32/examples/exporter.yaml (1 hunks)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (1 hunks)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (1 hunks)
  • packages/jumpstarter-driver-esp32/pyproject.toml (1 hunks)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/jumpstarter-driver-esp32/pyproject.toml
  • pyproject.toml
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py
  • packages/jumpstarter-driver-esp32/README.md
  • packages/jumpstarter-driver-esp32/examples/exporter.yaml
🧰 Additional context used
🧬 Code graph analysis (1)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (4)
packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)
  • OpendalAdapter (60-86)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (6)
  • client (36-37)
  • chip_info (40-58)
  • erase_flash (69-78)
  • flash_firmware (81-124)
  • read_flash (127-141)
  • enter_bootloader (144-153)
packages/jumpstarter/jumpstarter/client/base.py (2)
  • DriverClient (19-99)
  • call (36-46)
packages/jumpstarter/jumpstarter/common/exceptions.py (1)
  • ArgumentError (54-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Redirect rules - jumpstarter-docs
  • GitHub Check: Header rules - jumpstarter-docs
  • GitHub Check: Pages changed - jumpstarter-docs
  • GitHub Check: pytest-matrix (macos-15, 3.13)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)
  • GitHub Check: pytest-matrix (macos-15, 3.12)
  • GitHub Check: e2e
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)
  • GitHub Check: pytest-matrix (macos-15, 3.11)
🔇 Additional comments (3)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (3)

19-22: chip_info: straightforward pass-through looks good

Direct delegation to driver via self.call("chip_info") is clear and consistent with the base client pattern.


53-66: read_flash: sane argument validation and delegation

Non-negative address and positive size checks here are appropriate; remaining logic correctly defers to the driver.


43-45: Confirm OpendalAdapter can be used with a synchronous “with”

If OpendalAdapter is an async context manager (@asynccontextmanager), using it with “with” (not “async with”) will break at runtime. Ensure there’s a sync wrapper or adapt to a blocking wrapper.

Run to verify:

#!/bin/bash
set -euo pipefail
rg -nC2 'def OpendalAdapter' packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py || true
rg -n '@asynccontextmanager' packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py || true
rg -nC2 'resource_async|resource\(' packages/jumpstarter/jumpstarter/client || true

If OpendalAdapter is async-only, either:

  • expose a blocking wrapper (e.g., client.portal.run_cm(OpendalAdapter(...))) or
  • provide a sync variant that uses client.resource(...) internally.

@mangelajo
Copy link
Member

very cool!, make the jumpstarte-all depend on this one! :D

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/jumpstarter-driver-esp32/README.md (1)

49-51: Align default flash address with driver/docs and fix markdownlint MD014

Prior feedback noted 0x10000 as the default app offset; this example still shows 0x1000. Also remove leading “$ ” (MD014).

-```shell
-$ j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x1000
+```console
+j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x10000

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/jumpstarter-driver-esp32/README.md (2)</summary><blockquote>

`24-29`: **YAML indentation consistency for the serial exporter**

Tighten indentation to match the esp32 block; current mix may confuse readers.


```diff
   serial:
-      type: jumpstarter_driver_pyserial.driver.PySerial
-      config:
-        url: "/dev/ttyUSB0"
-        baudrate: 115200
+    type: jumpstarter_driver_pyserial.driver.PySerial
+    config:
+      url: "/dev/ttyUSB0"
+      baudrate: 115200

31-39: Document check_present to match API

Docs page lists check_present; README table omits it. Add for parity.

 | reset_pin    | GPIO pin number for hardware reset (if connected)                    | int  | no       | null        |
 | boot_pin     | GPIO pin number for boot mode control (if connected)                 | int  | no       | null        |
+| check_present| Check if the serial port exists during exporter initialization       | bool | no       | true        |
docs/source/reference/package-apis/drivers/esp32.md (1)

51-68: Add fence language to CLI help block (MD040)

Specify a language to satisfy markdownlint and improve rendering.

-```
+```text
 jumpstarter ⚡ local ➤ j esp32
 Usage: j esp32 [OPTIONS] COMMAND [ARGS]...
 
   ESP32 client
@@
   reset        Reset the device

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 23b19f55b55283ef4d441320a4b295ccd81f517a and 32a8ffbcc0c03dc64a3f65457b7ae77b906204b5.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `docs/source/reference/package-apis/drivers/esp32.md` (1 hunks)
* `docs/source/reference/package-apis/drivers/index.md` (2 hunks)
* `packages/jumpstarter-all/pyproject.toml` (1 hunks)
* `packages/jumpstarter-driver-esp32/README.md` (1 hunks)
* `packages/jumpstarter-driver-esp32/examples/example.py` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

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

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (1)</summary>

<details>
<summary>packages/jumpstarter-driver-esp32/examples/example.py (1)</summary><blockquote>

<details>
<summary>packages/jumpstarter/jumpstarter/utils/env.py (1)</summary>

* `env` (41-50)

</details>

</blockquote></details>

</details><details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/source/reference/package-apis/drivers/esp32.md</summary>

51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>
<details>
<summary>packages/jumpstarter-driver-esp32/README.md</summary>

50-50: Dollar signs used before commands without showing output

(MD014, commands-show-output)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)</summary>

* GitHub Check: Redirect rules - jumpstarter-docs
* GitHub Check: Header rules - jumpstarter-docs
* GitHub Check: Pages changed - jumpstarter-docs
* GitHub Check: pytest-matrix (macos-15, 3.13)
* GitHub Check: e2e
* GitHub Check: pytest-matrix (macos-15, 3.12)
* GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)
* GitHub Check: pytest-matrix (macos-15, 3.11)
* GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)
* GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>packages/jumpstarter-driver-esp32/examples/example.py (1)</summary><blockquote>

`2-2`: **Verify import path for env()**

The function lives at jumpstarter.utils.env in this repo. If there’s no re-export from jumpstarter.common.utils, switch to the canonical import.


```diff
-from jumpstarter.common.utils import env
+from jumpstarter.utils.env import env

If both exist, prefer the latter for directness.

docs/source/reference/package-apis/drivers/esp32.md (1)

42-45: Sphinx autodoc target should not include parentheses

The object path must be dotted import, no “()”. This can break the docs build.

-```{eval-rst}
-.. autoclass:: jumpstarter_driver_esp32.client.ESP32Client()
-    :members: chip_info, reset, erase_flash, flash_firmware, flash_firmware_file, read_flash, enter_bootloader
-```
+```{eval-rst}
+.. autoclass:: jumpstarter_driver_esp32.client.ESP32Client
+   :members: chip_info, reset, erase_flash, flash_firmware, flash_firmware_file, read_flash, enter_bootloader
+```
⛔ Skipped due to learnings
Learnt from: mangelajo
PR: jumpstarter-dev/jumpstarter#526
File: packages/jumpstarter-driver-power/README.md:27-30
Timestamp: 2025-06-18T13:07:24.108Z
Learning: In the jumpstarter project, the Sphinx autoclass directive requires parentheses after the class name (e.g., `.. autoclass:: jumpstarter_driver_power.client.PowerClient()`) to work properly in their documentation setup.

Comment on lines +121 to +125
```{code-block} console
# Get device information
$ j esp32 info
# Flash firmware to default app partition (0x10000)
$ j esp32 flash firmware.bin
# Flash firmware to specific address
$ j esp32 flash firmware.bin --address 0x1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Double-check the default flash address matches the driver constant

This page states 0x10000 as the default (Line 125) and shows an explicit 0x1000 example (Line 129). Ensure the driver’s default matches what we document as “default”.


🏁 Script executed:

#!/bin/bash
# Verify any hardcoded/default flash address in code and docs
rg -nI -C2 -S -e '0x1000\b|0x10000\b|default.*address' \
  packages/jumpstarter-driver-esp32 jumpstarter docs packages | sed -n '1,200p'

Length of output: 5104


Align default flash address between docs and driver (0x1000 vs 0x10000)

Docs declare default 0x10000 (docs/source/reference/package-apis/drivers/esp32.md, ~lines 123–130) but the codebase contains conflicting defaults: an async implementation uses "async def flash_firmware(self, src: str, address: int = 0x1000)" while other functions/CLI/fallbacks use 0x10000 (e.g., "def flash_firmware(..., address: int = 0x10000)", "flash_firmware_file(..., address: int = 0x10000)", CLI --address default "0x10000", _parse_size fallback 0x10000). Decide the correct default, update the discrepant implementation or the docs to match, and centralize the value into a single constant used by code, CLI, and docs.

🤖 Prompt for AI Agents
In docs/source/reference/package-apis/drivers/esp32.md around lines 121–130 the
README shows a default flash address of 0x10000 but the codebase contains mixed
defaults (some functions use 0x1000 and others 0x10000). Pick the correct
canonical default (use 0x10000 to match CLI and most implementations unless
product/team decides otherwise), update the async flash_firmware signature to
use that value (or update the docs if you choose 0x1000), and centralize the
default by adding a single constant (e.g., ESP32_DEFAULT_FLASH_ADDRESS) in the
driver module used by all flash_firmware variants, flash_firmware_file, CLI
default parsing, and _parse_size fallback; then update docs to reference that
constant/default so code and docs stay consistent.

Signed-off-by: Benny Zlotnik <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/jumpstarter-driver-esp32/README.md (1)

49-51: Align flash address example with the driver’s default (0x10000).

The CLI example uses 0x1000, which conflicts with the documented/default 0x10000 elsewhere. Update the example to avoid user confusion.

-```shell
-$ j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x1000
-```
+```shell
+$ j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x10000
+```
docs/source/reference/package-apis/drivers/esp32.md (1)

121-130: Ensure the documented default flash address matches the driver constant (0x10000).

Docs show 0x10000 as default and a 0x1000 explicit example; confirm the driver’s default and CLI default flag value are 0x10000 to avoid drift.

🧹 Nitpick comments (5)
packages/jumpstarter-driver-esp32/README.md (3)

24-29: YAML indentation/style consistency for the serial exporter block.

Indent children of serial: by 2 spaces (like esp32:) for consistency/readability.

   serial:
-      type: jumpstarter_driver_pyserial.driver.PySerial
-      config:
-        url: "/dev/ttyUSB0"
-        baudrate: 115200
+    type: jumpstarter_driver_pyserial.driver.PySerial
+    config:
+      url: "/dev/ttyUSB0"
+      baudrate: 115200

7-10: Fix markdownlint MD014 by removing shell prompts from command-only blocks.

These blocks don’t show output; drop the leading $ to satisfy MD014.

 ```{code-block} console
 :substitutions:
-$ pip3 install --extra-index-url {{index_url}} jumpstarter-driver-esp32
+pip3 install --extra-index-url {{index_url}} jumpstarter-driver-esp32

---

`31-39`: **Document `check_present` config parameter to match implementation/docs.**

Add the missing row so README matches the driver and the reference page.


```diff
 | boot_pin     | GPIO pin number for boot mode control (if connected)                 | int  | no       | null        |
+| check_present | Check if the serial port exists during exporter initialization      | bool | no       | True        |
docs/source/reference/package-apis/drivers/esp32.md (2)

42-45: Remove parentheses from autoclass target; Sphinx expects a class path, not a call.

This can break autodoc resolution in some setups.

 ```{eval-rst}
-.. autoclass:: jumpstarter_driver_esp32.client.ESP32Client()
+.. autoclass:: jumpstarter_driver_esp32.client.ESP32Client
     :members: chip_info, reset, erase_flash, flash_firmware, flash_firmware_file, read_flash, enter_bootloader

---

`51-68`: **Specify language for CLI block to satisfy MD040 and render consistently.**

Use MyST’s code-block directive with `console`.


```diff
-```
+```{code-block} console
 jumpstarter ⚡ local ➤ j esp32
 Usage: j esp32 [OPTIONS] COMMAND [ARGS]...
 
   ESP32 client
 
 Options:
   --help  Show this message and exit.
 
 Commands:
   bootloader   Enter bootloader mode
   chip-id      Get chip ID information
   erase        Erase the entire flash
   flash        Flash firmware to the device
   info         Get device information
   read-flash   Read flash contents
   reset        Reset the device

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 32a8ffbcc0c03dc64a3f65457b7ae77b906204b5 and 2f595da902e978fb3fd230b78c92026f969a7d55.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `docs/source/reference/package-apis/drivers/esp32.md` (1 hunks)
* `docs/source/reference/package-apis/drivers/index.md` (2 hunks)
* `packages/jumpstarter-all/pyproject.toml` (1 hunks)
* `packages/jumpstarter-driver-esp32/README.md` (1 hunks)
* `packages/jumpstarter-driver-esp32/examples/example.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* packages/jumpstarter-all/pyproject.toml
* docs/source/reference/package-apis/drivers/index.md
* packages/jumpstarter-driver-esp32/examples/example.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>docs/source/reference/package-apis/drivers/esp32.md</summary>

51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>
<details>
<summary>packages/jumpstarter-driver-esp32/README.md</summary>

50-50: Dollar signs used before commands without showing output

(MD014, commands-show-output)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)</summary>

* GitHub Check: Redirect rules - jumpstarter-docs
* GitHub Check: Header rules - jumpstarter-docs
* GitHub Check: Pages changed - jumpstarter-docs
* GitHub Check: pytest-matrix (macos-15, 3.13)
* GitHub Check: pytest-matrix (macos-15, 3.12)
* GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)
* GitHub Check: pytest-matrix (macos-15, 3.11)
* GitHub Check: e2e
* GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)
* GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/source/reference/package-apis/drivers/esp32.md (1)

121-129: Align default flash address across docs and examples (use 0x10000).

You state default app partition is 0x10000 (Line 125) but show a 0x1000 example (Line 129). Update the example to 0x10000. Also ensure the driver/CLI default is sourced from a single constant.

-# Flash firmware to specific address
-$ j esp32 flash firmware.bin --address 0x1000
+# Flash firmware to specific address
+$ j esp32 flash firmware.bin --address 0x10000
🧹 Nitpick comments (6)
packages/jumpstarter-driver-esp32/README.md (3)

33-40: Add missing config parameter row: check_present.

README’s table omits check_present, which is documented elsewhere. Add it for consistency.

 | boot_pin     | GPIO pin number for boot mode control (if connected)                 | int  | no       | null        |
+| check_present| Check if the serial port exists during exporter initialization       | bool | no       | True        |

16-29: Clarify YAML example: optional serial exporter is unrelated to ESP32 config.

As written, readers may think ESP32 depends on the “serial” export. Either remove it or label it as an independent, optional exporter.

 export:
   esp32:
     type: jumpstarter_driver_esp32.driver.ESP32
     config:
       port: "/dev/ttyUSB0"
       baudrate: 115200
       chip: "esp32"
-  serial:
+  # Optional: independent serial exporter (not required by ESP32 driver)
+  serial:
       type: jumpstarter_driver_pyserial.driver.PySerial
       config:
         url: "/dev/ttyUSB0"
         baudrate: 115200

49-51: Satisfy markdownlint MD014 (no leading “$” without output).

Use a console fence and drop the prompt character to avoid MD014.

-```shell
-$ j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x1000
+```console
+j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x10000

</blockquote></details>
<details>
<summary>docs/source/reference/package-apis/drivers/esp32.md (3)</summary><blockquote>

`42-45`: **Remove parentheses from autoclass target.**

Sphinx autodoc expects the class path without “()”; parentheses can break linking.

```diff
-```{eval-rst}
-.. autoclass:: jumpstarter_driver_esp32.client.ESP32Client()
+```{eval-rst}
+.. autoclass:: jumpstarter_driver_esp32.client.ESP32Client
     :members: chip_info, reset, erase_flash, flash_firmware, flash_firmware_file, read_flash, enter_bootloader

---

`51-68`: **Specify language for fenced block to fix MD040.**

Add a language to the CLI usage block.

```diff
-```
+```console
 jumpstarter ⚡ local ➤ j esp32
 Usage: j esp32 [OPTIONS] COMMAND [ARGS]...
 
   ESP32 client
 ...

---

`121-142`: **Optional: avoid leading “$” prompts or include outputs.**

To appease MD014 and improve copy-paste, either drop the “$” or show sample outputs.

```diff
-$ j esp32 info
+j esp32 info
...
-# Flash firmware to default app partition (0x10000)
-$ j esp32 flash firmware.bin
+# Flash firmware to default app partition (0x10000)
+j esp32 flash firmware.bin
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f595da and 153674a.

📒 Files selected for processing (4)
  • docs/source/reference/package-apis/drivers/esp32.md (1 hunks)
  • docs/source/reference/package-apis/drivers/index.md (2 hunks)
  • packages/jumpstarter-driver-esp32/README.md (1 hunks)
  • packages/jumpstarter-driver-esp32/examples/example.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/jumpstarter-driver-esp32/examples/example.py
  • docs/source/reference/package-apis/drivers/index.md
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
docs/source/reference/package-apis/drivers/esp32.md

51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

packages/jumpstarter-driver-esp32/README.md

50-50: Dollar signs used before commands without showing output

(MD014, commands-show-output)

🪛 GitHub Actions: documentation
docs/source/reference/package-apis/drivers/esp32.md

[warning] 74-74: Sphinx: Unknown directive type: 'testcode-skip' [myst.directive_unknown]


[warning] 83-83: Sphinx: Unknown directive type: 'testcode-skip' [myst.directive_unknown]


[warning] 97-97: Sphinx: Unknown directive type: 'testcode-skip' [myst.directive_unknown]


[warning] 105-105: Sphinx: Unknown directive type: 'testcode-skip' [myst.directive_unknown]

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Redirect rules - jumpstarter-docs
  • GitHub Check: Header rules - jumpstarter-docs
  • GitHub Check: Pages changed - jumpstarter-docs
  • GitHub Check: pytest-matrix (macos-15, 3.13)
  • GitHub Check: pytest-matrix (macos-15, 3.12)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)
  • GitHub Check: pytest-matrix (macos-15, 3.11)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)
  • GitHub Check: e2e

@bennyz bennyz force-pushed the esp32-driver branch 2 times, most recently from 8a07b34 to 302dd46 Compare September 13, 2025 17:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/jumpstarter-driver-esp32/README.md (1)

49-51: Fix flash address (use 0x10000) and fence language; remove leading “$”.

Using 0x1000 risks clobbering the bootloader; examples should default to the ESP32 app offset 0x10000. Also switch to "console" and drop "$" to satisfy markdownlint.

-```shell
-$ j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x1000
-```
+```console
+j esp32 flash ESP32_GENERIC-20250911-v1.26.1.bin -a 0x10000
+```

To ensure consistency across code/docs, run:

#!/bin/bash
# Verify defaults and examples for flash offset across the repo
rg -nP -C2 '(0x1000|0x10000)\b' --type=md --type=py
🧹 Nitpick comments (3)
packages/jumpstarter-driver-esp32/README.md (3)

24-29: Normalize YAML indentation under serial for readability.

Indent levels are inconsistent vs. the esp32 block. Use 2 spaces per level.

-  serial:
-      type: jumpstarter_driver_pyserial.driver.PySerial
-      config:
-        url: "/dev/ttyUSB0"
-        baudrate: 115200
+  serial:
+    type: jumpstarter_driver_pyserial.driver.PySerial
+    config:
+      url: "/dev/ttyUSB0"
+      baudrate: 115200

41-45: Document the client API alongside the driver.

Expose ESP32Client in the README to align with the CLI users will touch.

 ```{autoclass} jumpstarter_driver_esp32.driver.ESP32
 :members:

+{autoclass} jumpstarter_driver_esp32.client.ESP32Client +:members: +


---

`47-47`: **Use a secondary heading for “Examples”.**

Keep a single H1 per page.  


```diff
-# Examples
+## Examples
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 153674a and 302dd46.

📒 Files selected for processing (4)
  • docs/source/reference/package-apis/drivers/esp32.md (1 hunks)
  • docs/source/reference/package-apis/drivers/index.md (2 hunks)
  • packages/jumpstarter-driver-esp32/README.md (1 hunks)
  • packages/jumpstarter-driver-esp32/examples/example.py (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • docs/source/reference/package-apis/drivers/index.md
  • docs/source/reference/package-apis/drivers/esp32.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/jumpstarter-driver-esp32/examples/example.py
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
packages/jumpstarter-driver-esp32/README.md

51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)
  • GitHub Check: e2e
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)

@bennyz bennyz changed the title esp32: introduce ESP32 driver WIP esp32: introduce ESP32 driver Sep 13, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (1)

81-84: Default flash address is wrong and missing validation — risk of overwriting bootloader.

ESP32 app offset is typically 0x10000. Defaulting to 0x1000 risks flashing over the bootloader. Also add a non‑negative check. This was flagged earlier; still present.

-    async def flash_firmware(self, src: str, address: int = 0x1000) -> str:
+    async def flash_firmware(self, src: str, address: int = 0x10000) -> str:
         """Flash firmware to the ESP32"""
-        address = int(address)
+        address = int(address)
+        if address < 0:
+            raise ValueError("Flash address must be non-negative")
#!/bin/bash
# Verify no other defaults use 0x1000 for flash address
rg -nP --hidden --no-ignore '(flash_firmware\(.+address:\s*int\s*=\s*0x[0-9a-fA-F]+\))|(--address[^\\n]*0x[0-9a-fA-F]+)' -g '!**/dist/**' -g '!**/build/**'
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (4)

32-45: Unify default flash address to 0x10000 (matches common app offset and CLI default).

Keep programmatic and CLI defaults consistent and safe.

-    def flash_firmware(self, operator: Operator, path: str, address: int = 0x1000) -> str:
+    def flash_firmware(self, operator: Operator, path: str, address: int = 0x10000) -> str:
@@
-        if address < 0:
-            raise ArgumentError("Flash address must be non-negative")
+        if address < 0:
+            raise ArgumentError("Flash address must be non-negative")

46-52: Harden local file handling and OpenDAL pathing.

Reject directories and construct the FS Operator with the parent directory as root; pass the filename as path. Aligns with backend expectations and avoids brittle absolute-path + root="/" pairing.

-    def flash_firmware_file(self, filepath: str, address: int = 0x1000) -> str:
+    def flash_firmware_file(self, filepath: str, address: int = 0x10000) -> str:
@@
-        absolute = Path(filepath).resolve()
-        if not absolute.exists():
-            raise ArgumentError(f"File not found: {filepath}")
-        return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)
+        absolute = Path(filepath).resolve()
+        if not absolute.exists() or not absolute.is_file():
+            raise ArgumentError(f"File not found or not a file: {filepath}")
+        op = Operator("fs", root=str(absolute.parent))
+        return self.flash_firmware(operator=op, path=absolute.name, address=address)

101-110: CLI integer parsing accepts floats/scientific notation and silently falls back — fail fast.

int(float(...)) will accept "1e5"; silent fallback to 0x10000 is dangerous. Parse with base autodetection and raise a Click error on invalid input.

-    def _parse_address(self, address):
-        """Parse address string to integer"""
-        try:
-            if isinstance(address, str) and address.startswith("0x"):
-                return int(address, 16)
-            else:
-                return int(float(address))
-        except (ValueError, TypeError):
-            return 0x10000  # Default fallback
+    def _parse_address(self, address):
+        """Parse address string to integer"""
+        return _parse_int(address, param_hint="--address")

Add helper near the imports (outside this hunk):

def _parse_int(value: str | int, *, param_hint: str) -> int:
    try:
        if isinstance(value, str):
            return int(value, 0)  # supports 0x/0o/0b/decimal
        return int(value)
    except Exception:
        raise click.BadParameter(f"invalid integer: {value}", param_hint=param_hint)

111-120: Same for size parsing — reject invalid values instead of defaulting to 1024.

-    def _parse_size(self, size):
-        """Parse size string to integer"""
-        try:
-            if size.startswith("0x"):
-                return int(size, 16)
-            else:
-                return int(float(size))
-        except (ValueError, TypeError):
-            return 1024  # Default fallback
+    def _parse_size(self, size):
+        """Parse size string to integer"""
+        return _parse_int(size, param_hint="SIZE")
🧹 Nitpick comments (5)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (3)

25-27: Call base post_init directly; the hasattr(super(), ...) guard is unnecessary.

Driver defines post_init, so just call it. The current hasattr(super(), "post_init") is unusual and can be dropped.

-    def __post_init__(self):
-        if hasattr(super(), "__post_init__"):
-            super().__post_init__()
+    def __post_init__(self):
+        super().__post_init__()

29-33: Avoid redundant import and limit path existence checks to POSIX serial paths.

You already import os at top; the inner import is redundant. Also, COM ports on Windows won't exist as filesystem paths—this would log a spurious warning. Guard by platform and drop loop:// exemption duplication.

-        if self.check_present and not self.port.startswith(("/dev/null", "loop://")):
-            import os
-
-            if not os.path.exists(self.port):
+        if self.check_present and os.name != "nt" and not self.port.startswith(("loop://",)):
+            if not os.path.exists(self.port):
                 self.logger.warning(f"Serial port {self.port} does not exist")

153-163: enter_bootloader does not actually enter bootloader without hardware pins.

When boot_pin is None, this only calls chip_info and claims success. Either implement a software path (e.g., toggling DTR/RTS or esptool reset-to-bootloader) or adjust messaging to avoid false success.

packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (2)

153-199: Ruff C901: cli() likely exceeds complexity threshold — quick unblock with noqa; plan refactor.

Short-term: add # noqa: C901 on def line. Longer-term: extract command callbacks to module-level functions to reduce complexity.

-    def cli(self):
+    def cli(self):  # noqa: C901

75-100: Prefer click.echo over print for CLI output.

Provides better stdout/stderr handling and unicode safety across platforms.

Examples:

-            print(f"{key}: {value}")
+            click.echo(f"{key}: {value}")

Apply similarly to other print calls in this section and below.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 302dd46 and b1bc760.

📒 Files selected for processing (2)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (1 hunks)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (4)
packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)
  • OpendalAdapter (60-86)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (6)
  • client (36-37)
  • chip_info (40-58)
  • erase_flash (69-78)
  • flash_firmware (81-124)
  • read_flash (127-150)
  • enter_bootloader (153-162)
packages/jumpstarter/jumpstarter/client/base.py (2)
  • DriverClient (19-99)
  • call (36-46)
packages/jumpstarter/jumpstarter/common/exceptions.py (1)
  • ArgumentError (54-57)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (2)
packages/jumpstarter/jumpstarter/driver/base.py (2)
  • Driver (56-269)
  • resource (225-252)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (6)
  • chip_info (19-21)
  • erase_flash (27-30)
  • info (160-162)
  • flash_firmware (32-44)
  • read_flash (53-69)
  • enter_bootloader (71-73)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Redirect rules - jumpstarter-docs
  • GitHub Check: Header rules - jumpstarter-docs
  • GitHub Check: Pages changed - jumpstarter-docs
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)
  • GitHub Check: pytest-matrix (macos-15, 3.12)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)
  • GitHub Check: pytest-matrix (macos-15, 3.13)
  • GitHub Check: pytest-matrix (macos-15, 3.11)
  • GitHub Check: e2e

Signed-off-by: Benny Zlotnik <[email protected]>
Signed-off-by: Benny Zlotnik <[email protected]>
Signed-off-by: Benny Zlotnik <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (5)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (3)

153-199: Ruff C901: cli too complex; quick unblock or extract commands.

Add # noqa: C901 or split callbacks into functions to reduce complexity.

-    def cli(self):
+    def cli(self):  # noqa: C901

Longer-term: move command callbacks to module-level helpers and register with base.add_command(...).


46-52: Harden local file path handling and align default address.

Enforce regular file, build FS operator from parent dir, and switch default to 0x10000.

-    def flash_firmware_file(self, filepath: str, address: int = 0x1000) -> str:
+    def flash_firmware_file(self, filepath: str, address: int = 0x10000) -> str:
         """Flash a local firmware file to the ESP32"""
         absolute = Path(filepath).resolve()
-        if not absolute.exists():
-            raise ArgumentError(f"File not found: {filepath}")
-        return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)
+        if not absolute.exists() or not absolute.is_file():
+            raise ArgumentError(f"File not found or not a file: {filepath}")
+        op = Operator("fs", root=str(absolute.parent))
+        return self.flash_firmware(operator=op, path=absolute.name, address=address)

101-111: CLI parsing: fail fast and parse ints robustly; set default to 0x10000.

Avoid int(float(...)) and silent fallbacks. Use base-0 parsing and raise click.BadParameter. Also update CLI default.

-    def _parse_address(self, address):
-        """Parse address string to integer"""
-        try:
-            if isinstance(address, str) and address.startswith("0x"):
-                return int(address, 16)
-            else:
-                return int(float(address))
-        except (ValueError, TypeError):
-            return 0x1000
+    def _parse_int(self, value, *, param_hint: str) -> int:
+        """Parse integer from str or int with base auto-detection"""
+        try:
+            if isinstance(value, str):
+                return int(value, 0)
+            return int(value)
+        except Exception:
+            raise click.BadParameter(f"invalid integer: {value}", param_hint=param_hint)
@@
-        address = self._parse_address(address)
+        address = self._parse_int(address, param_hint="--address")
@@
-        @click.option("--address", "-a", default="0x1000", type=str, help="Flash address (hex or decimal)")
+        @click.option("--address", "-a", default="0x10000", type=str, help="Flash address (hex like 0x10000 or decimal)")

And in read-flash:

-        address = self._parse_address(address)
-        size = self._parse_size(size)
+        address = self._parse_int(address, param_hint="ADDRESS")
+        size = self._parse_int(size, param_hint="SIZE")
+        if size <= 0:
+            raise click.BadParameter("size must be positive", param_hint="SIZE")

Also applies to: 121-127, 179-183

docs/source/reference/package-apis/drivers/esp32.md (2)

120-125: Avoid suggesting 0x1000 in CLI examples.

To prevent bricking, prefer 0x10000 as explicit address; show “specific address” with a safe alternative (e.g., 0x20000) if needed.

-# Flash firmware to specific address
-$ j esp32 flash firmware.bin --address 0x1000
+# Flash firmware to specific address (example: second app slot)
+$ j esp32 flash firmware.bin --address 0x20000

84-89: Use safe default flash address (0x10000) in example.

Example currently shows 0x1000, which targets the bootloader region on typical ESP32 layouts.

-result = esp32.flash_firmware_file("firmware.bin", address=0x1000)
+result = esp32.flash_firmware_file("firmware.bin", address=0x10000)
🧹 Nitpick comments (4)
packages/jumpstarter-driver-esp32/README.md (1)

31-39: Document the check_present option for parity with driver/docs.

README table omits check_present (bool, default: True). Add it to avoid config drift.

 | chip         | The ESP32 chip type (esp32, esp32s2, esp32s3, esp32c3, etc.)        | str  | no       | esp32       |
 | reset_pin    | GPIO pin number for hardware reset (if connected)                    | int  | no       | null        |
 | boot_pin     | GPIO pin number for boot mode control (if connected)                 | int  | no       | null        |
+| check_present| Check if the serial port exists during exporter initialization       | bool | no       | True        |
docs/source/reference/package-apis/drivers/esp32.md (1)

51-68: Add code block language for CLI usage.

Specify a language to satisfy MD040 and improve rendering.

-```
+```console
 jumpstarter ⚡ local ➤ j esp32
 Usage: j esp32 [OPTIONS] COMMAND [ARGS]...
 ...

</blockquote></details>
<details>
<summary>packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (2)</summary><blockquote>

`111-120`: **Remove silent size fallback; validate explicitly.**

Defaulting to 1024 on parse errors hides user mistakes during flash reads.


```diff
-    def _parse_size(self, size):
-        """Parse size string to integer"""
-        try:
-            if size.startswith("0x"):
-                return int(size, 16)
-            else:
-                return int(float(size))
-        except (ValueError, TypeError):
-            return 1024  # Default fallback
+    # handled via _parse_int(...) and explicit >0 check in callers

75-100: Use click.echo for consistent CLI output.

Improves testability and respects Click’s output redirection.

-        for key, value in chip_info.items():
-            print(f"{key}: {value}")
+        for key, value in chip_info.items():
+            click.echo(f"{key}: {value}")
@@
-        print(result)
+        click.echo(result)
@@
-        print("Erasing flash...")
+        click.echo("Erasing flash...")
@@
-        print(f"Flashing {firmware_file} to address 0x{address:x}...")
+        click.echo(f"Flashing {firmware_file} to address 0x{address:x}...")
@@
-        print(f"Reading {size} bytes from address 0x{address:x}...")
+        click.echo(f"Reading {size} bytes from address 0x{address:x}...")
@@
-            print(f"Data written to {output}")
+            click.echo(f"Data written to {output}")
@@
-                print(f"0x{addr_offset:08x}: {line}")
+                click.echo(f"0x{addr_offset:08x}: {line}")
@@
-        print(result)
+        click.echo(result)

Also applies to: 121-147, 148-152

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1bc760 and e204945.

📒 Files selected for processing (6)
  • docs/source/reference/package-apis/drivers/esp32.md (1 hunks)
  • docs/source/reference/package-apis/drivers/index.md (2 hunks)
  • packages/jumpstarter-driver-esp32/README.md (1 hunks)
  • packages/jumpstarter-driver-esp32/examples/example.py (1 hunks)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (1 hunks)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/jumpstarter-driver-esp32/examples/example.py
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py
🧰 Additional context used
🧬 Code graph analysis (1)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (4)
packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/adapter.py (1)
  • OpendalAdapter (60-86)
packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py (6)
  • client (36-37)
  • chip_info (40-58)
  • erase_flash (69-78)
  • flash_firmware (81-124)
  • read_flash (127-150)
  • enter_bootloader (153-162)
packages/jumpstarter/jumpstarter/client/base.py (2)
  • DriverClient (19-99)
  • call (36-46)
packages/jumpstarter/jumpstarter/common/exceptions.py (1)
  • ArgumentError (54-57)
🪛 markdownlint-cli2 (0.17.2)
docs/source/reference/package-apis/drivers/esp32.md

51-51: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

packages/jumpstarter-driver-esp32/README.md

50-50: Dollar signs used before commands without showing output

(MD014, commands-show-output)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Redirect rules - jumpstarter-docs
  • GitHub Check: Header rules - jumpstarter-docs
  • GitHub Check: Pages changed - jumpstarter-docs
  • GitHub Check: pytest-matrix (macos-15, 3.12)
  • GitHub Check: pytest-matrix (macos-15, 3.13)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.13)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.12)
  • GitHub Check: pytest-matrix (ubuntu-24.04, 3.11)
  • GitHub Check: e2e
🔇 Additional comments (3)
docs/source/reference/package-apis/drivers/index.md (2)

76-76: LGTM: ESP32 driver entry added to list.

Entry reads clearly and matches package name.


90-90: LGTM: toctree includes esp32.md.

Ensures the page is built and discoverable.

packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py (1)

32-32: Incorrect: repository defaults use 0x1000 (client, driver, README).

All occurrences use 0x1000 — matches found at:

  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/driver.py:81 — async def flash_firmware(... address: int = 0x1000)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py:32 — def flash_firmware(... address: int = 0x1000)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py:46 — def flash_firmware_file(... address: int = 0x1000)
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py:109 — return 0x1000
  • packages/jumpstarter-driver-esp32/jumpstarter_driver_esp32/client.py:181 — click.option("--address", default="0x1000")
  • packages/jumpstarter-driver-esp32/README.md:50 — example uses -a 0x1000

No change required; if you want to standardize on 0x10000, update the locations above.

Likely an incorrect or invalid review comment.

Comment on lines +32 to +45
def flash_firmware(self, operator: Operator, path: str, address: int = 0x1000) -> str:
"""Flash firmware to the ESP32
Args:
operator: OpenDAL operator for file access
path: Path to firmware file
address: Flash address
"""
if address < 0:
raise ArgumentError("Flash address must be non-negative")

with OpendalAdapter(client=self, operator=operator, path=path) as handle:
return self.call("flash_firmware", handle, address)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Blocking: async context manager misused; this will fail at runtime.

OpendalAdapter is async; using with ... in a sync method will raise. Run the async context inside the portal and call the async RPC.

-    def flash_firmware(self, operator: Operator, path: str, address: int = 0x1000) -> str:
+    def flash_firmware(self, operator: Operator, path: str, address: int = 0x10000) -> str:
         """Flash firmware to the ESP32
@@
-        if address < 0:
+        if address < 0:
             raise ArgumentError("Flash address must be non-negative")
 
-        with OpendalAdapter(client=self, operator=operator, path=path) as handle:
-            return self.call("flash_firmware", handle, address)
+        async def _run():
+            async with OpendalAdapter(client=self, operator=operator, path=path) as handle:
+                return await self.call_async("flash_firmware", handle, address)
+        return self.portal.call(_run)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def flash_firmware(self, operator: Operator, path: str, address: int = 0x1000) -> str:
"""Flash firmware to the ESP32
Args:
operator: OpenDAL operator for file access
path: Path to firmware file
address: Flash address
"""
if address < 0:
raise ArgumentError("Flash address must be non-negative")
with OpendalAdapter(client=self, operator=operator, path=path) as handle:
return self.call("flash_firmware", handle, address)
def flash_firmware(self, operator: Operator, path: str, address: int = 0x10000) -> str:
"""Flash firmware to the ESP32
Args:
operator: OpenDAL operator for file access
path: Path to firmware file
address: Flash address
"""
if address < 0:
raise ArgumentError("Flash address must be non-negative")
async def _run():
async with OpendalAdapter(client=self, operator=operator, path=path) as handle:
return await self.call_async("flash_firmware", handle, address)
return self.portal.call(_run)

@bennyz bennyz changed the title WIP esp32: introduce ESP32 driver esp32: introduce ESP32 driver Sep 14, 2025
@bennyz bennyz requested a review from mangelajo September 14, 2025 11:02
--help Show this message and exit.
Commands:
bootloader Enter bootloader mode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 awesome!

Copy link
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks great, just some comments about fragmentation of interfaces, IMHO at least all the small MCU embedded should follow the same pattern, I am happy to change probe-rs as I am sure it has no users yet but me.

self.logger.info("Erasing flash... this may take a while")
return self.call("erase_flash")

def flash_firmware(self, operator: Operator, path: str, address: int = 0x1000) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make it follow an extended https://github.com/jumpstarter-dev/jumpstarter/blob/main/packages/jumpstarter-driver-opendal/jumpstarter_driver_opendal/client.py#L582 flash interface

I should do the same in probe-rs myself

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack

raise ArgumentError(f"File not found: {filepath}")
return self.flash_firmware(operator=Operator("fs", root="/"), path=str(absolute), address=address)

def read_flash(self, address: int, size: int) -> bytes:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

under flash interface this is "dump"

Perhaps we cannot follow exactly the same interface, but implement on top of this? (as this gives bytes, and of course can be useful)

I should do the same here:

https://github.com/jumpstarter-dev/jumpstarter/blob/main/packages/jumpstarter-driver-probe-rs/jumpstarter_driver_probe_rs/client.py

Whatever we end up implementing in this esp32 patch, I will unify on the probe-rs :) perhaps we should document it so all similar drivers aren't fragmented.

@mangelajo
Copy link
Member

@bennyz I'd say, let's merge it, and let's consider esp32 and probers interfaces not stable, then we can unify them whenever we have time.

WDYT?

@bennyz
Copy link
Member Author

bennyz commented Sep 22, 2025

@bennyz I'd say, let's merge it, and let's consider esp32 and probers interfaces not stable, then we can unify them whenever we have time.

WDYT?

I have made some changes, to the interface as well, I need to fix an issue with the dump method and i'll post it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants