Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 35 additions & 32 deletions configure.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
#!/usr/bin/env python3
import sys
import argparse
import json
import time
from pathlib import Path
from typing import Iterator

import serial

if len(sys.argv) != 3:
print(f'Usage: {sys.argv[0]} <config_file> <device_path>')
sys.exit()
parser = argparse.ArgumentParser(description='Configure an ESP32 running Lizard firmware')
parser.add_argument('config_file', help='Path to the .liz configuration file')
parser.add_argument('device_path', help='Serial device path (e.g., /dev/ttyUSB0)')
parser.add_argument('--serial-bus', type=int, metavar='NODE_ID',
help='Send configuration via serial bus to the specified node ID')
parser.add_argument('--bus-name', default='bus',
help='Name of the SerialBus module (default: bus)')
args = parser.parse_args()

txt_path, usb_path = sys.argv[1:]


def send(line_: str) -> None:
def send(payload: str) -> None:
"""Send a payload string to the ESP32, optionally over a serial bus."""
line_ = f'{args.bus_name}.send({args.serial_bus}, {json.dumps(payload)})' if args.serial_bus else payload
print(f'Sending: {line_}')
checksum_ = 0
for c in line_:
checksum_ ^= ord(c)
port.write((f'{line_}@{checksum_:02x}\n').encode())


with serial.Serial(usb_path, baudrate=115200, timeout=1.0) as port:
startup = Path(txt_path).read_text('utf-8')
if not startup.endswith('\n'):
startup += '\n'
def read(*, timeout: float) -> Iterator[str]:
"""Yield lines read from the serial port until the timeout expires."""
deadline = time.time() + timeout
while time.time() < deadline:
try:
yield port.read_until(b'\r\n').decode().rsplit('@', 1)[0]
except UnicodeDecodeError:
continue


with serial.Serial(args.device_path, baudrate=115200, timeout=1.0) as port:
startup = Path(args.config_file).read_text('utf-8') + '\n'
checksum = sum(ord(c) for c in startup) % 0x10000

send('!-')
Expand All @@ -32,33 +47,21 @@ def send(line_: str) -> None:
send('!.')
send('core.restart()')

# Wait for "Ready." message with a deadline depending on the number of expanders
timeout = 3.0 + 3.0 * startup.count('Expander')
deadline = time.time() + timeout
while time.time() < deadline:
try:
line = port.read_until(b'\r\n').decode().rstrip()
except UnicodeDecodeError:
continue
if line == 'Ready.':
print('ESP32 booted and sent "Ready."')
for line in read(timeout=3.0 + 0.5 * len(startup.splitlines()) + 3.0 * startup.count('Expander')):
if line.endswith('Ready.'):
print('ESP booted and sent "Ready."')
break
else:
raise TimeoutError('Timeout waiting for device to restart!')

# Immediately check checksum after ready
send('core.startup_checksum()')
deadline = time.time() + 3.0
while time.time() < deadline:
try:
line = port.read_until(b'\r\n').decode().rstrip()
except UnicodeDecodeError:
continue
if line.startswith('checksum: '):
if int(line.split()[1].split('@')[0], 16) == checksum:
for line in read(timeout=5.0):
words = line.split()
if words[-2] == 'checksum:':
received = int(words[-1], 16)
if received == checksum:
print('Checksum matches.')
break
else:
raise ValueError('Checksum mismatch!')
raise ValueError(f'Checksum mismatch! Expected {checksum:#06x}, got {received:#06x}.')
else:
raise TimeoutError('Timeout waiting for checksum!')
9 changes: 9 additions & 0 deletions docs/module_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ It is automatically created right after the boot sequence.
| `core.get_pin_status(pin)` | Print the status of the chosen pin | `int` |
| `core.set_pin_level(pin, value)` | Turns the pin into an output and sets its level | `int`, `int` |
| `core.get_pin_strapping(pin)` | Print value of the pin from the strapping register | `int` |
| `core.forget_serial_bus()` | Remove the saved SerialBus configuration from NVS | |
| `core.pause_broadcasts()` | Pause property broadcasts (all modules) | |
| `core.resume_broadcasts()` | Resume property broadcasts | |

Expand Down Expand Up @@ -84,6 +85,14 @@ The serial bus module lets multiple ESP32s share a UART link with a coordinator
| `bus.send(receiver, payload)` | Send a single line of text to a peer `receiver` (0-255) | `int`, `str` |
| `bus.make_coordinator(peer_ids...)` | Set the list of peer IDs, making this node the coordinator | `int`s |

**Bus Backup:**
When a SerialBus is created, its configuration (pins, baud rate, UART number, node ID) is automatically saved to non-volatile storage.
If multiple SerialBus modules exist, only the first one is backed up.
On boot, if the startup script does not create a SerialBus but a backup config exists,
Lizard removes all existing Serial modules and recreates the SerialBus from the saved config.
This keeps the node reachable over the bus even if a broken script is deployed, avoiding the need for physical USB access.
To remove the saved configuration, call `core.forget_serial_bus()`.

**Firmware Updates:**
Peers on the serial bus can be updated remotely via the coordinator.
Use the `otb_update.py` tool to push new firmware to any peer node.
Expand Down
14 changes: 12 additions & 2 deletions main/compilation/expression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,18 @@ int Expression::print_to_buffer(char *buffer, size_t buffer_len) const {
return csprintf(buffer, buffer_len, "%lld", this->evaluate_integer());
case number:
return csprintf(buffer, buffer_len, "%f", this->evaluate_number());
case string:
return csprintf(buffer, buffer_len, "\"%s\"", this->evaluate_string().c_str());
case string: {
char escaped[buffer_len];
int pos = 0;
for (char c : this->evaluate_string()) {
if (c == '"' || c == '\\') {
escaped[pos++] = '\\';
}
escaped[pos++] = c;
}
escaped[pos] = '\0';
return csprintf(buffer, buffer_len, "\"%s\"", escaped);
}
case identifier:
return csprintf(buffer, buffer_len, "%s", this->evaluate_identifier().c_str());
default:
Expand Down
4 changes: 4 additions & 0 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "rom/gpio.h"
#include "rom/uart.h"
#include "storage.h"
#include "utils/bus_backup.h"
#include "utils/tictoc.h"
#include "utils/timing.h"
#include "utils/uart.h"
Expand Down Expand Up @@ -419,6 +420,9 @@ void app_main() {
echo("error while loading startup script: %s", e.what());
}

bus_backup::save_if_present();
bus_backup::restore_if_needed();

printf("\nReady.\n");

while (true) {
Expand Down
4 changes: 4 additions & 0 deletions main/modules/core.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "core.h"
#include "../global.h"
#include "../storage.h"
#include "../utils/bus_backup.h"
#include "../utils/string_utils.h"
#include "../utils/timing.h"
#include "../utils/uart.h"
Expand Down Expand Up @@ -154,6 +155,9 @@ void Core::call(const std::string method_name, const std::vector<ConstExpression
echo("Not a strapping pin");
break;
}
} else if (method_name == "forget_serial_bus") {
Module::expect(arguments, 0);
bus_backup::remove();
} else if (method_name == "pause_broadcasts") {
Module::expect(arguments, 0);
Module::broadcast_paused = true;
Expand Down
4 changes: 4 additions & 0 deletions main/modules/serial.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Serial::Serial(const std::string name,
this->initialize_uart();
}

Serial::~Serial() {
this->deinstall();
}

void Serial::initialize_uart() const {
const uart_config_t uart_config = {
.baud_rate = baud_rate,
Expand Down
1 change: 1 addition & 0 deletions main/modules/serial.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Serial : public Module {

Serial(const std::string name,
const gpio_num_t rx_pin, const gpio_num_t tx_pin, const long baud_rate, const uart_port_t uart_num);
~Serial();
void initialize_uart() const;
void enable_line_detection() const;
void deinstall() const;
Expand Down
7 changes: 7 additions & 0 deletions main/modules/serial_bus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ void SerialBus::call(const std::string method_name, const std::vector<ConstExpre
// respond to poll
if (bus->requesting_node) {
try {
// if this is the first response, send "Ready." (will be simplified in the future when broadcasts are implemented)
if (bus->ready_pending) {
char payload[PAYLOAD_CAPACITY];
const int len = std::snprintf(payload, sizeof(payload), "%sReady.", ECHO_CMD);
bus->send_message(bus->requesting_node, payload, len);
bus->ready_pending = false;
}
bus->send_outgoing_queue();
bus->send_message(bus->requesting_node, DONE_CMD, sizeof(DONE_CMD) - 1);
} catch (const std::exception &e) {
Expand Down
9 changes: 7 additions & 2 deletions main/modules/serial_bus.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
#include <cstdint>
#include <vector>

class SerialBus;
using SerialBus_ptr = std::shared_ptr<SerialBus>;

class SerialBus : public Module {
public:
static constexpr size_t PAYLOAD_CAPACITY = 256;

const ConstSerial_ptr serial;
const uint8_t node_id;

SerialBus(const std::string &name, const ConstSerial_ptr serial, const uint8_t node_id);

void step() override;
Expand All @@ -32,8 +38,6 @@ class SerialBus : public Module {
char payload[PAYLOAD_CAPACITY];
};

const ConstSerial_ptr serial;
const uint8_t node_id;
std::vector<uint8_t> peer_ids;

QueueHandle_t outbound_queue = nullptr;
Expand All @@ -43,6 +47,7 @@ class SerialBus : public Module {
unsigned long poll_start_millis = 0;
size_t poll_index = 0;
uint8_t requesting_node = 0;
bool ready_pending = true;
uint8_t echo_target_id = 0; // node ID that should receive relayed echo output (0 = no relay)
otb::BusOtbSession otb_session;

Expand Down
94 changes: 94 additions & 0 deletions main/utils/bus_backup.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include "bus_backup.h"

#include "../global.h"
#include "../modules/serial.h"
#include "../modules/serial_bus.h"
#include "uart.h"

#include "nvs.h"

#define NVS_NAMESPACE "bus_backup"

namespace bus_backup {

void save_if_present() {
for (const auto &[name, module] : Global::modules) {
if (module->type != serial_bus) {
continue;
}
const auto bus = std::static_pointer_cast<SerialBus>(module);
nvs_handle handle;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
return;
}
bool ok = nvs_set_i8(handle, "tx", bus->serial->tx_pin) == ESP_OK &&
nvs_set_i8(handle, "rx", bus->serial->rx_pin) == ESP_OK &&
nvs_set_i32(handle, "baud", bus->serial->baud_rate) == ESP_OK &&
nvs_set_i8(handle, "uart", bus->serial->uart_num) == ESP_OK &&
nvs_set_i8(handle, "node", bus->node_id) == ESP_OK;
if (!ok) {
echo("error saving bus backup to NVS");
}
nvs_commit(handle);
nvs_close(handle);
return;
}
}

void restore_if_needed() {
for (const auto &[name, module] : Global::modules) {
if (module->type == serial_bus) {
return;
}
}

nvs_handle handle;
if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) {
return;
}
int8_t tx, rx, uart, node;
int32_t baud;
bool ok = nvs_get_i8(handle, "tx", &tx) == ESP_OK &&
nvs_get_i8(handle, "rx", &rx) == ESP_OK &&
nvs_get_i32(handle, "baud", &baud) == ESP_OK &&
nvs_get_i8(handle, "uart", &uart) == ESP_OK &&
nvs_get_i8(handle, "node", &node) == ESP_OK;
nvs_close(handle);
if (!ok) {
return;
}

echo("restoring serial bus from backup");
try {
std::vector<std::string> serials_to_remove;
for (const auto &[name, module] : Global::modules) {
if (module->type == serial) {
serials_to_remove.push_back(name);
}
}
for (const std::string &name : serials_to_remove) {
Global::modules.erase(name);
Global::variables.erase(name);
}
Serial_ptr backup_serial = std::make_shared<Serial>(
"_backup_serial", static_cast<gpio_num_t>(rx), static_cast<gpio_num_t>(tx),
baud, static_cast<uart_port_t>(uart));
Global::add_module("_backup_serial", backup_serial);
SerialBus_ptr bus = std::make_shared<SerialBus>("_backup_bus", backup_serial, node);
Global::add_module("_backup_bus", bus);
} catch (const std::runtime_error &e) {
echo("bus backup error: %s", e.what());
}
}

void remove() {
nvs_handle handle;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
return;
}
nvs_erase_all(handle);
nvs_commit(handle);
nvs_close(handle);
}

} // namespace bus_backup
9 changes: 9 additions & 0 deletions main/utils/bus_backup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

namespace bus_backup {

void save_if_present();
void restore_if_needed();
void remove();

} // namespace bus_backup