inanimate/resident

0.5.0

Latest
uploaded 19 hours ago
Sandbox with hardware IO and hot reload for ESP32 devices

readme

# Resident

Sandbox with hardware IO and hot reload for ESP32 devices.

Resident provides a sandboxed Lua runtime that can be loaded with new code over the network at any time. Hardware peripherals are exposed to Lua through a driver interface, so apps can draw to displays, read sensors, and control outputs without touching C++.

## What Resident includes

1. The Resident firmware library for a sandbox on ESP32 devices and custom hardware integration, with optional managed connectivity to load sandbox apps remotely.
2. A default websocket backend at `resident.inanimate.tech/devices/<deviceId>` to relay apps and events during development.
3. Agent skills to create, validate and push sandbox apps. [Install the Resident skills plug-in](tools/agent-plugin/README.md).

Point your agent at [docs/start-building.md](docs/start-building.md) to add the Resident sandbox to your hardware.

## Examples

Working PlatformIO projects for specific boards live under [examples/](examples/) — currently the M5StickC Plus2 and the Adafruit ESP32-S2 TFT Feather. Each is buildable as-is; use them as templates for bringing up your own hardware.

### Quick start

`Resident::Sandbox` composes [Courier](https://github.com/inanimate-tech/courier) for connectivity with the Lua runtime. It handles WiFi, WebSocket transport, and message routing automatically — populate `cfg.network` and the sandbox connects on `setup()`.

```cpp
#include <Resident.h>
#include "MyDisplayDriver.h"
#include "MyButtonDriver.h"

MyDisplayDriver display;
MyButtonDriver button{...};   // however your driver takes config

Resident::SandboxConfig makeConfig() {
    Resident::SandboxConfig cfg;
    cfg.deviceType    = "demo";
    cfg.statusDisplay = &display;
    cfg.extensions    = {&display, &button};

    // Courier::Config has a constructor with default args, so designated
    // initializers (`Courier::Config{ .host = ... }`) don't compile under
    // strict ESP-IDF builds. Use direct field assignment.
    Courier::Config courier;
    courier.host = "resident.inanimate.tech";
    cfg.network  = courier;

    return cfg;
}

Resident::Sandbox sandbox{makeConfig()};

void setup() {
    // Optional: override the default WS path on the canonical relay.
    sandbox.onTransportsWillConnect([]() {
        String path = String("/devices/") + sandbox.getDeviceId();
        sandbox.ws().setEndpoint("resident.inanimate.tech", 443, path.c_str());
    });

    sandbox.setup();
}

void loop() {
    sandbox.loop();
}
```

The device connects to WiFi (via a WiFiManager captive portal), opens a WebSocket to your server, and accepts Lua apps and shader expressions as JSON messages.

> Omit `cfg.network` and the sandbox runs standalone with no WiFi pulled in — `sandbox.loop()` ticks Lua at 10 FPS unconditionally, and `isConnected()` returns `false`.

Register reactive callbacks before `setup()` to react to lifecycle events:

```cpp
sandbox.onConnected([]() {
    // load a bootstrap Lua app once the WS is up
});

sandbox.onMessage([](const char* transport, const char* type, JsonDocument& doc) {
    // fires only for non-reserved types — Resident handles app/shader/app_event
});
```

## Writing a Driver

Drivers expose hardware to Lua via a builder API:

```cpp
#include <ResidentDriver.h>
#include <ResidentLuaModule.h>
#include <M5Unified.h>

extern "C" {
  #include "lua/lua.h"
  #include "lua/lauxlib.h"
}

class IMUDriver : public Resident::Driver {
public:
    const char* name() const override { return "imu"; }
    void registerModule(Resident::LuaModule& m) override {
        m.method<IMUDriver, &IMUDriver::accel>("accel");
    }

    int accel(lua_State* L) {
        M5.Imu.update();
        auto d = M5.Imu.getImuData();
        lua_pushnumber(L, d.accel.x);
        lua_pushnumber(L, d.accel.y);
        lua_pushnumber(L, d.accel.z);
        return 3;
    }
};
```

Then in Lua:

```lua
function on_tick(ctx, dt_ms)
    local ax, ay, az = imu.accel()
    -- use acceleration data
end
```

For Lua-only extensions that don't expose hardware or emit events, extend `Resident::Extension` directly instead of `Resident::Driver` — the same `registerModule(LuaModule&)` and lifecycle hooks apply.

### Driver lifecycle

- `begin()` — called once by `Sandbox::setup()` in registration order.
  Hardware init goes here. Idempotent: a manual early call is safe (the
  Sandbox's call becomes a no-op).
- `update()` — called every iteration of `Sandbox::loop()`. Use for
  per-tick driver work like polling and debouncing. Runs at full main-loop
  rate, distinct from Lua's 10 FPS `on_tick`.
- `registerModule(LuaModule& m)` — called once by `Sandbox::setup()`
  to register the driver's Lua-visible global. Use the builder's
  `method<>`, `staticMethod`, and `constant` overloads.
- `onAppReset()` — called when a new app is loaded (before compilation).
- `onAppRunning(bool)` — called when an app starts or stops running.

## Message Protocol

Resident routes three JSON message types internally — your `sandbox.onMessage(cb)` callback only fires for *other* types:

```json
{ "type": "app", "code": "function on_tick(ctx, dt_ms) ... end" }
{ "type": "shader", "expr": "rgb(sin(time_ms/1000)*0.5+0.5, 0, 0)" }
{ "type": "app_event", "name": "button_press", "data": { "id": 1 } }
```

Register `sandbox.onMessage(cb)` to handle custom types. No super-call is needed — the reserved types never reach your callback.

### Sandbox lifecycle

- `init(ctx)` — called once after compilation
- `on_tick(ctx, dt_ms)` — called at 10 FPS with elapsed time
- `on_event(ctx, event)` — called for app_event messages and driver events

The `ctx` table contains: `time_ms`, `trigger_count`, `utc_h`, `utc_m`, `localtime_h`, `localtime_m`.

`localtime_h` / `localtime_m` return local time when a timezone has been set on the sandbox via `Sandbox::setTimezone(ianaZone)` and ezTime recognised the zone; otherwise they equal `utc_h` / `utc_m`.

### Shader expressions

Shader messages are converted to Lua via a template function you provide. The expression has access to `time_ms`, `trigger_count`, and time variables. Built-in helpers: `rgb(r,g,b)`, `fract(x)`, `beat(bpm)`, `noise2d(x,y)`.

### Timezone

`Sandbox::setTimezone(const char* ianaZone)` — set the sandbox's local timezone for `ctx.localtime_h/m` and the `time.*` Lua bindings. Pass an IANA zone string (e.g. `"Europe/London"`). ezTime performs a UDP lookup to `timezoned.rop.nl` on first sight of a zone and caches the POSIX string in EEPROM. On failure (null / empty / unrecognised zone), the sandbox falls back to UTC.

`Sandbox::hasTimezone() const` — returns `true` after a successful `setTimezone`. Exposed to Lua as `time.has_timezone()`. When false, `time.hour()` / `time.minute()` / `time.second()` return UTC.

## Building

### PlatformIO

```ini
[env:dev]
platform = espressif32@6.12.0
board = esp32-s3-devkitc-1
framework = arduino
lib_deps =
    https://github.com/inanimate-tech/resident.git
    https://github.com/inanimate-tech/courier.git
    tzapu/WiFiManager@^2.0.17
    bblanchon/ArduinoJson@^7.4.2
    ropg/ezTime@^0.8.3
    fischer-simon/Esp32Lua@^5.4.7
```

### ESP-IDF (Arduino as component)

Add to your `CMakeLists.txt`:

```cmake
set(EXTRA_COMPONENT_DIRS ../vendor)
```

And in `idf_component.yml`:

```yaml
dependencies:
  inanimate/resident:
    version: "^0.1.0"
```

## License

[MIT](LICENSE)

Links

Supports all targets

License: MIT

To add this component to your project, run:

idf.py add-dependency "inanimate/resident^0.5.0"

download archive

Stats

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

Badge

inanimate/resident version: 0.5.0
|