Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
101 changes: 63 additions & 38 deletions configure.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
#!/usr/bin/env python3
import sys
import argparse
import time
from pathlib import Path

import serial

if len(sys.argv) != 3:
print(f'Usage: {sys.argv[0]} <config_file> <device_path>')
sys.exit()

txt_path, usb_path = sys.argv[1:]
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')
args = parser.parse_args()


def send(line_: str) -> None:
Expand All @@ -20,45 +21,69 @@ def send(line_: str) -> None:
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'
checksum = sum(ord(c) for c in startup) % 0x10000
def configure(payload: str) -> None:
if args.serial_bus:
send(f"bus.send({args.serial_bus}, '{payload}')")
else:
send(payload)

send('!-')
for line in startup.splitlines():
send(f'!+{line}')
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')
def read_lines(timeout: float):
deadline = time.time() + timeout
while time.time() < deadline:
try:
line = port.read_until(b'\r\n').decode().rstrip()
yield port.read_until(b'\r\n').decode().rstrip()
except UnicodeDecodeError:
continue
if line == 'Ready.':
print('ESP32 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:
print('Checksum matches.')

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

configure('!-')
for line in startup.splitlines():
configure(f'!+{line}')
configure('!.')
configure('core.restart()')

reboot_timeout = 3.0 + 3.0 * startup.count('Expander')

if args.serial_bus:
target = f'node {args.serial_bus}'
prefix = f'bus[{args.serial_bus}]: checksum: '
print(f'Waiting {reboot_timeout:.1f}s for {target} to reboot...')
time.sleep(reboot_timeout)

configure('core.startup_checksum()')
for line in read_lines(5.0):
if len(line) > 3 and line[-3] == '@':
line = line[:-3]
if prefix in line:
received = int(line[line.index(prefix) + len(prefix):], 16)
if received == checksum:
print(f'{target} checksum matches.')
break
raise ValueError(f'{target} checksum mismatch! expected {checksum:#06x}, got {received:#06x}')
else:
raise TimeoutError(f'Timeout waiting for {target} checksum!')
else:
for line in read_lines(reboot_timeout):
if line == 'Ready.':
print('ESP32 booted and sent "Ready."')
break
else:
else:
raise TimeoutError('Timeout waiting for device to restart!')

send('core.startup_checksum()')
for line in read_lines(3.0):
if line.startswith('checksum: '):
received = int(line.split()[1].split('@')[0], 16)
if received == checksum:
print('Checksum matches.')
break
raise ValueError('Checksum mismatch!')
else:
raise TimeoutError('Timeout waiting for checksum!')
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 @@ -36,6 +36,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.delete_bus_backup()` | Clear the saved serial bus backup config from NVS | |

The output `format` is a string with multiple space-separated elements of the pattern `<module>.<property>[:<precision>]` or `<variable>[:<precision>]`.
The `precision` is an optional integer specifying the number of decimal places for a floating point number.
Expand Down Expand Up @@ -90,6 +91,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.
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 clear the saved backup config, call `core.delete_bus_backup()`.

## Input

The input module is associated with a digital input pin that is be connected to a pushbutton, sensor or other input signal.
Expand Down
4 changes: 3 additions & 1 deletion main/compilation/expression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ int Expression::print_to_buffer(char *buffer, size_t buffer_len) const {
case number:
return csprintf(buffer, buffer_len, "%f", this->evaluate_number());
case string:
return csprintf(buffer, buffer_len, "\"%s\"", this->evaluate_string().c_str());
return csprintf(buffer, buffer_len,
this->evaluate_string().find('"') != std::string::npos ? "'%s'" : "\"%s\"",
this->evaluate_string().c_str());
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/ota.h"
#include "utils/tictoc.h"
#include "utils/timing.h"
Expand Down Expand Up @@ -420,6 +421,9 @@ void app_main() {
echo("error while loading startup script: %s", e.what());
}

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

try {
xTaskCreate(&ota::verify_task, "ota_verify_task", 8192, NULL, 5, NULL);
} catch (const std::runtime_error &e) {
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/ota.h"
#include "../utils/string_utils.h"
#include "../utils/timing.h"
Expand Down Expand Up @@ -163,6 +164,9 @@ void Core::call(const std::string method_name, const std::vector<ConstExpression
echo("Not a strapping pin");
break;
}
} else if (method_name == "delete_bus_backup") {
Module::expect(arguments, 0);
bus_backup::remove();
} else {
Module::call(method_name, arguments);
}
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
5 changes: 3 additions & 2 deletions main/modules/serial_bus.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ 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 @@ -31,8 +34,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 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);
const auto 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