Communications · Tutorial
Chat, messages, Inbox, and Outbox
Use SolarOS as a network chat client, an off-grid radio messenger, and a notification terminal. This tutorial starts with a local notification, then connects each built-in messaging provider and follows a message from compose to delivery.
Commands in text blocks run at the SolarOS shell. Commands beginning with / run in Chat's input field. Replace example addresses, credentials, radio names, and identifiers with your own values. A name such as community is a local connection label that you choose; it is not a server account.
Understand the shared system
| Component | What you use it for |
|---|---|
| Chat | Select conversations, read retained history, and send messages across providers. |
| Messages | Inspect and control the same conversations from the shell or scripts. |
| Contacts | Keep endpoint addresses, local names, and trust decisions. |
| Inbox | Read incoming notifications from Chat, email, pagers, and local producers. Its unread count appears in the status bar. |
| Outbox | Inspect and cancel pending Chat sends. Completed and failed messages stay in conversation history. |
| Provider jobs | Maintain connections and move messages while the foreground apps are closed. |
A provider class is a protocol, such as irc. An instance is one configured connection, such as community or work-irc. Each instance owns its settings and routing. Two IRC servers can both have a #general channel without mixing their messages.
The four Chat provider classes are Gateway, IRC, MeshCore, and Link. MeshCore has local-radio and BLE-companion backends; Link has packet-radio and ESP-NOW transports. Email and POCSAG publish to Inbox through their own jobs. They do not create reply-capable Chat conversations.
Check what your board and firmware flavor include:
apps
jobs
messages classes
messages instances
messages status
contacts status
inbox status
Missing commands or provider classes require a firmware build with that feature. Radio examples also need an attached, compatible radio. The quick start covers shell access, storage, and Wi-Fi setup. Use writable mounted storage before creating connection profiles. Check the status commands for persistence errors rather than assuming settings were saved.
Try Inbox without a network
Post a local notification and inspect it:
inbox post tutorial "Your messaging terminal is ready."
inbox list unread
inbox
In Inbox, select the entry with Up or Down, then press Enter to open it and mark it read. Left, Backspace, or Esc returns to the list. Press u to switch the unread-only filter, m to toggle read state, and q to exit.
For shell-only use, copy the decimal Inbox ID printed by inbox list:
inbox read <INBOX_ID>
inbox notify test
inbox notify off
On audio-capable boards, notification sound is enabled by default. Use inbox notify on to restore it, or press s in Inbox. The setting persists. inbox post creates a local notification; it does not send a network message or add anything to Outbox.
Connect Gateway chat
Gateway is SolarOS's room-based chat protocol. Use it with a SolarOS gateway server, including the built-in chatd job. It uses the device-wide identity:
identity user alex
identity hostname pocket
Make a small LAN chat server
On one SolarOS device connected to your local network:
job start chatd 7777 demo-token
job status chatd
On a second SolarOS device, connect Wi-Fi using the quick start, then run:
messages add gateway lan chat://192.168.1.113:7777 demo-token
messages connect lan
job start gateway-sync
messages join lan general
messages instances
chat lan
Replace 192.168.1.113 with the server's actual address. To try the client on the server device itself, use chat://127.0.0.1:7777 instead. Create the same lan connection on other clients and join general. Send a short message from each device and check that the others receive it.
chat:// and tcp:// use plaintext TCP. The built-in chatd is a small server for trusted local networks. For a gateway that supplies TLS, configure its chats:// or tls:// endpoint with the administrator's host, port, and token. A gateway token controls access; it does not encrypt a plaintext link.
Use an existing or additional gateway
The default Gateway instance also has dedicated commands:
gateway configure chats://gateway.example.org:7778 example-token
gateway connect
job start gateway-sync
gateway join general
gateway rooms
chat gateway
This is an example endpoint, not a public service address. Use your gateway's settings. For another independent connection, use messages add gateway with another local instance name. One gateway-sync job maintains all enabled Gateway instances; it takes no polling interval.
Selecting a known Gateway room in Chat requests a join and opens it after the server confirms membership. The worker reconnects and requests messages newer than the retained room cursor. Closing Chat leaves the worker running. Gateway currently exposes room conversations; use IRC, MeshCore, or Link for the direct-message examples below.
Connect native IRC
IRC connects directly to an IRC server over verified TLS. It supports channels, private conversations, multiple servers, and optional SASL login. Connect Wi-Fi and ensure the system clock is correct before connecting:
date
ntp
irc add community irc.example.org 6697 pocket
irc connect community
job start irc-sync
irc join community '#general'
irc status
chat community
Replace the example hostname, nickname, and channel with your network's settings. pocket is an IRC nickname, independent of identity user. If the network requires an account, configure it before irc connect:
irc sasl community account-name account-password
SASL PLAIN runs inside verified TLS. If configured authentication fails, SolarOS does not fall back to anonymous login. Server passwords are separate: use irc pass community server-password only if your network requires one. For a private certificate authority, use irc ca community /sdcard/certs/network-ca.pem.
Open a private conversation with an actual nickname on that server:
irc query community anothernick
messages conversations
In Chat you can perform the same membership operations:
/join community '#general'
/query community anothernick
/leave
/leave acts on the selected conversation. Private IRC conversations do not require a Contacts entry. An IRC nickname identifies a route and display name; TLS protects the connection to the server, without end-to-end encryption.
irc-sync reconnects and rejoins saved channels, including when Chat is closed. IRC receives live traffic only: it does not request server history replay, so messages sent by others while you are disconnected can be missed. An outgoing sent state means the complete IRC command was written to the connection, without confirmation that the other user received it. See the IRC reference for authentication and connection behavior.
Use MeshCore with a local radio
MeshCore supplies encrypted direct messages and shared-key group messages over a compatible LoRa radio. SolarOS acts as a non-forwarding node. Use a radio already registered as radio0, or attach one following the expansion guide. Select the appropriate regional profile for your location and network; this example uses the EU868 preset.
messages add meshcore field radio radio0 meshcore-eu868
messages connect field
job start meshcore
messages join field '#general'
messages instances
chat field
Other nearby MeshCore nodes must use compatible radio settings and the exact same group name and key. A #hashtag derives its key from its case-sensitive name; anyone who knows that name can derive the key. For a private group, replace BASE64_PSK with a shared key that decodes to 16 or 32 bytes:
messages configure field channel team BASE64_PSK
messages join field team
Configuration changes restart the affected enabled local instance. Public and private groups remain sender-unverified because group members share a key; encryption alone does not authenticate each displayed sender name.
To open a direct conversation, obtain the peer's full public key from its Contacts endpoint:
contacts list discovered
contacts show <CONTACT_ID>
messages query field <PUBLIC_KEY_HEX>
messages conversations
Review and trust the endpoint as described below before sending. Local-radio direct messages wait for a cryptographic acknowledgement, retry twice after the first attempt, and become delivered or failed. Group sends become sent after transmission and have no recipient acknowledgement. See MeshCore for identity, adverts, channel keys, and additional regional presets. A radio claimed by MeshCore cannot also run radio-link or the pager receiver at the same time.
Use a MeshCore BLE companion
A separate MeshCore device can own the LoRa radio while SolarOS supplies the keyboard, Chat, Contacts, and Inbox. Scan for its actual BLE address and address type, then replace the example address and PIN:
ble status
ble scan
messages add meshcore companion ble 68:ee:8f:69:5f:35 public 123456
messages connect companion
job start meshcore-ble
job status meshcore-ble
messages instances
chat companion
If BLE is disabled for this boot, run ble enable and reboot before scanning; that command saves the next-boot setting. Use the address type shown by the scan, rather than assuming public. Supplying the companion's six-digit PIN requests BLE encryption and bonding. Configure the radio region, device identity, and channel slots on the companion itself. SolarOS imports its contacts, eight channel slots, and queued messages. Select an imported group in Chat, or join an existing channel by its name:
messages join companion '#general'
That group must already exist on the companion. Direct conversations use messages query companion <PUBLIC_KEY_HEX>. The local-radio meshcore commands do not configure a BLE companion.
Both MeshCore jobs can run together on separate resources. Chat marks groups as [radio] or [companion]. A group marked (history) remains readable but cannot accept sends because it is outside the current transport or channel configuration. Companion direct sends wait for the expected MeshCore ACK; group sends become sent when the companion accepts them.
Use SolarOS Link over packet radio
Link carries short text packets between SolarOS devices. It is separate from MeshCore and uses its own wire protocol. For a registered packet radio and a profile suitable for both devices:
messages add link nearby link0
messages connect nearby
job start radio-link link0 radio0 lora-eu868 chat=on
messages join nearby broadcast
link status link0
chat nearby
Run the matching transport and Chat setup on the peer device. Send a broadcast from Chat; each receiver learns the sender's device ID and creates a discovered Contact and direct conversation. Use the peer's ID from its link status output to open a direct conversation explicitly:
messages query nearby 0x12345678
Replace that example ID with the real peer ID. Review its Contacts endpoint before sending direct messages. Broadcasts become sent after transmission; unicast can become delivered after a matching Link acknowledgement. Link packets are unencrypted, and device IDs can be spoofed. Trusting a local contact does not add encryption or cryptographic identity checks.
Each message must fit the transport's packet payload. Check link status for the active MTU and keep messages short. The base packet protocol supplies no fragmentation or automatic retransmission. Its reliable byte-stream feature is a separate service; see SolarOS Link.
Use SolarOS Link over ESP-NOW
ESP-NOW uses the ESP32's Wi-Fi radio without requiring an access point or an internet connection. On two devices, choose the same channel and PHY:
messages add link nearby esp0
messages connect nearby
job start espnow-link esp0 channel=6 phy=normal chat=on
messages join nearby broadcast
link status esp0
chat nearby
This is an alternative to the preceding nearby example. Choose a different instance name if keeping both connections. A fixed channel requires any simultaneous Wi-Fi station or AP connection to use that channel. With channel=auto, ESP-NOW follows an active station or AP, or starts on channel 6.
Broadcast once from each peer so both learn the other's device-ID-to-MAC mapping. Direct conversations then work like packet-radio Link. To initiate unicast immediately after reboot, save the peer's actual mapping:
espnow peer add 0x12345678 24:6f:28:11:22:33
espnow peers
messages query nearby 0x12345678
One Link Chat instance binds each named transport. For either Link transport, chat=on already projects received messages into Inbox. Use inbox=on instead of chat=on only when you want simple incoming text notifications without Chat conversations; the options are mutually exclusive.
Receive email in Inbox
Email has its own receive-only IMAPS account and viewer. Configure a mailbox with its actual host, user, and password or app password:
email configure imaps://imap.example.com user@example.com app-password INBOX
email sync
email status
email
Wi-Fi must be connected, and TLS certificate validation is mandatory. The one-shot synchronization runs asynchronously; check status for completion. Open inbox afterward to see the incoming email notifications alongside Chat. For periodic checks:
job start email-sync 300
job status email-sync
The interval is in seconds, with a default of 300 and a range of 30–86400. The first sync imports up to the newest eight messages; later polls fetch new messages in batches of eight. The email viewer keeps the newest 32 messages in memory, while Inbox notifications persist independently.
The viewer shows best-effort text previews. SMTP sending, attachments, full MIME decoding, and server-side read flags are not implemented. Reading locally updates Inbox state, without marking the message read on the mail server. Email does not use Chat's Outbox. See the email job reference.
Receive POCSAG pager messages
The pager job listens on a compatible radio, filters by receiver identity code (RIC), and posts decoded pages to Inbox. Replace the frequency, baud rate, and RIC with your pager system's settings; the frequency is in Hz:
job start pocsag radio0 <PAGER_FREQUENCY_HZ> 1200 <YOUR_RIC> alpha
pocsag status
inbox list unread
The radio must support the required continuous FSK receive path and be free of other jobs. The receiver supports alphanumeric and numeric pages, and normal or inverted polarity. Identical repeated pages within 30 seconds produce one notification. There is no Chat conversation or automatic reply.
For a system where you are authorized to transmit, SolarOS also has a direct page-send command. Stop a receiver using the same half-duplex radio first:
job stop pocsag
pocsag send radio0 <PAGER_FREQUENCY_HZ> 1200 <YOUR_RIC> "SolarOS calling" alpha normal
This transmission bypasses the shared Outbox. See the pager reference for supported modes and reception diagnostics.
Read and send in Chat
After connecting any Chat provider:
chat
Chat initially selects the newest unread conversation. Use chat community to filter by instance, chat irc to filter by provider class, or chat 7 to open a decimal conversation ID obtained from messages conversations.
| Control | Action |
|---|---|
Tab | Switch between Channels and Chat. |
Up / Down in Channels | Select a conversation. |
Enter in Channels | Open it; known Gateway rooms request a join first. |
Enter in Chat | Send the input text or run a slash command. |
Up / Down in Chat | Browse input history. |
Page Up / Page Down | Scroll retained messages. |
Esc or /quit | Exit Chat. Provider jobs continue. |
Useful in-app commands include:
/help
/classes
/instances
/status
/join lan general
/query community anothernick
/new <CONTACT_ID>
/new uses a contact's preferred endpoint. Choose an explicit instance and /query target when you need a particular server or transport. The header reports connection state: Gateway and IRC use disconnected, connecting, and connected; MeshCore and Link use stopped, starting, ready, and error. A running job alone does not prove that its connection is ready.
Manage Contacts and endpoint trust
A contact can hold several endpoints, each with its own trust state. Inspect the endpoint address before trusting it:
contacts list discovered
contacts show <CONTACT_ID>
contacts rename <CONTACT_ID> Alice
contacts trust <CONTACT_ID> <ENDPOINT_ID>
Use IDs from the preceding output. A discovered MeshCore advert proves possession of a public key, without proving the human identity. Link IDs have no cryptographic authentication. Verify the address with the person through a channel you already trust.
Sending directly to a discovered endpoint requires a second Enter confirmation in Chat, or --allow-untrusted for one shell send. That confirmation does not change its saved trust. Blocked endpoints reject direct inbound and outbound messages. To block one endpoint:
contacts block <CONTACT_ID> <ENDPOINT_ID>
contacts link <TARGET_CONTACT_ID> <SOURCE_CONTACT_ID> merges the source contact's endpoints into the target and removes the source contact record. It preserves each endpoint's trust; trusting a person's MeshCore key does not automatically trust their Gateway or Link endpoint. The Contacts TUI supports / to search and Enter to inspect an entry.
Follow a send through Outbox
Use a real conversation ID from messages conversations. Here 7 is an example, not a fixed room number:
messages conversations
messages send 7 "Hello from the shell."
outbox
messages list 7
The send command prints a stable hexadecimal message ID. Outbox lists pending requests; a fast successful send may leave it before you inspect it. Review conversation history for the final state.
| State | Meaning |
|---|---|
queued | Accepted into the shared in-memory queue and waiting for its provider. |
sending | The provider is processing the request. |
sent | The provider completed its send step; recipient receipt depends on the protocol. |
delivered | The provider received its supported acknowledgement. It is not a human read receipt. |
failed | The send failed or could not safely resume. Inspect the provider and resend explicitly if appropriate. |
cancelled | A queued request was cancelled before its provider consumed it. |
IRC has no recipient delivery ACK. MeshCore groups and Link broadcasts have no recipient ACK either. MeshCore direct messages use cryptographic ACKs; Link unicast ACKs are not cryptographically authenticated.
Cancel a still-pending request by copying its exact message ID:
outbox cancel <MESSAGE_ID_HEX>
messages outbox and messages cancel <MESSAGE_ID_HEX> expose the same operations. Cancellation is not a recall: it cannot retract a packet or command already transmitted. A completed or already-consumed request may no longer be cancellable.
The queue is bounded to 16 requests. A disconnected provider may retain an accepted queued request until it can send, but acceptance can fail when the queue is full or a conversation cannot send. Outbox is volatile: after reboot, retained queued or sending messages become failed and require an explicit resend. Closing Chat preserves jobs and queues; rebooting does not.
Keep notifications and history straight
Inbox IDs are decimal notification IDs. Conversation IDs are decimal routing IDs. Message IDs are stable 64-bit hexadecimal strings. Copy each from its own command output; they are not interchangeable.
messages list <CONVERSATION_ID>
messages read <CONVERSATION_ID>
inbox list unread
inbox read <INBOX_ID>
Chat and Inbox align read state for linked messages. Inbox holds notifications from other sources too, including email, pages, and inbox post. An Inbox notification is not a separate outgoing conversation.
Both stores are bounded: the shared messaging service retains up to 64 messages, and Inbox up to 64 notifications. SD-backed Chat uses /.messages/messages.bin. On internal flash it restores the compact message copy in /.inbox/messages.bin, without creating a second flash ring. Old entries can be evicted; these stores are recent history, not an archive.
Choose the scope carefully when deleting:
| Command | Effect |
|---|---|
inbox delete <INBOX_ID> | Delete one Inbox notification. |
messages delete <MESSAGE_ID_HEX> | Delete one retained message and its linked Inbox notification. |
messages clear community | Clear that instance's retained history and owned Inbox notifications. Keep its connection profile. |
messages remove community | Disconnect and remove that instance, its settings and credentials, pending sends, local messages, and Inbox notifications. |
inbox clear | Clear the entire universal Inbox, including email, pager, and local notifications. |
These are local operations, without remote message recall or mailbox deletion. On internal-flash-only systems, clearing Inbox also removes the backing copy used to restore compact Chat history after reboot. Use messages disconnect community to disable a connection while keeping its profile and history. Removing a Link Chat instance keeps the underlying Link transport and byte streams running. Recreating an instance name allocates a new instance ID.
Start your chosen workers after reboot
Connection settings and desired enabled state are saved, but the workers must also run. Use setterm startup to find the selected startup script. Add only the workers you use, after the networking or expansion setup they need:
job start gateway-sync
job start irc-sync
job start meshcore
job start meshcore-ble
job start email-sync 300
This is a menu of worker lines, not a requirement to run every provider. MeshCore's no-argument starts use the saved enabled bindings. Link workers need their transport arguments, for example:
job start espnow-link esp0 channel=6 phy=normal chat=on
Profiles, contact files, and credentials on mounted storage are not encrypted. Protect access to the volume and use placeholders in shared scripts or screenshots. Save credentials through the setup commands once instead of placing passwords in the startup script. TLS and MeshCore's transport protection do not encrypt the device's local storage.
Diagnose a missing message
Start with the shared view, then inspect the worker that owns the connection:
messages instances
messages status
messages conversations
outbox
inbox status
job status irc-sync
Replace the last job name with the provider you are using. For Gateway, inspect gateway status and gateway rooms; for IRC, irc status; for a BLE companion, job status meshcore-ble; for Link, link status <TRANSPORT>; for email or pages, email status or pocsag status.
If no conversation appears, join the room or query the peer on the intended instance. If sends stay queued, check that the instance is enabled, its worker is running, and the transport is ready. For a failed direct send, check trust, the peer address, and acknowledgement support. For (history) groups, select an active group. For a missing notification, inspect retained messages and the bounded store's drop and persistence status. IRC cannot recover traffic missed while disconnected.
The same conversation store is available to automation through Python and Lua. Those APIs use hexadecimal strings for message IDs and enforce the same endpoint trust rules as Chat and the shell. The Messaging reference describes the shared instance, identity, and storage contracts.