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.
Save a small command
Create
hello.luain your source checkout:function hello(name) return "Hello " .. name end command("hello", "name:string:32", "Greet a name", "hello", "public", "!hello mesh")name:string:32allows 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 withreply().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.jsonExpected:
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.luaThe last JSON line contains
"text":"Hello mesh". A scenario checks replies automatically and exits nonzero on a mismatch. Runner dependencies, scenarios and replay commands.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/adminto 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 \ statusReplace
NODEand the password-file path with your own. Wait until status shows the selected source is active. This changes the file namedgreeting, not the bot's identity or other named files.Browser uploads, other platforms and replacement limits
A whole-program browser upload or
package-installreplaces 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.
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 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:
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.
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.
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.
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.
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.packageThe C example uses the supplied
meshcore_v1SDK. It needs no WASI, filesystem access or shell.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 statusThe package identifies its runtime automatically. Use
--runtime wasmfor 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.Try a private note
Example · Bot DM · C notes package active alongside Lua
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.
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 statusin 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 usessource wasm statusand its own lifecycle. Keep backups and known-good source files.
Back to command and API overview · Find the Management endpoint · All source recipes.