igrr/hotreload

0.9.0

Latest
uploaded 6 days ago
Runtime hot reload for ESP chips - load and reload ELF modules without reflashing. Enables rapid iteration during development by updating code over HTTP while the device keeps running.

readme

# ESP-IDF Hot Reload Component

Runtime hot reload for ESP chips - load and reload ELF modules without reflashing. Enables rapid iteration during development by updating code over HTTP while the device keeps running.

## Features

- **Runtime ELF Loading**: Load position-independent code from flash or RAM at runtime
- **HTTP Server**: Upload and reload code over the network
- **CMake Integration**: Simple `RELOADABLE` keyword in `idf_component_register()` handles all build complexity
- **PSRAM Support**: On chips with PSRAM, load reloadable code into external memory
- **Multi-Architecture**: Supports both Xtensa and RISC-V instruction sets

## Requirements

- ESP-IDF v5.0 or later
- See [Supported Targets](#supported-targets) below

## Installation

Add to your project's `idf_component.yml`:

```yaml
dependencies:
  igrr/hotreload: "*"
```

## Quick Start

Suppose you have an application which looks like this:

```c
void app_main(void) {
    init();
    while (true) {
        do_work();
    }
}
```

and you want to iterate on `do_work`. This component allows you to do so, without reflashing and restarting the entire application!

### 1. Move the Function into a Reloadable Component

If the code you want to reload is not yet in a separate component, create one and move the code there:

```
components/reloadable/
├── CMakeLists.txt
├── include/
│   └── reloadable.h
└── reloadable.c
```

**reloadable.h**:
```c
#pragma once

void do_work(void);
```

**reloadable.c**:
```c
#include <stdio.h>
#include "reloadable.h"

void do_work(void) {
    printf("Hello from reloadable code!\n");
}
```

Add `RELOADABLE` option to `idf_component_register` call in your **CMakeLists.txt**:
```cmake
idf_component_register(
    RELOADABLE
    INCLUDE_DIRS "include"
    PRIV_REQUIRES esp_system
    SRCS reloadable.c
)
```

You can also make an existing component reloadable via sdkconfig without modifying its CMakeLists.txt:

```
CONFIG_HOTRELOAD_COMPONENTS="reloadable"
```

### 2. Update the Application Code

Modify your main loop to load the reloadable code and check for updates:

```c
#include "hotreload.h"
#include "reloadable.h"      // Your reloadable API (contains do_work)

void app_main(void) {
    init();

    // Load the reloadable ELF from flash
    hotreload_config_t config = HOTRELOAD_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(hotreload_load(&config));

    while (true) {
        do_work();

        // Check for updates at a safe point (no reloadable code on the stack)
        if (hotreload_update_available()) {
            printf("Update available, reloading...\n");
            hotreload_reload(&config);
        }
    }
}
```

If you need to suspend or reinitialize something when the code is reloaded (e.g. background tasks that call reloadable functions), do so before and after the reload:

```c
        if (hotreload_update_available()) {
            suspend_background_tasks();
            hotreload_reload(&config);
            resume_background_tasks();
        }
```

### 3. Add a Partition for Reloadable Code

Add `hotreload` partition to your `partitions.csv`:

```csv
# Name,     Type, SubType, Offset,  Size,   Flags
nvs,        data, nvs,     0x9000,  0x6000,
phy_init,   data, phy,     0xf000,  0x1000,
factory,    app,  factory, 0x10000, 1M,
hotreload,  app,  0x40,    ,        512k,
```

### 4. Build and Flash

```bash
idf.py build flash monitor
```

The build system automatically:
1. Compiles your reloadable code as a shared library
2. Generates stub functions and symbol table
3. Strips and optimizes the ELF
4. Flashes it to the `hotreload` partition

### 5. Update Code at Runtime

Modify `reloadable.c`, rebuild, and flash just the reloadable partition:

```bash
idf.py build hotreload-flash
```

Or use the HTTP server for over-the-air updates (see below).

## HTTP Server for OTA Reload

Start the HTTP server to enable over-the-air updates. The server uses a **cooperative reload** model: it receives uploads but does NOT automatically trigger reload. Your application must poll for updates and reload at safe points.

```c
#include "hotreload.h"

void app_main(void) {
    // Initialize WiFi first...

    // Load initial code
    hotreload_config_t config = HOTRELOAD_CONFIG_DEFAULT();
    hotreload_load(&config);

    // Start HTTP server (receives uploads, does NOT auto-reload)
    hotreload_server_config_t server_config = HOTRELOAD_SERVER_CONFIG_DEFAULT();
    hotreload_server_start(&server_config);

    // Main loop with cooperative reload
    while (1) {
        // Call reloadable functions
        reloadable_do_work();

        // Check for updates at safe point (no reloadable code on stack)
        if (hotreload_update_available()) {
            printf("Update available, reloading...\n");
            hotreload_reload(&config);
        }

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}
```

### HTTP Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/upload` | POST | Upload ELF file to flash partition |
| `/pending` | GET | Check if an update is pending reload |
| `/status` | GET | Check server status |

### Upload with curl

```bash
# Build the reloadable ELF
idf.py build

# Upload (reload happens when app polls hotreload_update_available())
curl -X POST --data-binary @build/esp-idf/reloadable/libreloadable_stripped.so \
    http://192.168.1.100:8080/upload

# Check if update is pending
curl http://192.168.1.100:8080/pending
```

### Using idf.py Commands

The component provides two idf.py commands for convenient development:

#### idf.py reload

Build and send the reloadable ELF to the device in one step:

```bash
# Set device URL (or use --url option)
export HOTRELOAD_URL=http://192.168.1.100:8080

# Build and reload
idf.py reload

# Or with explicit URL
idf.py reload --url http://192.168.1.100:8080
```

#### idf.py watch

Watch source files and automatically reload on changes:

```bash
# Start watching (Ctrl+C to stop)
idf.py watch --url http://192.168.1.100:8080

# With custom debounce time
idf.py watch --url http://192.168.1.100:8080 --debounce 1.0
```

The watch command:
1. Monitors components marked with `RELOADABLE` or listed in `CONFIG_HOTRELOAD_COMPONENTS` for file changes
2. Waits for changes to settle (debouncing)
3. Automatically rebuilds and uploads to the device
4. Shows build errors inline

## API Reference

See [API.md](API.md) for the complete API documentation.

## How It Works

See [HOW_IT_WORKS.md](HOW_IT_WORKS.md) for a detailed explanation of the architecture, constraints, and implementation.

**Key points:**
- Calls to reloadable functions go through stub functions and a symbol table
- When code is reloaded, only the symbol table pointers are updated
- Reload must only be triggered when no reloadable functions are on any task's call stack

## Example Project

See `examples/basic/` for a complete working example with:
- Reloadable component with math functions
- HTTP server for OTA updates
- Unity tests for the ELF loader
- QEMU testing support

## Supported Targets

The canonical list of supported targets is in [`idf_component.yml`](idf_component.yml).

| Target | Architecture | Build Command | Notes |
|--------|-------------|---------------|-------|
| ESP32-S3 | Xtensa | `idf.py --preset esp32s3-hardware build` | PSRAM supported |
| ESP32-S2 | Xtensa | `idf.py --preset esp32s2-hardware build` | PSRAM supported |
| ESP32-C3 | RISC-V | `idf.py --preset esp32c3-hardware build` | |

## Testing

```bash
cd test_apps/hotreload_test

# Build for your target
idf.py --preset <target>-hardware build

# Run tests (replace <target> and port)
pytest test_hotreload.py::test_hotreload_unit_tests_hardware -v -s \
    --embedded-services esp,idf \
    --port /dev/cu.usbserial-XXXX \
    --target <target> \
    --build-dir build/<target>-hardware
```

## License

MIT License - see [LICENSE](LICENSE) for details.

Maintainer

  • Ivan Grokhotkov <ivan@espressif.com>

License: MIT

To add this component to your project, run:

idf.py add-dependency "igrr/hotreload^0.9.0"

download archive

Stats

  • Archive size
    Archive size ~ 113.70 KB
  • Downloaded in total
    Downloaded in total 4 times
  • Downloaded this version
    This version: 0 times

Badge

igrr/hotreload version: 0.9.0
|