Skip to content

Commit 9ddc9fa

Browse files
JensOgorekfalkoschindlerclaude
authored
Serial bus No. 2: Update over Serial (#182)
* added base version of serial bus * freertos 1000 hz * added ram debugging * add receive answers * updated logic of serial bus coordinator and peers and documentation * add ota * update ota * update to work with bus connected to expander * update lower chunk size default * update docs * update remove unnecessary information * removed debug * update minimal fixes * remove debug * code review * refactor * refactor & error handling * Agent Review fix * default handling & relay simplification * refactoring * refactoring serial_bus_ota * update and refactor code * documentation * AI Review * simplify verification and just use esp_ota_end() to verify * add line breaks in Markdown files * cleanup whitespace, reduce diff * simplify CSV parsing * adressing review * remove obsolete OTA server * remove outdated "OTA" references * tiny formatting improvement * remove `parse_ota_partition` and use default range * simplify OTB protocol and otb_update.py * update otb & readd sliding window (4x speed) * changes based on review * match const pattern of other files * fix docs * minor Markdown improvements * improve some comments * refactor OTB responses from polled buffer to send callback * auto-format otb.cpp * remove bus_reset_session from header file * avoid re-implementing base64 decoder * resove the `abort_flash` parameter * remove redundant check * respond with an error instead of silently logging an unknown message type * handle session mismatch consistently * call millis() inside the function * make more variables const * use human-readable error messages * added handling for error and ack msgs --------- Co-authored-by: Falko Schindler <falko@zauberzeug.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 87fbf2d commit 9ddc9fa

File tree

19 files changed

+441
-484
lines changed

19 files changed

+441
-484
lines changed

.cursor/rules/python-tools.mdc

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ Python scripts in this project are **build/flash/monitor tools**, not the main a
1616

1717
## Key Scripts
1818

19-
| Script | Purpose |
20-
|--------|---------|
21-
| `build.py` | Build for ESP32/ESP32-S3 (wraps `idf.py build`) |
22-
| `flash.py` | Flash firmware to device |
23-
| `monitor.py` | Serial monitor |
24-
| `espresso.py` | OTA deployment tool |
25-
| `configure.py` | Project configuration |
26-
| `core_dumper.py` | Analyze core dumps |
19+
| Script | Purpose |
20+
| ---------------- | ----------------------------------------------- |
21+
| `build.py` | Build for ESP32/ESP32-S3 (wraps `idf.py build`) |
22+
| `flash.py` | Flash firmware to device |
23+
| `monitor.py` | Serial monitor |
24+
| `espresso.py` | Deployment tool |
25+
| `configure.py` | Project configuration |
26+
| `core_dumper.py` | Analyze core dumps |
2727

2828
## Pattern
2929

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ lizard/
9393
│ ├── parser.h # Generated parser (from language.owl via gen_parser.sh)
9494
│ ├── compilation/ # DSL compilation (expressions, variables, routines, rules)
9595
│ ├── modules/ # Hardware modules (motors, sensors, I/O, CAN, etc.)
96-
│ └── utils/ # Utilities (UART, OTA, timing, string helpers)
96+
│ └── utils/ # Utilities (UART, OTB updates, timing, string helpers)
9797
├── components/ # ESP-IDF components and submodules
9898
├── docs/ # MkDocs documentation
99-
├── examples/ # Usage examples (ROS integration, OTA, trajectories)
99+
├── examples/ # Usage examples (ROS integration, trajectories)
100100
├── build.py # Build script (wraps idf.py)
101101
├── flash.py # Flash script
102102
├── monitor.py # Serial monitor

docs/module_reference.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,16 @@ It is automatically created right after the boot sequence.
3232
| `core.print(...)` | Print arbitrary arguments to the command line | arbitrary |
3333
| `core.output(format)` | Define the output format | `str` |
3434
| `core.startup_checksum()` | Show 16-bit checksum of the startup script | |
35-
| `core.ota(ssid, password, url)` | Starts OTA update on a URL with given WiFi | 3x `str` |
3635
| `core.get_pin_status(pin)` | Print the status of the chosen pin | `int` |
3736
| `core.set_pin_level(pin, value)` | Turns the pin into an output and sets its level | `int`, `int` |
3837
| `core.get_pin_strapping(pin)` | Print value of the pin from the strapping register | `int` |
38+
| `core.pause_broadcasts()` | Pause property broadcasts (all modules) | |
39+
| `core.resume_broadcasts()` | Resume property broadcasts | |
3940

4041
The output `format` is a string with multiple space-separated elements of the pattern `<module>.<property>[:<precision>]` or `<variable>[:<precision>]`.
4142
The `precision` is an optional integer specifying the number of decimal places for a floating point number.
4243
For example, the format `"core.millis input.level motor.position:3"` might yield an output like `"92456 1 12.789"`.
4344

44-
The OTA update will try to connect to the specified WiFi network with the provided SSID and password.
45-
After initializing the WiFi connection, it will attempt an OTA update from the given URL.
46-
Upon successful updating, the ESP will restart and attempt to verify the OTA update.
47-
It will reconnect to the WiFi and try to access URL + `/verify` to receive a message with the current version of Lizard.
48-
The test is considered successful if an HTTP request is received, even if the version does not match or is empty.
49-
If the newly updated Lizard cannot connect to URL + `/verify`, the OTA update will be rolled back.
50-
5145
`core.get_pin_status(pin)` reads the pin's voltage, not the output state directly.
5246

5347
## Bluetooth
@@ -90,6 +84,11 @@ The serial bus module lets multiple ESP32s share a UART link with a coordinator
9084
| `bus.send(receiver, payload)` | Send a single line of text to a peer `receiver` (0-255) | `int`, `str` |
9185
| `bus.make_coordinator(peer_ids...)` | Set the list of peer IDs, making this node the coordinator | `int`s |
9286

87+
**Firmware Updates:**
88+
Peers on the serial bus can be updated remotely via the coordinator.
89+
Use the `otb_update.py` tool to push new firmware to any peer node.
90+
See [OTB Update](tools.md#otb-update) for details.
91+
9392
## Input
9493

9594
The input module is associated with a digital input pin that is be connected to a pushbutton, sensor or other input signal.

docs/tools.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,82 @@ You can also use an SSH monitor to access a microcontroller via SSH:
3636

3737
Note that the serial monitor cannot communicate while the serial interface is busy communicating with another process.
3838

39+
### OTB Update
40+
41+
`otb_update.py` pushes firmware to a peer over a `SerialBus` coordinator using the OTB (Over The Bus) protocol.
42+
43+
```bash
44+
./otb_update.py build/lizard.bin --port /dev/ttyUSB0 --target <peer_id> [--bus <name>] [--expander <name>]
45+
```
46+
47+
| Argument | Description |
48+
| ------------ | ----------------------------------------------------- |
49+
| `firmware` | Path to the firmware binary (e.g. `build/lizard.bin`) |
50+
| `--port` | Serial port (default: `/dev/ttyUSB0`) |
51+
| `--baud` | Baudrate (default: `115200`) |
52+
| `--target` | Bus ID of the target node (required) |
53+
| `--bus` | Name of the SerialBus module (default: `bus`) |
54+
| `--expander` | Expander name when coordinator is behind an expander |
55+
56+
**Expander chains:**
57+
58+
When the SerialBus coordinator sits behind an expander (e.g. `p0`), pass `--expander p0`.
59+
The script will pause broadcasts on that expander via `core.pause_broadcasts()` before the transfer
60+
and resume them afterwards to keep the UART link clear.
61+
62+
Example:
63+
64+
```bash
65+
./otb_update.py build/lizard.bin --port /dev/ttyUSB0 --target 1 --expander p0
66+
```
67+
68+
This flashes node 1 through expander `p0`.
69+
The target node will reboot with the new firmware after a successful transfer.
70+
71+
**OTB Protocol:**
72+
73+
The OTB (Over The Bus) protocol uses these message types:
74+
75+
| Host → Target | Description |
76+
| -------------------------- | ---------------------------------------------------------- |
77+
| `__OTB_BEGIN__` | Begin firmware update session |
78+
| `__OTB_CHUNK_<seq>__:data` | Send base64-encoded firmware chunk (incl. sequence number) |
79+
| `__OTB_COMMIT__` | Commit update and set boot partition |
80+
| `__OTB_ABORT__` | Cancel the update session |
81+
82+
| Host ← Target | Description |
83+
| ------------------------- | ----------------------------------------- |
84+
| `__OTB_ACK_BEGIN__` | Acknowledge begin |
85+
| `__OTB_ACK_CHUNK_<seq>__` | Acknowledge chunk (incl. sequence number) |
86+
| `__OTB_ACK_COMMIT__` | Acknowledge commit |
87+
| `__OTB_ERROR__:reason` | Error response with reason code |
88+
89+
Flow:
90+
91+
```
92+
Host Target
93+
| |
94+
|--- __OTB_BEGIN__ ------------->|
95+
|<-- __OTB_ACK_BEGIN__ ----------|
96+
| |
97+
|--- __OTB_CHUNK_<0>__:... ----->|
98+
|<-- __OTB_ACK_CHUNK_<0>__ ------|
99+
| |
100+
|--- __OTB_CHUNK_<1>__:... ----->|
101+
|<-- __OTB_ACK_CHUNK_<1>__ ------|
102+
| |
103+
| ... more chunks ... |
104+
| |
105+
|--- __OTB_CHUNK_<N-1>__:... --->|
106+
|<-- __OTB_ACK_CHUNK_<N-1>__ ----|
107+
| |
108+
|--- __OTB_COMMIT__ ------------>|
109+
|<-- __OTB_ACK_COMMIT__ ---------|
110+
| |
111+
```
112+
113+
On error at any point, the target responds with `__OTB_ERROR__:<message>` with a human-readable error message.
114+
39115
### Configure
40116

41117
Use the configure script to send a new startup script to the microcontroller.

espresso.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import sys
55
import time
66
from contextlib import contextmanager
7-
from typing import Generator
87
from pathlib import Path
8+
from typing import Generator
99
import argparse
1010

1111
try:
@@ -101,6 +101,7 @@ def release(self) -> None:
101101
parser.add_argument('--swap', action='store_true', help='Swap En and G0 pins for piggyboard version lower than v0.5')
102102
parser.add_argument('--firmware', default='build/lizard.bin', help='Path to firmware binary')
103103
parser.add_argument('--chip', choices=['esp32', 'esp32s3'], default='esp32', help='ESP chip type')
104+
parser.add_argument('--reset-partition', action='store_true', help='Reset to default OTA partition after flashing')
104105
parser.add_argument('-d', '--dry-run', action='store_true', help='Dry run')
105106
parser.add_argument('--device', nargs='?', default=DEFAULT_DEVICE, help='Serial device path (auto-detected on Jetson)')
106107

@@ -201,6 +202,22 @@ def erase() -> None:
201202
raise RuntimeError('Failed to erase flash.')
202203

203204

205+
def reset_partition() -> None:
206+
"""Reset the OTA partition to the default state."""
207+
print_bold('Resetting partition to "ota_0"...')
208+
success = run(
209+
'esptool.py',
210+
'--chip', args.chip,
211+
'--port', DEVICE,
212+
'--baud', '115200',
213+
'erase_region',
214+
'0xf000', # otadata partition offset (default ESP-IDF partition table)
215+
'0x2000', # otadata partition size (default ESP-IDF partition table)
216+
)
217+
if not success:
218+
raise RuntimeError('Failed to reset OTA partition.')
219+
220+
204221
def flash() -> None:
205222
"""Flash the microcontroller."""
206223
print_bold('Flashing...')
@@ -224,6 +241,8 @@ def flash() -> None:
224241
)
225242
if not success:
226243
raise RuntimeError('Flashing failed. Use "sudo" and check your parameters.')
244+
if args.reset_partition:
245+
reset_partition()
227246

228247

229248
def run(*run_args: str) -> bool:

examples/ota_example/main.py

Lines changed: 0 additions & 62 deletions
This file was deleted.

main/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ idf_component_register(
2525
esp_timer
2626
esp_wifi
2727
lwip
28+
mbedtls
2829
nvs_flash
2930
spi_flash
3031
)

main/main.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
#include "rom/gpio.h"
1919
#include "rom/uart.h"
2020
#include "storage.h"
21-
#include "utils/ota.h"
2221
#include "utils/tictoc.h"
2322
#include "utils/timing.h"
2423
#include "utils/uart.h"
@@ -420,12 +419,6 @@ void app_main() {
420419
echo("error while loading startup script: %s", e.what());
421420
}
422421

423-
try {
424-
xTaskCreate(&ota::verify_task, "ota_verify_task", 8192, NULL, 5, NULL);
425-
} catch (const std::runtime_error &e) {
426-
echo("error while verifying OTA: %s", e.what());
427-
}
428-
429422
printf("\nReady.\n");
430423

431424
while (true) {

main/modules/core.cpp

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#include "core.h"
22
#include "../global.h"
33
#include "../storage.h"
4-
#include "../utils/ota.h"
54
#include "../utils/string_utils.h"
65
#include "../utils/timing.h"
76
#include "../utils/uart.h"
@@ -80,14 +79,6 @@ void Core::call(const std::string method_name, const std::vector<ConstExpression
8079
checksum += c;
8180
}
8281
echo("checksum: %04x", checksum);
83-
} else if (method_name == "ota") {
84-
Module::expect(arguments, 3, string, string, string);
85-
auto *params = new ota::ota_params_t{
86-
arguments[0]->evaluate_string(),
87-
arguments[1]->evaluate_string(),
88-
arguments[2]->evaluate_string(),
89-
};
90-
xTaskCreate(ota::ota_task, "ota_task", 8192, params, 5, nullptr);
9182
} else if (method_name == "get_pin_status") {
9283
Module::expect(arguments, 1, integer);
9384
const int gpio_num = arguments[0]->evaluate_integer();
@@ -163,6 +154,14 @@ void Core::call(const std::string method_name, const std::vector<ConstExpression
163154
echo("Not a strapping pin");
164155
break;
165156
}
157+
} else if (method_name == "pause_broadcasts") {
158+
Module::expect(arguments, 0);
159+
Module::broadcast_paused = true;
160+
echo("broadcasts paused");
161+
} else if (method_name == "resume_broadcasts") {
162+
Module::expect(arguments, 0);
163+
Module::broadcast_paused = false;
164+
echo("broadcasts resumed");
166165
} else {
167166
Module::call(method_name, arguments);
168167
}

main/modules/expander.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ void Expander::deinstall() {
229229
}
230230

231231
void Expander::send_proxy(const std::string module_name, const std::string module_type, const std::vector<ConstExpression_ptr> arguments) {
232-
static char buffer[256];
232+
static char buffer[512];
233233
int pos = csprintf(buffer, sizeof(buffer), "%s = %s(", module_name.c_str(), module_type.c_str());
234234
pos += write_arguments_to_buffer(arguments, &buffer[pos], sizeof(buffer) - pos);
235235
pos += csprintf(&buffer[pos], sizeof(buffer) - pos, "); ");
@@ -238,14 +238,14 @@ void Expander::send_proxy(const std::string module_name, const std::string modul
238238
}
239239

240240
void Expander::send_property(const std::string proxy_name, const std::string property_name, const ConstExpression_ptr expression) {
241-
static char buffer[256];
241+
static char buffer[512];
242242
int pos = csprintf(buffer, sizeof(buffer), "%s.%s = ", proxy_name.c_str(), property_name.c_str());
243243
pos += expression->print_to_buffer(&buffer[pos], sizeof(buffer) - pos);
244244
this->serial->write_checked_line(buffer, pos);
245245
}
246246

247247
void Expander::send_call(const std::string proxy_name, const std::string method_name, const std::vector<ConstExpression_ptr> arguments) {
248-
static char buffer[256];
248+
static char buffer[512];
249249
int pos = csprintf(buffer, sizeof(buffer), "%s.%s(", proxy_name.c_str(), method_name.c_str());
250250
pos += write_arguments_to_buffer(arguments, &buffer[pos], sizeof(buffer) - pos);
251251
pos += csprintf(&buffer[pos], sizeof(buffer) - pos, ")");

0 commit comments

Comments
 (0)