VE6SLP / Projects

Build a command. Talk to your bot.

Write a greeting in Lua, check it on your computer, then add it to a node without flashing firmware. Continue with private notes, timers or a C/Rust command when you need them.

Choose the right conversation. Send ! commands to the bot. Send node settings and source commands through authenticated Management, using its admin console or /admin on your trusted LAN. A public channel is not an admin console.

The conversations below are labelled examples, rendered as text rather than screenshots. Names are illustrative; no private keys, passwords or real chat history are shown. Receipt ACKs do not mean a command succeeded.

How to add your first Lua command

Lua runs on Aspen, Pine and the native worker used by Birch and Willow. Start on your computer; you do not need a radio for the first two steps.

  1. Save a small command

    Create hello.lua in your source checkout:

    function hello(name)
      return "Hello " .. name
    end
    command("hello", "name:string:32", "Greet a name",
            "hello", "public", "!hello mesh")

    name:string:32 allows a token up to 32 bytes. The declaration supplies help and checks the arguments. Return a string for one reply; do not also send that reply with reply().

  2. Check the replies locally

    From the repository root, run the supplied copy of this command:

    make -s -C firmware/esp32 bot-local \
      SOURCE=firmware/runtime/plugins/template.lua \
      SCENARIO=firmware/runtime/plugins/examples/hello.scenario.json

    Expected: PASS 4 native replay assertions. This checks valid, missing, quoted and extra arguments. The first run builds the runner and fetches dependencies; it never connects to a radio or sends RF.

    Test your own file and understand the output
    printf '!hello mesh\n' | make -s -C firmware/esp32 bot-local \
      SOURCE=hello.lua

    The last JSON line contains "text":"Hello mesh". A scenario checks replies automatically and exits nonzero on a mismatch. Runner dependencies, scenarios and replay commands.

  3. Install through Management

    Want to install over LoRa? Follow the complete RF file-upload walkthrough: choose the Management identity, prepare a separate KISS gateway, export the current set and add hello.lua. It includes native companion-console login and durable chunk/activation examples. The LAN alternative is below.

    Check the node's source status and export your existing files first. On Aspen, sign in to http://NODE/admin to inspect the selected Lua program. For adding one file while keeping the others, use a named source operation:

    python3 tools/hardware/admin.py \
      --web http://NODE --password-file /private/node-password \
      source-install greeting hello.lua
    python3 tools/hardware/admin.py \
      --web http://NODE --password-file /private/node-password \
      status

    Replace NODE and the password-file path with your own. Wait until status shows the selected source is active. This changes the file named greeting, not the bot's identity or other named files.

    Browser uploads, other platforms and replacement limits

    A whole-program browser upload or package-install replaces that runtime's entire command set. Export it first and include the commands you want to keep. Named files share one Lua environment, source limit and command registry; a duplicate name rejects the candidate.

    Native hosts can use their private owner socket; Pine supports authenticated RF source administration. Transport options and named file recovery.

    Do not provision or erase a filesystem to install a command. Durable source returns after restart; replacement cancels that runtime's pending work but retains scoped data.

  4. Try it in a bot DM

    In your MeshCore app, advertise your companion so the bot can learn its contact, then open the bot's private conversation.

    Example · Private DM to Aspen-Bot · after installation
    1. You!hello mesh
    2. Aspen-BotHello mesh
    3. You!help hello

    The bot returns generated help for hello. Its exact formatting depends on the installed runtime.

    A local reply through Aspen's own Base is a local reflection. To check a conversation over RF, use a separate companion radio on the same complete radio profile.

How to call a command in a channel

If the operator has enabled the bot in your channel and saved the aspen-bot alias, address it explicitly:

Example · #lab · addressed-only command policy
  1. Alice!@aspen-bot hello mesh
  2. Aspen-BotHello mesh
  3. Alice!hello mesh

The unaddressed command stays silent with an addressed-only policy. A companion's !@[Aspen-Bot] hello mesh mention uses the same saved alias.

Operator: prepare a new addressed-only channel safely

These are authenticated Management commands, not channel posts. Inspect memberships first and select an unused slot; 2 and #lab are examples. Joining resets the slot to a permissive default, so disable and restart the bot before creating it:

bot off
apply

Reconnect to Management. Confirm bot status reports disabled, then configure the unused slot:

bot membership 2 #lab
bot access 2 default 12
bot aliases aspen,aspen-bot,a
bot membership 2
bot access 2 list 0
bot aliases
bot on
apply

Verify the saved membership and default 12 before enabling. Alias changes replace the list: include existing names you want to keep. The last restart activates the saved bot selection. An existing addressed-only channel needs neither rejoining nor restarting just to try a command.

Example · Authenticated Management console · checking saved aliases
  1. Operatorbot aliases
  2. ManagementAliases: aspen,aspen-bot,a; live=applied

Channel keys and nicknames do not grant owner privileges or private DM storage. Endpoints and permissions.

How to keep private data through upgrades

Start with the complete notes-v2.lua example. It uses authenticated DM callers as separate storage owners and writes a rollback-compatible note format.

make -s -C firmware/esp32 bot-local \
  SOURCE=firmware/runtime/plugins/examples/notes-v2.lua \
  SCENARIO=firmware/runtime/plugins/examples/notes.scenario.json

Expected: eight assertions, including two private users and switching between code versions.

Example · Separate private DMs · notes-v2 installed
  1. Alice → Bot!note-save lunch
  2. Bot → AliceSaved
  3. Bob → Bot!note-read
  4. Bot → BobNo note
  5. Alice → Bot!note-read
  6. Bot → Alicelunch
Advanced: transactions, schemas and uncertain writes

The example migrates old data with one transaction, checks the schema and writes both version-1 and version-2 fields. Older code can still read notes after a code rollback. Use the complete migration function, not an unchecked write fragment.

conflict means nothing changed. For unknown, read the affected keys before deciding to write again. Check rf_safe before returning arbitrary stored bytes; these example values enter through bounded printable arguments. Never turn storage failure into a success reply.

Annotated transaction recipe · Storage bounds and authority.

How to wait without blocking other commands

The lab.lua example sets a timer and waits cooperatively. Trusted time is required:

function alarm(name, seconds)
  timer.set(name, seconds)
  local state = timer.wait(name)
  return "Timer " .. name .. " " .. state.state
end
command("alarm", "name:string:32,seconds:int:1:60",
        "Wait on a durable timer", "alarm", "dm", "!alarm tea 2")

The waiting job yields so other work can run within the node's job limits. A restart retains the timer but loses the old coroutine. Reconnect and use the example's !timer-state tea to inspect it.

Test restarts or deliver a later reminder

Use @clock, @send, @advance and @restart in the local runner. For a message that should be delivered after restart, choose the reminder API with its owner grant, fresh trusted time and direct route. A timer alone is not a guaranteed future RF reply.

Timer recipe · Async replay guide.

How to add a C or Rust command

Use Wasm on an ESP32-S3 with PSRAM or a Wasm-enabled native Linux worker. Pine does not run Wasm. In Management, check source api runtimes before compiling or uploading.

  1. Build a supplied example

    Set up the pinned Clang/Rust tools from the Wasm build guide, then run:

    make -C firmware/esp32 bot-wasm-examples
    python3 tools/hardware/admin.py package create \
      .tmp/wasm-examples/c-notes.wasm .tmp/wasm-notes.package \
      --name wasm-notes --version 1.0.0 --capability cmdmeta --capability kv
    python3 tools/hardware/admin.py package inspect .tmp/wasm-notes.package

    The C example uses the supplied meshcore_v1 SDK. It needs no WASI, filesystem access or shell.

  2. Install only the Wasm command set

    python3 tools/hardware/admin.py \
      --web http://NODE --password-file /private/node-password \
      package-install .tmp/wasm-notes.package
    python3 tools/hardware/admin.py \
      --web http://NODE --password-file /private/node-password \
      --runtime wasm status

    The package identifies its runtime automatically. Use --runtime wasm for status, readback, rollback or removal; lifecycle commands default to Lua. Wasm installation cancels Wasm jobs and replaces that set, while Lua keeps running and durable data remains stored.

  3. Try a private note

    Example · Bot DM · C notes package active alongside Lua
    1. You!wnote lunch
    2. BotNote saved
    3. You!wnote
    4. Botlunch
Advanced: async C/Rust I/O and approved services

In notes.c, mc_start submits a scoped storage operation and returns MC_PENDING. Native completion calls mc_resume, which checks the result and replies. Error and success paths stay separate.

The SDK also has arithmetic and configured-RPC examples in C and Rust. RPC needs an operator-approved endpoint, trusted time and the home grant; guest code receives no private keys, credentials or arbitrary URL access.

Wasm recipe · SDK, memory and execution limits.

If something goes wrong

The bot does not answer
Check the destination, companion advert, matching radio profile and command policy. In a channel, check the saved alias and address the bot. A receipt ACK is not an execution result.
The upload was accepted, but the command is missing
Read source status in native Management and wait for activation. Inspect any validation error. Queue acceptance is not installation completion.
A stored note or network operation reports an unknown outcome
Read its state before another explicit attempt. Do not automatically resend a command that may already have changed data or sent a packet.
The new source fails after a restart
Use native Management source status, then retry, rollback or explicitly remove that selection. Wasm uses source wasm status and its own lifecycle. Keep backups and known-good source files.

Back to command and API overview · Find the Management endpoint · All source recipes.