DownloadMeshCore Interface Improvements May 17 2026
> Draft Notice: This proposal is a draft, generated by AI assistance, and may not have been reviewed for technical accuracy. Validate the details against the live code before implementation.
Overview
The current PacketBBS interface already works as a compact command shell for MeshCore-style access, but it is still shaped mostly like a tiny terminal. That is a problem if the same core is expected to serve:
-
MeshCore
-
Meshtastic
-
AX.25 TNC adapters
-
an SMS gateway
Those transports do not just differ in line width. They differ in total payload budget, operator patience, delivery latency, and how expensive extra prompts are. The interface should therefore become more sparse, more stateful, and more adapter-neutral.
This proposal recommends evolving PacketBBS from a line-width renderer into a transport-budgeted command interface with one canonical command grammar and adapter-specific output profiles.
Constraints
Assume these practical payload ceilings for user-visible text:
| Transport | Maximum payload |
|---|---:|
| ASCII SMS | 160 chars |
| MeshCore | 150 chars |
| Meshtastic | 200 chars |
AX.25 can carry more, but it should still use the same sparse command grammar so operators can move between transports without learning a different shell.
Current Issues
The current interface is already terse, but several parts still optimize for rows and columns more than actual transport budgets:
-
`src/PacketBbs/PacketBbsTextRenderer.php` pages lists by row count and width, not by total character budget.
-
A `meshcore` page can currently exceed 150 characters once headers, body lines, and footers are counted.
-
The command set has accumulated multiple aliases (`MAIL`, `NM`, `NETMAIL`, `READ`, `NR`, `R`, etc.), which is good for compatibility but noisy for low-friction onboarding.
-
Paging is list-oriented rather than task-oriented. The user often has to remember whether they were in `MAIL`, `AREAS`, or a message body.
-
Compose mode is functional, but not especially radio-friendly. It assumes multi-packet focus and does not expose draft state very clearly.
-
There is no explicit SMS profile, even though SMS is the most severe transport constraint in this family.
Design Goals
-
Keep one sparse CLI across all low-bandwidth adapters.
-
Separate canonical commands from compatibility aliases.
-
Budget by total payload chars first, line width second.
-
Prefer short stateful verbs over repeated long commands.
-
Make the most common flows fit in one or two exchanges.
-
Preserve room for adapter-specific polish without forking the user model.
-
Maintain lightweight session context so follow-up commands can infer the current working area or object.
Recommendation
Adopt a two-layer interface model:
Layer 1: Canonical command grammar
Define a small stable command set that all adapters document first:
-
`H` help
-
`L user code` login
-
`W` who
-
`N` netmail list
-
`A` areas list
-
`T <tag>` open area
-
`R <id>` read
-
`Y <id>` reply
-
`S <user> <subj>` send netmail
-
`P <tag> <subj>` post echomail
-
`M` more / next
-
`B` back / previous
-
`Q` quit
-
`U` status / where-am-I
Existing long aliases should remain supported for compatibility, but the system should present and teach only the short grammar by default.
The command shell should also preserve working context. If a user opens an area such as LVLY_TEST, later commands should assume that area until the context changes or the session ends.
Layer 2: Adapter profiles
Keep transport-specific rendering in profiles, but make them operate on payload budgets:
| Profile | Soft target | Hard cap | Notes |
|---|---:|---:|---|
| sms | 120 | 160 | Aggressive abbreviation, one-screen replies |
| meshcore | 120 | 150 | Normal compact mode |
| meshtastic | 160 | 200 | Same budget as MeshCore; keep output almost identical |
| tnc | 320 | configurable | Same commands, less aggressive truncation |
The key change is this: page generation should stop when the next appended line would exceed the profile cap, not when a fixed number of rows has been emitted.
Interface Improvements
1. Replace row-count paging with char-budget paging
This is the highest-value change.
Today, width and rows are separate knobs. For SMS and mesh transports, total visible chars matter more than either. Rendering should:
-
build responses line by line
-
track total payload length including newlines
-
stop before exceeding the adapter cap
-
append a compact continuation footer such as `M=more`
That makes the interface predictable across adapters and avoids accidental oversize pages.
2. Add an explicit SMS profile
Even if the SMS gateway is not implemented yet, the core should support it now. SMS should not be treated as a smaller meshcore; it needs its own profile with:
-
stronger abbreviation
-
fewer rows per reply
-
shorter prompts
-
no decorative intro text
This forces the command shell to stay honest about brevity.
3. Make help stateful and tiny
The current help text is still a bit chatty for first contact. Replace it with task-first help:
H: L user code | N mail | A areas
R id | Y id | M more | U status
Then allow contextual help:
H N
N:list R id:read S u subj:send
The system should avoid advertising every compatibility alias in-band.
Example help output
Default help:
H: L user code | N mail | A areas
T tag | R id | Y id | P post
M more | B back | U status | Q quit
Contextual help while an area is active:
Area LVLY_TEST
R id | P post | M more | U status
Q quit
Contextual help for posting:
H P
P: post in current area
No area? use T tag
Subj? Msg: /S /C
Contextual help for reading:
H R
R id: read item
In list, id may be slot number
Use M/B to move
Contextual help for status:
H U
U: show active area, list, msg,
or draft state
4. Add U for status
Low-bandwidth users lose context easily. A short U command should report current state, for example:
mail p2/3
or:
area LVLY_CHAT p1/4
or:
draft netmail to bob subj Test
This is better than forcing the user to guess whether M will continue a list, a body, or do nothing.
5. Add persistent working context
The interface should maintain a small amount of explicit session context, at minimum:
-
current area
-
current list type
-
current page
-
current message
-
current compose draft
That allows commands to become shorter and more natural. For example:
AREA LVLY_TEST
sets the current area context to LVLY_TEST, after which:
POST Test message
can be interpreted as:
POST LVLY_TEST Test message
Likewise:
-
`M` should continue the current list or message
-
`R 2` should read within the current list context
-
`POST` without an area should use the current area when one exists
-
`Y` or `REPLY` during a message view should default to the current message if that is unambiguous
This statefulness is especially important on SMS and mesh links because repeating area tags wastes user effort and airtime.
The same principle should apply not just between top-level commands, but within a command flow. If a user has already selected an area, then a bare POST should be allowed to start a guided multi-step interaction:
AREA LVLY_TEST
POST
Subj?
Testing from field
Line 1. /S=send /C=cancel
hello from radio
+
/S
Posted LVLY_TEST
That is, command context and prompt/response state should both live in session state.
Context model
The current packet_bbs_sessions concept should be extended from general pagination state into an explicit working-context model. Rather than expanding the table with more narrowly-scoped columns, the session row should gain a generic session_state JSON field that can store evolving interface state.
A practical session_state object would include values such as:
| Key | Purpose |
|---|---|
| current_area_tag | Active echoarea tag such as LVLY_TEST |
| current_area_domain | Optional area domain such as lovlynet |
| current_list_type | Current list scope: netmail, areas, echomail, message |
| current_list_page | Current page number in that scope |
| current_message_id | Message currently being viewed |
| current_message_type | netmail or echomail |
| current_slots | Mapping of visible slots like 1, 2, 3 to real message IDs or area identifiers |
| compose_target_type | netmail or echomail |
| compose_target_ref | Target user or current area for the draft |
Example:
{
"current_area_tag": "LVLY_TEST",
"current_area_domain": "lovlynet",
"current_list_type": "echomail",
"current_list_page": 1,
"current_message_id": 4421,
"current_message_type": "echomail",
"current_slots": {
"1": { "type": "message", "id": 4421 },
"2": { "type": "message", "id": 4418 }
},
"compose_target_type": null,
"compose_target_ref": null
}
This is the better long-term fit because:
-
new state keys can be added without schema churn
-
different adapters may need slightly different transient context
-
shorthand command experiments do not require table changes
-
stale keys can simply be ignored by newer code
The existing scalar columns that are already central to session lifecycle, such as authentication identity and last activity time, can remain as normal columns. The JSON field should hold command-shell working context rather than replace the entire session row.
Context rules
The interface should behave predictably. The main rules should be:
-
`AREA <tag>` sets `current_area_*` and makes that area the working area.
-
`AREAS` does not clear the current area unless the user explicitly opens another one.
-
`R <id>` on a message inside an area sets both `current_message_*` and preserves the current area.
-
`MAIL` switches list context to netmail, but should not necessarily destroy area context. The user may return to posting in the same area later.
-
`POST <subject>` with no area should use `current_area_*` if set.
-
`POST <tag> <subject>` should override and replace the current area context.
-
`REPLY` or `Y` with no ID should reply to `current_message_id` if one is active and unambiguous.
-
`Q` should clear all context.
-
Session timeout should clear authentication and compose state, and should probably clear message context; area context may be preserved if that proves useful.
-
In-progress command flows such as `POST`, `SEND`, and `REPLY` should record their step inside `session_state` so the next inbound line is interpreted as the answer to the current prompt, not as a fresh top-level command.
Context-sensitive command examples
With this model, these sequences become valid and unsurprising:
AREA LVLY_TEST
POST Testing from field
Meaning:
POST LVLY_TEST Testing from field
And:
AREA LVLY_TEST
R 2
REPLY
Meaning:
-
read slot or message `2` from the current area list
-
then reply to the message currently in view
And:
AREA LVLY_TEST
MAIL
POST Follow-up
Meaning:
-
list netmail now
-
but still post into `LVLY_TEST`, because area context remains active unless explicitly replaced
This is one place where a U status command becomes important, because it lets the user verify the active context cheaply:
area LVLY_TEST
list mail p1/2
If that mixed-context model feels too implicit in practice, a stricter rule can be adopted:
-
only one primary working context at a time
-
entering `MAIL` suspends area context until `AREA <tag>` is used again
My recommendation is to preserve area context across MAIL, because posting back into the same area is a common sparse-interface workflow.
Multi-step command state
The interface should support guided command flows for operators who do not want to send the full command in one packet. This is especially important for SMS and lossy mesh links where shorter exchanges are easier to compose.
A command flow should be able to advance through explicit steps stored in session_state, for example:
| Key | Purpose |
|---|---|
| active_flow | Current flow such as post, send, reply |
| flow_step | Current prompt stage such as await_subject or await_body |
| flow_context | Small JSON object containing target area, reply target, partial subject, draft counts, etc. |
Example:
{
"current_area_tag": "LVLY_TEST",
"active_flow": "post",
"flow_step": "await_subject",
"flow_context": {
"target_type": "echomail",
"target_area_tag": "LVLY_TEST",
"target_area_domain": null
}
}
Then after the subject is received:
{
"current_area_tag": "LVLY_TEST",
"active_flow": "post",
"flow_step": "await_body",
"flow_context": {
"target_type": "echomail",
"target_area_tag": "LVLY_TEST",
"target_area_domain": null,
"subject": "Testing from field"
}
}
This lets the command shell interpret incoming lines correctly:
-
when no flow is active, parse input as a top-level command
-
when a flow is active, treat the next line as flow input
-
`/S` completes the flow
-
`/C` cancels the flow and clears `active_flow`, `flow_step`, and `flow_context`
Example guided flows
Guided post with current area
AREA LVLY_TEST
POST
Subj?
Testing from field
Msg:
hello from radio
+
/S
Posted LVLY_TEST
Interpretation:
-
`AREA LVLY_TEST` sets `current_area_tag`
-
`POST` sees that area context exists and begins a `post` flow
-
next line becomes the subject
-
subsequent lines become body lines
-
`/S` submits using the stored target area
Guided netmail send
SEND bob
Subj?
Meeting
Msg:
See you at 8
/S
Sent bob
Guided reply
R 2
REPLY
Msg:
Agreed
/S
Replied
In the reply case, the current message context supplies the reply target, so the user only needs to provide the body.
Prompt design for sparse links
Guided prompts should be extremely short:
-
`Subj?`
-
`Msg:`
-
`More?`
-
`/S /C`
The system should avoid verbose compose banners on the smallest transports. If more context is needed, U should show it:
draft post LVLY_TEST
subj Testing from field
2 lines
This gives the user confidence about the active flow without spending every response on repeated explanatory text.
6. Prefer slot-oriented list navigation
For very small transports, reading by database ID is awkward. The interface should optionally show short slots in the current page:
N 1/2
1* Bob Meeting
2 Al Files
R 1-2 M=more
Then R 1 means "read slot 1 from the current page" while R #123 can remain available for absolute IDs if needed.
This is especially useful for SMS, where shorter operator input matters.
7. Shorten recurring prompts
Common boilerplate should be compressed:
-
`Not logged in. LOGIN <user> <code>` -> `Login: L user code`
-
`Unknown. Send HELP.` -> `Unknown. H`
-
`Already at first page.` -> `At start.`
-
`No more. Try MAIL or AREA <tag>.` -> `No more. U`
This does not remove clarity; it removes repeated cost.
8. Make compose mode draft-aware
Compose mode should expose state more clearly with minimal text:
Draft mail bob
Subj Test
/S send /C cancel
After each appended line, SMS and mesh profiles should continue to answer with very short acknowledgements, ideally one of:
-
`+`
-
`2 lines`
-
`140 chars`
The point is to let the operator know the draft still exists without wasting a packet on full prose.
9. Preserve one transport-neutral mental model
Meshtastic, MeshCore, SMS, and TNC should all feel like the same shell with different compression levels. Do not create:
-
a MeshCore-specific menu tree
-
an SMS-only command set
-
transport-specific verbs for core actions
The adapter may alter rendering, truncation, and maybe notification policy, but not the primary operator workflow.
Suggested Response Patterns
These patterns would work well across all sparse adapters:
Login
Hi alice. N mail A areas H
Netmail list
N 1/2
1* Bob Meeting
2 Alice Files
R 1-2 M=more
Areas list
A 1/2
1 LVLY_CHAT
2 FIDO_SYSOP
T 1 M=more
Enter area context
AREA LVLY_TEST
LVLY_TEST 1/3
1 Matt Welcome
2 Sue Test post
R 1-2 P=post M=more
Post using current area
POST Testing from radio
Draft post LVLY_TEST
Subj Testing from radio
/S send /C cancel
Message read
#12 Bob 2026-05-17
Meeting
Can do 8pm.
Y 12 M=more
Status
msg #12 p1/2
Command Tables
The final user-facing documentation in docs/PacketBBS.md should include clear command tables grouped by high-level context. Each table should include:
-
full command name
-
short code
-
description
-
whether the command uses current session context when available
That structure will make the sparse interface easier to learn without forcing the live HELP output to become verbose.
Global context
| Full command | Short code | Description |
|---|---|---|
| HELP | H | Show general help or contextual help for a command. |
| LOGIN <user> <code> | L <user> <code> | Log in using PacketBBS TOTP. |
| WHO | W | Show who is online. |
| STATUS | U | Show current area, current list, current message, or draft state. |
| QUIT | Q | End the session and clear context. |
Area navigation context
| Full command | Short code | Description |
|---|---|---|
| AREAS | A | List subscribed areas. |
| AREA <tag> | T <tag> | Open an area and make it the current working area. |
| MORE | M | Show the next page of the current list or message. |
| BACK | B | Show the previous page of the current list or message. |
Netmail context
| Full command | Short code | Description |
|---|---|---|
| MAIL | N | List netmail messages. |
| READ <id> | R <id> | Read a netmail or current-list item by slot or ID. |
| REPLY <id> | Y <id> | Reply to a specific message, or to the current message when the ID is omitted and context is clear. |
| SEND <user> <subject> | S <user> <subject> | Start a netmail draft in one step. |
| SEND <user> | S <user> | Start a guided netmail flow and prompt for subject. |
Echomail / current area context
| Full command | Short code | Description |
|---|---|---|
| AREA <tag> | T <tag> | Open an area and set area context. |
| READ <id> | R <id> | Read a message from the current area list by slot or ID. |
| REPLY <id> | Y <id> | Reply to a specific message, or to the current message when context is clear. |
| POST <tag> <subject> | P <tag> <subject> | Start a post in a named area. |
| POST <subject> | P <subject> | Start a post in the current area. |
| POST | P | Start a guided post flow in the current area and prompt for subject. |
Guided flow context
| Full command | Short code | Description |
|---|---|---|
| SEND | /S | Submit the current draft. |
| CANCEL | /C | Cancel the current draft or guided flow. |
| STATUS | U | Show current draft target, subject, and body progress. |
For implementation clarity, the runtime should still accept compatibility aliases such as RP, NM, NETMAIL, AREA, POST, and other current forms. The tables above describe the recommended user-facing grammar, not necessarily the full parser surface.
Implementation Direction
Core changes
-
Replace the current fixed `PAGE_SIZES` and `MSG_PAGE_SIZES` model with profile objects that include:
- soft target
- hard cap
- line width
- abbreviation level
-
Teach `PacketBbsTextRenderer` to paginate by accumulated payload length.
-
Add a canonical short-command help mode and keep older aliases as hidden compatibility inputs.
-
Add `sms` as a first-class interface value.
-
Add `U` status support in `src/PacketBbs/PacketBbsGateway.php`.
-
Add a generic `session_state` JSON field to persist `current_area`, `current_message`, slots, and other working context.
-
Consider adding slot-based page state so current-page item `1`, `2`, `3` can be resolved without exposing long IDs every time.
-
Define deterministic context-set and context-clear rules before adding new shorthand commands, so the shell stays predictable.
-
Represent in-progress guided flows inside `session_state` using keys like `active_flow`, `flow_step`, and `flow_context`.
-
Update `docs/PacketBBS.md` so the shipped docs match the new sparse command grammar, context rules, and guided-flow behavior.
Things that should not change yet
-
Authentication model using TOTP.
-
Bridge adapter architecture with `bridge_node_id` and `node_id`.
-
Shared PacketBBS core concept.
-
Existing long-form command aliases, at least initially.
Proposed Rollout
Phase 1
-
Add transport-budgeted renderer
-
add `sms` profile
-
add persistent current-area context
-
add guided multi-step flow state for `POST`, `SEND`, and `REPLY`
-
shorten help and error text
-
add `U` status
Phase 2
-
add slot-based reading for paged lists
-
allow `POST` and related commands to infer the current area
-
shorten guided prompts for SMS and mesh transports
-
compact compose acknowledgements
-
document canonical short grammar and grouped command tables in `docs/PacketBBS.md`
Phase 3
-
adapter-specific tuning based on real bridge testing
-
decide whether notifications or summaries should differ by transport
Conclusion
The main improvement is not "make MeshCore prettier." It is to make PacketBBS truly transport-budgeted and adapter-neutral. If that is done well, MeshCore, Meshtastic, SMS, and AX.25 can all use the same sparse CLI while differing only in how aggressively the renderer compresses output.
That keeps the interface coherent, keeps bridges thin, and makes future adapters cheaper to add.
|