audio bus crypto display ez global gps keyboard mesh net radio sprite storage synth system wifi

LoRa mesh networking with MeshCore protocol

Provides access to the MeshCore mesh network for peer-to-peer communication. Supports node discovery, channel messaging, direct messages, and identity management with Ed25519 cryptography. The mesh runs in the background, automatically handling packet routing and retransmission. Subscribe to bus messages (channel/message, message/received) for incoming data.

Functions

build_packet() → string|nil Build a raw mesh packet for transmission
ez.mesh.build_packet(route_type, payload_type, payload, path) -> string|nil
Constructs a serialized MeshCore packet from components. The packet includes header, path, and payload. Use ez.mesh.ROUTE and ez.mesh.PAYLOAD constants.
ParameterDescription
route_typeRoute type constant (FLOOD=1, DIRECT=2)
payload_typePayload type constant (ADVERT=4, GRP_TXT=5, etc.)
payloadBinary string payload (max 184 bytes)
pathOptional binary string of path hashes (default: empty)
Returns: Serialized packet as binary string, or nil on error
local pkt = ez.mesh.build_packet(
ez.mesh.ROUTE.FLOOD,
ez.mesh.PAYLOAD.GRP_TXT,
encrypted_message
)
ez.mesh.queue_send(pkt)
calc_shared_secret() → string|nil Calculate ECDH shared secret with another node
ez.mesh.calc_shared_secret(other_pub_key) -> string|nil
Performs X25519 key exchange to derive a shared secret that only this node and the other party can compute. Used for end-to-end encryption of direct messages between two nodes.
ParameterDescription
other_pub_key32-byte Ed25519 public key of the other party
Returns: 32-byte shared secret as binary string, or nil on error (with error message as second return)
local secret = ez.mesh.calc_shared_secret(other_node.pub_key)
local key = ez.crypto.sha256(secret):sub(1, 16)  -- Derive 128-bit key
clear_packet_queue() Clear all packets from the queue
ez.mesh.clear_packet_queue()
Removes all packets from the receive queue without processing them. Use when switching contexts or resetting the receiver state.
ez.mesh.clear_packet_queue()  -- Discard unprocessed packets
clear_tx_queue() Clear all packets from transmit queue
ez.mesh.clear_tx_queue()
Removes all pending packets from the transmit queue. Use when you need to cancel queued transmissions, such as when changing channels.
ez.mesh.clear_tx_queue()  -- Cancel all pending transmissions
ed25519_sign() → signature Sign data with this node's private key
ez.mesh.ed25519_sign(data) -> signature
Creates an Ed25519 signature of the provided data using this node's private key. The signature can be verified by anyone with the public key. Used for message authentication and non-repudiation.
ParameterDescription
dataBinary string to sign
Returns: 64-byte Ed25519 signature as binary string, or nil on error
local message = "Hello, mesh!"
local sig = ez.mesh.ed25519_sign(message)
local pubkey = ez.mesh.get_public_key()
assert(ez.mesh.ed25519_verify(message, sig, pubkey))
ed25519_verify() → boolean Verify an Ed25519 signature
ez.mesh.ed25519_verify(data, signature, pub_key) -> boolean
Verifies that an Ed25519 signature was created by the holder of the corresponding private key. Returns true only if the signature is valid for the given data and public key.
ParameterDescription
dataBinary string that was signed
signature64-byte Ed25519 signature
pub_key32-byte Ed25519 public key
Returns: true if signature is valid
local valid = ez.mesh.ed25519_verify(message, signature, sender_pubkey)
if valid then
print("Message authenticated!")
end
enable_packet_queue() Enable or disable packet queuing for polling
ez.mesh.enable_packet_queue(enabled)
When enabled, incoming packets are added to an internal queue instead of triggering callbacks. Use has_packets() and pop_packet() to process them. This polling-based API is safer than callbacks and works well with coroutines.
ParameterDescription
enabledBoolean to enable/disable
ez.mesh.enable_packet_queue(true)
-- In main loop:
while ez.mesh.has_packets() do
local pkt = ez.mesh.pop_packet()
process_packet(pkt)
end
get_announce_interval() → integer Get current auto-announce interval
ez.mesh.get_announce_interval() -> integer
Returns the current auto-announce interval setting.
Returns: Interval in milliseconds (0 = disabled)
local interval = ez.mesh.get_announce_interval()
if interval == 0 then
print("Auto-announce disabled")
end
get_node_count() → integer Get number of known nodes
ez.mesh.get_node_count() -> integer
Returns the count of nodes in the discovery table. This is faster than getting the full node list when you only need the count.
Returns: Node count
local count = ez.mesh.get_node_count()
print("Discovered " .. count .. " nodes")
get_node_id() → string Get this node's full ID
ez.mesh.get_node_id() -> string
Returns this node's unique identifier derived from its Ed25519 public key. The ID is the first 6 bytes of the SHA-256 hash of the public key, displayed as hex.
Returns: 12-character hex string (6 bytes), or nil if mesh not initialized
local id = ez.mesh.get_node_id()  -- e.g., "A1B2C3D4E5F6"
get_node_name() → string Get this node's display name
ez.mesh.get_node_name() -> string
Returns the human-readable name configured for this node. This name is broadcast in ADVERT packets and displayed to other mesh users.
Returns: Node name string, or nil if mesh not initialized
local name = ez.mesh.get_node_name()  -- e.g., "Alice's T-Deck"
get_nodes() → table Get list of discovered mesh nodes
ez.mesh.get_nodes() -> table
Returns an array of all nodes discovered via ADVERT packets. Each node table contains routing information, signal quality, and optional location data. Nodes are considered stale after ~5 minutes without a new ADVERT.
Returns: Array of node tables with path_hash, name, rssi, snr, last_seen, hops, role, age_seconds, advert_timestamp, pub_key_hex (optional), has_location, lat, lon (if has_location)
for _, node in ipairs(ez.mesh.get_nodes()) do
print(node.name, node.rssi .. "dBm", node.hops .. " hops")
end
get_path_check() → boolean Get current path check setting
ez.mesh.get_path_check() -> boolean
Returns whether path loop detection is enabled.
Returns: true if path check is enabled
local enabled = ez.mesh.get_path_check()
get_path_hash() → integer Get this node's path hash (first byte of public key)
ez.mesh.get_path_hash() -> integer
Returns this node's path hash, a single byte used in packet routing to identify the node in the path field. This is the first byte of the public key.
Returns: Path hash as integer (0-255)
local hash = ez.mesh.get_path_hash()  -- e.g., 0xA1 = 161
get_public_key() → string Get this node's public key as binary string
ez.mesh.get_public_key() -> string
Returns this node's Ed25519 public key as a raw 32-byte binary string. Used for cryptographic operations like shared secret calculation and signature verification.
Returns: 32-byte Ed25519 public key, or nil if mesh not initialized
local pubkey = ez.mesh.get_public_key()
local shared = ez.mesh.calc_shared_secret(other_pubkey)
get_public_key_hex() → string Get this node's public key as hex string
ez.mesh.get_public_key_hex() -> string
Returns this node's Ed25519 public key as a 64-character hexadecimal string for display or storage in text format.
Returns: 64-character hex string, or nil if mesh not initialized
local hex = ez.mesh.get_public_key_hex()
print("Public key: " .. hex)
get_rx_count() → integer Get total packets received
ez.mesh.get_rx_count() -> integer
Returns the cumulative count of valid packets received by this node since boot. Only counts packets that passed CRC and basic validation.
Returns: Receive count
local rx = ez.mesh.get_rx_count()
get_short_id() → string Get this node's short ID
ez.mesh.get_short_id() -> string
Returns an abbreviated node identifier for display purposes. This is the first 3 bytes of the full node ID, providing a shorter but less unique identifier.
Returns: 6-character hex string (3 bytes), or nil if mesh not initialized
local short = ez.mesh.get_short_id()  -- e.g., "A1B2C3"
get_tx_count() → integer Get total packets transmitted
ez.mesh.get_tx_count() -> integer
Returns the cumulative count of packets transmitted by this node since boot. Includes all packet types (ADVERTs, messages, rebroadcasts).
Returns: Transmit count
local tx = ez.mesh.get_tx_count()
local rx = ez.mesh.get_rx_count()
print("TX: " .. tx .. ", RX: " .. rx)
get_tx_queue_capacity() → integer Get maximum transmit queue capacity
ez.mesh.get_tx_queue_capacity() -> integer
Returns the maximum number of packets the transmit queue can hold.
Returns: Max queue size
local cap = ez.mesh.get_tx_queue_capacity()  -- e.g., 16
get_tx_queue_size() → integer Get number of packets waiting in transmit queue
ez.mesh.get_tx_queue_size() -> integer
Returns the current number of packets in the transmit queue waiting to be sent. Check this to avoid queueing more packets when the queue is full.
Returns: Queue size
local pending = ez.mesh.get_tx_queue_size()
print(pending .. " packets pending")
get_tx_throttle() → integer Get current throttle interval
ez.mesh.get_tx_throttle() -> integer
Returns the current minimum interval between queued packet transmissions.
Returns: Milliseconds between transmissions
local interval = ez.mesh.get_tx_throttle()  -- e.g., 100
has_packets() → boolean Check if packets are available in the queue
ez.mesh.has_packets() -> boolean
Returns true if there are packets waiting in the receive queue. Only works when packet queuing is enabled via enable_packet_queue(true).
Returns: true if one or more packets are queued
if ez.mesh.has_packets() then
local pkt = ez.mesh.pop_packet()
end
is_initialized() → boolean Check if mesh networking is initialized
ez.mesh.is_initialized() -> boolean
Returns whether the MeshCore networking stack has been initialized and is ready to send/receive packets. Check this before calling other mesh functions.
Returns: true if mesh is ready
if ez.mesh.is_initialized() then
local nodes = ez.mesh.get_nodes()
end
is_tx_queue_full() → boolean Check if transmit queue is full
ez.mesh.is_tx_queue_full() -> boolean
Returns true if the transmit queue cannot accept more packets. Check this before calling queue_send() to avoid failed queuing.
Returns: true if queue is full
if not ez.mesh.is_tx_queue_full() then
ez.mesh.queue_send(packet)
end
make_header() → integer Create a packet header byte from components
ez.mesh.make_header(route_type, payload_type, version) -> integer
Constructs a MeshCore packet header byte from route type, payload type, and version fields. Use ez.mesh.ROUTE and ez.mesh.PAYLOAD constants.
ParameterDescription
route_typeRoute type constant
payload_typePayload type constant
versionOptional version (default: 0)
Returns: Header byte as integer
local header = ez.mesh.make_header(ez.mesh.ROUTE.FLOOD, ez.mesh.PAYLOAD.GRP_TXT)
on_group_packet() Set callback for raw group packets (DEPRECATED - use bus.subscribe("mesh/group_packet") instead)
ez.mesh.on_group_packet(callback)
Registers a callback for receiving GRP_TXT and GRP_DATA packets. The callback receives pre-parsed group packet data including the encrypted payload. Also posts to message bus "mesh/group_packet". Pass nil to remove the callback.
ParameterDescription
callbackFunction(packet_table) called with {channel_hash, data, sender_hash, rssi, snr}
ez.mesh.on_group_packet(function(pkt)
local decrypted = Channel.decrypt(pkt.channel_hash, pkt.data)
end)
on_node_discovered() Set callback for node discovery (DEPRECATED - use bus.subscribe("mesh/node_discovered") instead)
ez.mesh.on_node_discovered(callback)
Registers a callback function invoked when a new node is discovered or an existing node sends a fresh ADVERT. Also posts to message bus "mesh/node_discovered". Pass nil to remove the callback.
ParameterDescription
callbackFunction(node_table) called when node discovered
ez.mesh.on_node_discovered(function(node)
print("Found: " .. node.name)
end)
on_packet() Set callback for ALL incoming packets (DEPRECATED - use bus.subscribe("mesh/packet") instead)
ez.mesh.on_packet(callback)
Registers a low-level callback for all received mesh packets before any processing. The callback can return (handled, rebroadcast) to control packet handling. Also posts to message bus "mesh/packet". Pass nil to remove the callback.
ParameterDescription
callbackFunction(packet_table) returning handled, rebroadcast booleans
ez.mesh.on_packet(function(pkt)
if pkt.payload_type == ez.mesh.PAYLOAD.ADVERT then
-- Handle ADVERT packet
end
return false, true  -- not handled, do rebroadcast
end)
packet_count() → integer Get number of packets in queue
ez.mesh.packet_count() -> integer
Returns the current number of packets waiting in the receive queue.
Returns: Number of queued packets
local count = ez.mesh.packet_count()
print(count .. " packets waiting")
parse_header() → route_type, payload_type, version Parse a packet header byte into components
ez.mesh.parse_header(header_byte) -> route_type, payload_type, version
Extracts the route type (bits 0-1), payload type (bits 2-5), and version (bits 6-7) from a MeshCore packet header byte.
ParameterDescription
header_byteSingle byte header value
Returns: route_type, payload_type, version as integers
local route, ptype, ver = ez.mesh.parse_header(pkt.header)
if ptype == ez.mesh.PAYLOAD.ADVERT then
-- Handle ADVERT
end
pop_packet() → table|nil Get and remove the next packet from queue
ez.mesh.pop_packet() -> table|nil
Removes and returns the oldest packet from the receive queue. Returns nil if the queue is empty. The packet table contains route_type, payload_type, version, path, payload, rssi, snr, and timestamp fields.
Returns: Packet table or nil if queue is empty
local pkt = ez.mesh.pop_packet()
if pkt then
print("Received:", pkt.payload_type, pkt.rssi .. "dBm")
end
queue_send() → boolean Queue packet for transmission (throttled, non-blocking)
ez.mesh.queue_send(data) -> boolean
Adds a packet to the transmit queue for throttled sending. Returns immediately without blocking. Packets are sent at the throttle interval to avoid flooding the radio channel. Preferred over send_raw() for most uses.
ParameterDescription
dataBinary string of serialized packet
Returns: true if queued successfully, false if queue full or error
local pkt = ez.mesh.build_packet(ez.mesh.ROUTE.FLOOD, ez.mesh.PAYLOAD.GRP_TXT, data)
if ez.mesh.queue_send(pkt) then
print("Message queued")
end
schedule_rebroadcast() Schedule raw packet data for rebroadcast
ez.mesh.schedule_rebroadcast(data)
Queues a packet for rebroadcast to extend the mesh network range. The packet should be raw bytes from a previously received packet. Used when manually handling packets via on_packet callback.
ParameterDescription
dataBinary string of raw packet bytes
ez.mesh.on_packet(function(pkt)
-- Manually rebroadcast after processing
ez.mesh.schedule_rebroadcast(raw_bytes)
end)
send_announce() → boolean Broadcast node announcement
ez.mesh.send_announce() -> boolean
Sends an ADVERT packet containing this node's identity, name, role, and optional location. Other nodes use ADVERTs for discovery and routing. Announcements are flood-routed to reach all nodes in the mesh.
Returns: true if sent successfully
ez.mesh.send_announce()  -- Announce presence to mesh
send_group_packet() → boolean Send raw encrypted group packet
ez.mesh.send_group_packet(channel_hash, encrypted_data) -> boolean
Sends a GRP_TXT packet to a channel. The data must already be encrypted with the channel key (use ez.crypto functions). The packet is flood-routed to reach all nodes subscribed to the channel.
ParameterDescription
channel_hashSingle byte channel identifier (first byte of channel key hash)
encrypted_dataPre-encrypted payload (MAC + ciphertext)
Returns: true if sent successfully
local encrypted = Channel.encrypt("Hello everyone!")
ez.mesh.send_group_packet(channel.hash, encrypted)
send_raw() → boolean Send raw packet data directly via radio (bypasses queue, immediate)
ez.mesh.send_raw(data) -> boolean
Transmits a packet immediately without queueing or throttling. Blocks until transmission completes. Use queue_send() for non-blocking throttled transmission in most cases.
ParameterDescription
dataBinary string of serialized packet
Returns: true if sent successfully
local pkt = ez.mesh.build_packet(ez.mesh.ROUTE.FLOOD, ez.mesh.PAYLOAD.GRP_TXT, data)
ez.mesh.send_raw(pkt)  -- Immediate transmission
set_announce_interval() Set auto-announce interval in milliseconds (0 = disabled)
ez.mesh.set_announce_interval(ms)
Configures automatic ADVERT transmission at a regular interval. Set to 0 to disable auto-announce and rely on manual send_announce() calls. Typical values are 60000-300000 (1-5 minutes).
ParameterDescription
msInteger - interval in milliseconds (0 to disable)
ez.mesh.set_announce_interval(120000)  -- Announce every 2 minutes
ez.mesh.set_announce_interval(0)       -- Disable auto-announce
set_node_name() → boolean Set this node's display name
ez.mesh.set_node_name(name) -> boolean
Sets the human-readable name for this node. The name is stored in NVS and broadcast to other nodes in ADVERT packets. Limited to 32 characters.
ParameterDescription
nameNew node name
Returns: true if successful
ez.mesh.set_node_name("Bob's T-Deck")
set_path_check() Enable or disable path check for flood routing
ez.mesh.set_path_check(enabled)
Controls whether the mesh rejects packets that already contain this node's path hash (indicating the packet has already passed through). Enabled by default to prevent routing loops. Disable only for debugging.
ParameterDescription
enabledBoolean - when true, packets with our hash in path are skipped
ez.mesh.set_path_check(false)  -- Disable for debugging
set_tx_throttle() Set minimum interval between transmissions
ez.mesh.set_tx_throttle(ms)
Sets the minimum time between queued packet transmissions. Higher values reduce channel congestion but increase latency. Default is 100ms.
ParameterDescription
msMilliseconds between transmissions (default 100)
ez.mesh.set_tx_throttle(200)  -- Slower, less channel usage
ez.mesh.set_tx_throttle(50)   -- Faster, more responsive
update() Process incoming mesh packets
ez.mesh.update()
Processes any pending radio packets, updates node discovery state, triggers callbacks for received messages, and handles packet rebroadcasting. Should be called frequently (at least every 50ms) for responsive mesh communication.
-- In main loop
ez.mesh.update()

Bus Messages

channel/message payload: table {channel, sender, sender_name, text, timesta... Posted when a new channel message is received
Fired when a message arrives on any subscribed channel. Contains the full message details for display or processing.
Payload: table {channel, sender, sender_name, text, timestamp, is_self}
ez.bus.subscribe("channel/message", function(msg)
print(string.format("[%s] %s: %s",
msg.channel, msg.sender_name, msg.text))
end)
channel/unread payload: string Format: "channel_name:count" Posted when a channel's unread count changes
Fired when new messages arrive or are marked as read. UI components can update badges or indicators.
Payload: string Format: "channel_name:count"
ez.bus.subscribe("channel/unread", function(data)
local channel, count = data:match("(.+):(%d+)")
self:update_badge(channel, tonumber(count))
end)
mesh/node_count payload: string Number of nodes as string Posted when the known node count changes
Fired when nodes are discovered or expire from the mesh network. The status bar subscribes to this to update the node count display.
Payload: string Number of nodes as string
ez.bus.subscribe("mesh/node_count", function(count)
self.node_count = tonumber(count) or 0
end)
message/acked payload: string Message ID that was acknowledged Posted when a sent message is acknowledged
Fired when the recipient confirms receipt of a direct message. Used to update message status indicators (checkmarks).
Payload: string Message ID that was acknowledged
ez.bus.subscribe("message/acked", function(msg_id)
self:mark_delivered(msg_id)
end)
message/received payload: table {from, from_name, text, timestamp, conversat... Posted when a direct message is received
Fired when a private message arrives from another node. The DM screen and notification system subscribe to this.
Payload: table {from, from_name, text, timestamp, conversation_id}
ez.bus.subscribe("message/received", function(msg)
Toast.show("DM from " .. msg.from_name)
end)