The README and the project documents are in ASD-STE100

The README, CONTRIBUTING, SECURITY, the docs index, the development
documents, the script READMEs, the comments of config.example.toml and
pyproject.toml, and the issue and pull request forms. The legal sections
of the README (attribution and licence) stay exactly as they were, between
ste: off and ste: on markers. The SignPath attribution line, the one-line
description of MeshTerm, every code block, and every URL do not change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-10-06 21:44:55 -04:00
1 parent 962d96f8d3
commit e6f6cf2621
14 files changed
+691 -619

No files matched your search

+45 -42
View File
@@ -5,30 +5,30 @@ body:
- type: markdown
attributes:
value: |
Hopefully you never need this form.
We hope that you never need this form.
If you do: start by running this, and attach the file it writes.
If you do need it, first run this command and attach the file that it writes.
```
meshterm diagnostics --out bug.md
```
It answers which MeshTerm, on which machine, in which terminal, talking to which
radio over which transport and firmware — most of what I would otherwise ask one
question at a time, and the details people most often end up guessing at. Nothing
in it is private: no keys, no pairing PINs, no positions, no contacts. That is a
tested property, not a promise.
The file says which MeshTerm you use, on which machine, in which terminal, and with
which radio, transport, and firmware. This is most of the information that I would
otherwise ask for in single questions. It is also the information that people most
often guess. The file has no private information: no keys, no pairing PINs, no
positions, and no contacts. A test checks this. It is not only a promise.
The **Diagnostics** page in the menu saves the same file (with `s`, or F3 on the
PicoCalc), next to the log.
The **Diagnostics** page in the menu saves the same file next to the log. Press `s`,
or F3 on the PicoCalc.
The rest of this form asks only for what that file cannot know.
The rest of this form asks only for the information that the file cannot know.
- type: textarea
id: problem
attributes:
label: What's the problem
description: What happens, and what you expected to happen instead.
label: What is the problem
description: What happens, and what you expected to happen.
validations:
required: true
@@ -47,11 +47,12 @@ body:
- type: textarea
id: repro
attributes:
label: How do I make it happen
label: How can I make it happen
description: >
The steps, in order, starting from launching MeshTerm. Screens and keys beat
descriptions — "Contacts, Enter on a node, Routes tab, Enter on the second route"
is worth more than "open a trace".
Write the steps in order. Start from the time that you start MeshTerm. Name the
screens and the keys. This is more useful than a description. For example,
"Contacts, Enter on a node, Routes tab, Enter on the second route" is better than
"open a trace".
placeholder: |
1.
2.
@@ -64,15 +65,16 @@ body:
attributes:
label: Does it still happen with `meshterm --mock`
description: >
`--mock` runs the whole app against a fake radio. If the bug survives that, it is
in MeshTerm and I can chase it without owning your hardware. If it doesn't, it is
in the link or the radio, which is a different hunt. This one question decides
which of those two jobs this is, so it is worth the minute it takes.
`--mock` runs the complete app with a fake radio. If the bug also occurs with
`--mock`, the bug is in MeshTerm, and I can look for it without your hardware. If
the bug does not occur with `--mock`, it is in the link or in the radio. This is a
different search. This one question tells me which of the two jobs this is, so
please take the minute to answer it.
options:
- "Yes, still happens with --mock"
- "No, --mock is fine"
- "Haven't tried"
- "Doesn't apply — the problem is connecting"
- "I have not tried it"
- "Does not apply: the problem is the connection"
validations:
required: true
@@ -81,9 +83,9 @@ body:
attributes:
label: The diagnostics block
description: >
Attach `bug.md` by dragging it in, or paste its contents here. If you genuinely
cannot produce one, because MeshTerm will not start at all, write down why instead,
along with your version, and I will work with that.
Drag `bug.md` into this field, or paste its contents here. If you cannot make the
file, because MeshTerm does not start at all, write the reason instead. Also write
your version. I will work with that information.
validations:
required: true
@@ -92,9 +94,9 @@ body:
attributes:
label: Which handheld, if it is a handheld
description: >
Only if you are running on something the diagnostics file cannot name. It knows a
PicoCalc from a regular terminal, but a uConsole and a laptop look much the same to
it. Leave blank on a desktop, laptop, or Mac.
Answer this only if you use a device that the diagnostics file cannot name. The file
can tell a PicoCalc from a regular terminal. But a uConsole and a laptop look almost
the same to it. Leave this blank on a desktop, a laptop, or a Mac.
placeholder: "ClockworkPi uConsole (CM4)"
- type: input
@@ -102,10 +104,10 @@ body:
attributes:
label: Which radio and firmware, if MeshTerm could not reach it
description: >
The diagnostics file reads the model and firmware off the radio itself, so when the
problem *is* connecting, it has nothing to read. That is the one case where I need
you to tell me. Be precise — "a Xiao" covers several boards that do not behave
alike.
The diagnostics file reads the model and the firmware from the radio itself. When
the problem is the connection, the file has nothing to read. In this one case, I
need you to tell me. Be exact. "A Xiao" can mean several boards that do not behave
in the same way.
placeholder: "Seeed XIAO nRF52840 + Wio-SX1262, MeshCore 1.15.0"
- type: textarea
@@ -113,8 +115,9 @@ body:
attributes:
label: The exact error, if there was one
description: >
Copy it as text rather than photographing the screen — text can be searched, and a
photo of a terminal is hard to read. Paste the whole traceback if it crashed.
Copy it as text. Do not take a photograph of the screen. A search can find text, and
a photograph of a terminal is difficult to read. If MeshTerm crashed, paste the
complete traceback.
render: text
- type: textarea
@@ -122,22 +125,22 @@ body:
attributes:
label: The log file
description: >
MeshTerm keeps one at `~/.meshterm/meshterm.log` (or wherever `MESHTERM_HOME`
points). Attach the file, or paste the last twenty lines inside a ``` fence. If the
problem is hard to catch, set **Log detail** to Debug on the Preferences page,
restart MeshTerm, do it once more, and send that instead. It records a great deal
more.
MeshTerm keeps a log at `~/.meshterm/meshterm.log` (or at the place that
`MESHTERM_HOME` points to). Attach the file, or paste the last twenty lines inside a
``` fence. If the problem is difficult to catch, set **Log detail** to Debug on the
Preferences page and restart MeshTerm. Then do the action one more time, and send
that log instead. It records much more information.
- type: textarea
id: workaround
attributes:
label: Is there a way around it
description: Anything that avoids it, or gets you unstuck. Say so even if it's ugly.
description: Write anything that avoids the problem or lets you continue. Tell us also when the way around is not good.
- type: textarea
id: extra
attributes:
label: Anything else
description: >
Did it used to work? Did it start after an update? Screenshots of a wrong-looking
screen are genuinely useful here, even though text is better for errors.
Did it work before? Did the problem start after an update? Screenshots of a screen
that looks wrong are very useful here. For errors, text is better.
+8 -7
View File
@@ -1,15 +1,16 @@
blank_issues_enabled: false
contact_links:
- name: Not sure it's a bug? Ask on Discord
- name: Not sure it is a bug? Ask on Discord
url: https://discord.gg/AZwe5Uvb3S
about: >
Setup trouble, "is it supposed to do this", or anything you want a second opinion
on. The #support forum is the place. Someone running your radio has probably hit it.
Use Discord for a problem with the setup, for the question "is it correct that it does
this", or when you want a second opinion. The #support forum is the correct place.
Probably someone who runs your radio had the same problem.
- name: "No GitHub account? Report it in the #bugs forum instead"
url: https://discord.gg/AZwe5Uvb3S
about: >
One post per bug, tagged, with the diagnostics file attached. Real ones get opened
here and your post is renamed with the issue number, so you can follow it without an
account — and something comes back to tell you when it ships. Being told beats
filing it tidily.
Make one post for each bug. Add a tag, and attach the diagnostics file. I open an issue
here for each real bug, and I rename your post with the number of the issue. Thus you can
follow it without an account. You also get a message when the correction is released.
It is better that you tell us about a bug than that you file it in a tidy way.
+7 -7
View File
@@ -5,15 +5,15 @@ body:
- type: markdown
attributes:
value: |
MeshTerm is one person's spare time, so the honest answer to a good idea is often
"yes, eventually". Saying what you are actually trying to do — rather than the
feature you have in mind — often finds something that already works.
One person works on MeshTerm in his spare time. For this reason, the honest answer to a
good idea is often "yes, but not soon". Tell us what you want to do, and not the
feature that you have in mind. Then we can often find a function that already works.
- type: textarea
id: problem
attributes:
label: What are you trying to do
description: The situation you are in, not the feature. What are you trying to find out about your mesh?
label: What do you want to do
description: Describe your situation, and not the feature. What do you want to find out about your mesh?
validations:
required: true
@@ -21,10 +21,10 @@ body:
id: idea
attributes:
label: What you had in mind
description: If you have a shape in mind — a screen, a key, a subcommand — describe it.
description: If you have an idea of the form, such as a screen, a key, or a subcommand, describe it.
- type: textarea
id: today
attributes:
label: What you do today instead
description: The workaround, if there is one. This says more about how much the feature is worth than anything else.
description: Describe the way around the problem, if there is one. This tells us more than anything else about how much the feature is worth.
+15 -14
View File
@@ -1,30 +1,31 @@
## What this changes, and why
<!-- The why matters more than the what — the diff already says the what. -->
<!-- The reason is more important than the description of the change, because the diff already shows the change. -->
## Discord
<!-- Your Discord username on the MeshTerm server, and a link to the message in
#contributing where you got a yes. A pull request without them is closed without
a review. -->
<!-- Write your Discord username on the MeshTerm server. Also write a link to the message
in #contributing where you got a yes. We close a pull request without these items
and do not review it. -->
Username:
Link to the yes:
## AI use
<!-- Required. Did an AI tool write any part of this change? If yes: which tool, what it
did, and what you checked yourself. Each commit it helped with also needs a line at
the end naming it, like `Co-Authored-By:` or `Assisted-by:`. If you used AI and don't
say so, this pull request is treated as a bot's and closed. -->
<!-- This is necessary. Did an AI tool write any part of this change? If yes, write which
tool, what it did, and what you checked yourself. Each commit that it helped with also
needs a line at the end that names the tool, for example `Co-Authored-By:` or
`Assisted-by:`. If you used AI and you do not say so, we treat this pull request as a
bot's pull request and close it. -->
## Before you mark it ready
- [ ] I've read [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md) and agree its licensing terms apply to this contribution
- [ ] I've put my Discord username and a link to the yes above
- [ ] If I used AI, I said so above and in the commits it helped with
- [ ] I have read [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md) and I agree that its licensing terms apply to this contribution
- [ ] I have written my Discord username and a link to the yes above
- [ ] If I used AI, I said so above and in the commits that it helped with
- [ ] `python -m pytest -q` passes, including the dual-platform gallery gate
- [ ] `ruff check .` and `ruff format --check .` are clean
- [ ] New screens, rows, and hints go through the helpers in `ui/menus.py`, and follow the
- [ ] `ruff check .` and `ruff format --check .` have no errors
- [ ] New screens, rows, and hints use the helpers in `ui/menus.py`, and follow the
lexicon and UX standards in `CLAUDE.md`
- [ ] Anything user-visible has a line in `CHANGELOG.md` under `[Unreleased]`
- [ ] Anything that the user can see has a line in `CHANGELOG.md` under `[Unreleased]`
+80 -68
View File
@@ -1,100 +1,104 @@
# Contributing to MeshTerm
Thanks for wanting to work on MeshTerm. A few things to know before you open a pull
Thank you for your wish to work on MeshTerm. Read these notes before you open a pull
request.
## Licensing — what happens to your contribution
## Licensing: what happens to your contribution
MeshTerm is licensed under the [Apache License, Version 2.0](LICENSE). By opening a
pull request, you agree to license your contribution under those same terms — and,
going a step beyond what Apache-2.0 alone requires, you additionally grant Jean-Pierre
Martineau (the project's copyright holder) the right to relicense your contribution as
part of the MeshTerm project, should the project's license ever change in the future.
You keep copyright over what you write; you're not signing it away, just granting these
usage rights alongside it. No separate form, bot, or signature is needed — opening the
PR is how you agree.
MeshTerm is licensed under the [Apache License, Version 2.0](LICENSE). When you open a
pull request, you agree to license your contribution under the same terms. You also agree
to one more term that Apache-2.0 alone does not require. You grant Jean-Pierre Martineau
(the project's copyright holder) the right to relicense your contribution as part of
the MeshTerm project, if the project's license changes in the future.
You keep the copyright of what you write. You do not give it away. You only grant these
rights of use in addition to it. You do not need a separate form, bot, or signature. When
you open the pull request, you agree.
If that's not something you're comfortable granting, say so on the PR before it's
merged and we'll talk about it, but by default that's the deal for anything merged into
this repository.
If you are not comfortable with this grant, say so on the pull request before we merge it,
and we will talk about it. If you do not say so, these are the terms for each contribution
that we merge into this repository.
## Before you start
- Read [CLAUDE.md](CLAUDE.md). It isn't just instructions for AI assistants — it's the
actual style guide for this codebase: the terminology (node vs. contact, heard vs.
seen, etc.), UX standards for screens/dialogs/menus, and where the reusable building
blocks live in code (`ui/menus.py`, `ui/markdown.py`, `ui/widgets.py`, `ui/theme.py`).
New code should read like it always belonged here.
- Tell us before you start working on something (see below). For anything bigger than a
small fix, expect to talk it through first. That saves everyone a rewrite.
- Read [CLAUDE.md](CLAUDE.md). It is not only instructions for AI assistants. It is the
real style guide for this codebase. It gives the terms (node and contact, heard and
seen, and other terms), the UX standards for screens, dialogs, and menus, and the place
in the code of each building block that you can use again (`ui/menus.py`,
`ui/markdown.py`, `ui/widgets.py`, `ui/theme.py`). New code must look like it was always
part of the project.
- Tell us before you start work on a change (refer to the sections below). If a change is
bigger than a small correction, expect to talk about it first. This saves everyone from
a rewrite.
## Who can contribute
If you use MeshTerm, you're welcome here. Maybe something broke for you, or you wished
it could do one more thing, or a screen looked wrong on your radio. Those are exactly
the changes we want.
If you use MeshTerm, you are welcome here. Maybe something did not work for you. Maybe you
wanted one more function. Maybe a screen looked wrong on your radio. We want exactly these
changes.
### How MeshTerm was made
Some people in the MeshCore community don't trust code written by AI. After some of
what has happened lately, that's easy to understand, and it deserves a straight answer.
Some persons in the MeshCore community do not trust code that AI wrote. After some recent
events, we understand this, and it needs a direct answer.
Jean-Pierre Martineau, who started MeshTerm, sees it differently. AI was a big help on
this project. Honestly, MeshTerm couldn't have been built without it. But the choices
were his. He decided what MeshTerm should be, what it should do, and how it should
look. He uses it every day. It's handmade, with AI's help.
Jean-Pierre Martineau started MeshTerm. He has a different opinion. AI helped much with
this project. Honestly, it was not possible to build MeshTerm without it. But he made the
choices. He decided what MeshTerm must be, what it must do, and how it must look. He uses
it each day. It is handmade, with the help of AI.
The project wants to stay that way. That's what the rules below are for.
The project must stay this way. The rules below are for this purpose.
### Come say hello first
Using AI tools to help you work is fine. What matters is that a real person who cares
about MeshTerm is behind the change.
It is permitted to use AI tools to help you work. What is important is that a real person
who cares about MeshTerm is behind the change.
The MeshTerm community lives on [Discord](https://discord.gg/AZwe5Uvb3S), and everyone
is welcome to join. But please join before you change things. Before you start writing
code, say hello in the **#contributing** channel and tell us what you'd like to work on.
Then wait until we say yes. This goes for small fixes too.
The MeshTerm community is on [Discord](https://discord.gg/AZwe5Uvb3S), and everyone is
welcome to join. But join before you change anything. Before you start to write code,
say hello in the **#contributing** channel and tell us what you want to work on. Then wait
until we say yes. This is also the rule for small corrections.
When you open your pull request, include two things:
When you open your pull request, include these two items:
- **Your Discord username**, the one you use on the MeshTerm server.
- **Your Discord username.** Use the one that you use on the MeshTerm server.
- **A link to the message where we said yes.** In Discord, right-click the message and
choose "Copy Message Link."
If you haven't joined the Discord, or nobody said yes to your idea, your pull request
will be closed without a review.
If you did not join the Discord, or if nobody said yes to your idea, we will close your
pull request without a review.
### No bots
Some accounts use AI to find open issues, write a fix, and open a pull request, all on
their own. There's no person behind it who read the change or can answer questions
about it. People call this "AI slop." We close those pull requests without a review,
even if the code looks fine. The same goes for issues and bug reports written that way.
Some accounts use AI to find open issues, write a correction, and open a pull request, all
by themselves. No person read the change, and no person can answer questions about it.
People call this "AI slop." We close these pull requests without a review, also when the
code looks correct. The same rule is true for issues and bug reports that are written in
this way.
### Tell us if you used AI
This part is required. If an AI tool wrote any part of your change, say so in two
This part is necessary. If an AI tool wrote any part of your change, you must say so in two
places:
- **In each commit it helped with.** Add a line at the end of the commit message naming
the tool, like `Co-Authored-By: Claude <noreply@anthropic.com>` or
- **In each commit that it helped with.** Add a line at the end of the commit message with
the name of the tool, for example `Co-Authored-By: Claude <noreply@anthropic.com>` or
`Assisted-by: GitHub Copilot`.
- **In your pull request.** Write a sentence about what the tool did and what you
checked yourself.
- **In your pull request.** Write a sentence about what the tool did and what you checked
yourself.
If you used AI and didn't say so, we'll treat your pull request as a bot's. Saying so
costs you nothing. Either way, you're the author, so be ready to explain your change.
If you used AI and you did not say so, we will treat your pull request as a bot's pull
request. It costs you nothing to say so. In all cases, you are the author, so be ready to
explain your change.
## Making a change
1. Fork the repo and branch off `main`.
1. Fork the repository and make a branch from `main`.
2. Set up a dev install:
```bash
pip install -e ".[dev]"
git config blame.ignoreRevsFile .git-blame-ignore-revs # skip the formatting sweeps
```
3. Before opening the PR, make sure these all pass:
3. Before you open the pull request, make sure that all these checks pass:
```bash
python -m pytest -q # tests, including tests/test_gallery.py — the dual-platform
# readability gate (screens must stay readable at 72 cols
@@ -102,29 +106,37 @@ costs you nothing. Either way, you're the author, so be ready to explain your ch
ruff check . # lint
ruff format --check . # formatting
```
4. Keep commits focused; write imperative-mood messages and explain *why* when it isn't
obvious from the diff.
5. Open the PR against `main` with a clear description of what changed and why.
The tests include `tests/test_writing_style.py`. It checks the docstrings and the
comments for the marks that ASD-STE100 does not use. Refer to
[How we write](docs/development/writing-style.md).
4. Keep each commit to one subject. Write the commit message in the imperative mood. Explain
*why* when the diff does not show the reason.
5. Open the pull request against `main`. Write a clear description of what changed and why.
## Code style
- Match the surrounding file: naming, comment density, idiom.
- Follow the lexicon and UX standards in [CLAUDE.md](CLAUDE.md) — one term per concept;
don't introduce a synonym for something that already has a name.
- New screens, dialogs, and rows go through the existing helpers rather than hand-rolled
layout — CLAUDE.md names the enforcement points.
- Make your change match the file around it: the names, the amount of comments, and the
idioms.
- Follow the terms and the UX standards in [CLAUDE.md](CLAUDE.md). Use one term for each
concept. Do not add a synonym for something that already has a name.
- Build new screens, dialogs, and rows with the existing helpers. Do not make the layout by
hand. CLAUDE.md names the places where the code enforces these standards.
- Write docstrings, comments, and documents in ASD-STE100 Simplified Technical English,
with Canadian spelling. [How we write](docs/development/writing-style.md) gives the
rules and the glossary.
## Reporting bugs / requesting features
Open a GitHub issue. For bugs, include what you expected, what happened, and enough to
reproduce it — device/platform, MeshCore firmware version if relevant, and steps.
Open a GitHub issue. For a bug, include what you expected, what happened, and enough
information for us to reproduce it. This is the device and platform, the MeshCore firmware
version if it is relevant, and the steps.
The [bug form](https://github.com/jpmartineau/MeshTerm/issues/new?template=bug.yml) asks
for all of this. Attach the file that `meshterm diagnostics --out bug.md` writes. No
GitHub account? Post it in the #bugs forum on [Discord](https://discord.gg/AZwe5Uvb3S)
instead.
for all of this. Attach the file that `meshterm diagnostics --out bug.md` writes. If you do
not have a GitHub account, post the report in the #bugs forum on
[Discord](https://discord.gg/AZwe5Uvb3S) instead.
---
Questions about any of this, licensing included, are welcome as a GitHub issue before you
put work in.
You can ask a question about any of this, and this includes licensing, as a GitHub issue
before you start work.
+192 -173
View File
@@ -1,17 +1,17 @@
# MeshTerm
**A full-featured TUI [MeshCore](https://meshcore.io/) client for your terminal** — tune
your radio, chat across the mesh, watch the network live, and keep a longitudinal record
of everything you overhear, all from a connected serial, Bluetooth, or network (TCP)
companion.
**A full-featured TUI [MeshCore](https://meshcore.io/) client for your terminal.** Use it
to tune your radio, chat across the mesh, and watch the network live. It also keeps a
long-term record of everything that you overhear. It works with a companion that is
connected by serial, Bluetooth, or network (TCP).
MeshTerm is interactive by default: a modern, keyboard-driven TUI built on
[Rich](https://github.com/Textualize/rich) and
[prompt_toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit). It is also
fully scriptable — **most screens have a mirror `meshterm` subcommand** — so the same
capabilities drive both an evening of exploring the mesh and a cron job. Every packet the
radio overhears is recorded to a local SQLite database, so the longer you run it, the more
your mesh's history is worth.
MeshTerm is interactive by default. It is a modern TUI that you operate with the keyboard,
built on [Rich](https://github.com/Textualize/rich) and
[prompt_toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit). You can also
use it in scripts. **Most screens have a mirror `meshterm` subcommand.** Thus the same
functions work for an evening of exploration on the mesh and for a cron job. MeshTerm
stores each packet that the radio overhears in a local SQLite database. The longer you
run MeshTerm, the more the history of your mesh is worth.
<p>
<a href="https://github.com/jpmartineau/MeshTerm/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/jpmartineau/MeshTerm/actions/workflows/ci.yml/badge.svg"></a>
@@ -23,69 +23,74 @@ your mesh's history is worth.
<a href="https://discord.gg/AZwe5Uvb3S"><img alt="Discord" src="https://img.shields.io/badge/chat-Discord-5865F2"></a>
</p>
> MeshTerm is a side project, run by one person. Bug reports are very welcome. Want to
> change something? Say hello on [Discord](https://discord.gg/AZwe5Uvb3S) first.
> [More on how it's run](#how-this-project-is-run).
> MeshTerm is a side project, and one person runs it. We welcome bug reports. If you want to
> change something, first say hello on [Discord](https://discord.gg/AZwe5Uvb3S).
> [Read more about how the project is run](#how-this-project-is-run).
https://github.com/user-attachments/assets/f91a6695-14d5-4ae0-8fdc-e65492d0955d
<p align="center"><sub>Under two minutes on a real mesh.</sub></p>
<p align="center"><sub>A video of less than two minutes, made on a real mesh.</sub></p>
<p align="center">
<img src="docs/screenshots/dashboard.png" alt="The dashboard: a packets-per-minute timeline over the last two hours, and every packet class counted" width="49%">
<img src="docs/screenshots/map.png" alt="The map: repeaters named over an OpenStreetMap basemap of the Lachine Canal" width="49%">
<img src="docs/screenshots/dashboard.png" alt="The dashboard: a timeline of the packets for each minute over the last two hours, and a count for each class of packet" width="49%">
<img src="docs/screenshots/map.png" alt="The map: repeaters with their names, on an OpenStreetMap basemap of the Lachine Canal" width="49%">
</p>
<p align="center">
<img src="docs/screenshots/trace.png" alt="A trace: the route it walked drawn as chips, then the SNR of every hop" width="49%">
<img src="docs/screenshots/path.png" alt="Message paths: every route a channel message was heard over, drawn on the ground" width="49%">
<img src="docs/screenshots/trace.png" alt="A trace: the route that the trace went through, drawn as chips, then the SNR of each hop" width="49%">
<img src="docs/screenshots/path.png" alt="Message paths: each route over which a channel message was heard, drawn on the map" width="49%">
</p>
<p align="center">
<img src="docs/screenshots/picocalc.png" alt="MeshTerm on the PicoCalc, a 53-column console with a radio wired inside" width="38%">
<img src="docs/screenshots/picocalc.png" alt="MeshTerm on the PicoCalc, a console that is 53 columns wide, with a radio installed inside it" width="38%">
</p>
<p align="center"><sub>The dashboard, the map, a trace, the paths a message took — and the whole thing on a PicoCalc.</sub></p>
<p align="center"><sub>The dashboard, the map, a trace, the paths of a message, and the whole app on a PicoCalc.</sub></p>
---
## Why MeshTerm
- **One tool, two faces.** Launch `meshterm` for a full-screen menu; pass a subcommand for
a scripted one-shot. The registry that builds the menu builds the CLI, so they never
drift apart.
- **It remembers.** A passive monitor logs every overheard advert and telemetry frame —
SNR, RSSI, shared location — to SQLite from the moment the radio opens. Maps, contact lists,
charts, and the Time Machine all read back that history.
- **It measures.** Live trace with per-hop reliability, a coarse→refine→verify TX-power
sweep, and a walkable graph of how the mesh actually hangs together.
- **It talks.** Live channel and direct messaging, room servers' message boards,
store-and-forward courier delivery for contacts that aren't there yet, and shareable
channels as QR codes and `meshcore://` links.
- **It connects however your companion does.** Serial (USB), Bluetooth LE, or TCP (a Wi-Fi
board or a network proxy) — the discoverable transports are auto-found and picked
interactively, a network device is named by hand, and every kind reconnects live when a link
drops. It pairs a PIN-protected Bluetooth companion itself on Windows and Linux, and when a
connection fails it says which step failed and what to do. No radio? A built-in simulator
covers development.
- **It works offline.** The street map caches OpenStreetMap tiles to disk and falls back to
a blank grid when there's no network — it never needs the internet to run.
- **One tool, two faces.** To open a full-screen menu, start `meshterm`. To run one scripted
task, give a subcommand. The same registry builds the menu and the CLI, so they do not
become different from one another.
- **It remembers.** A passive monitor stores each advert and each telemetry packet that it
overhears. This includes the SNR, the RSSI, and the shared position. The monitor starts
when the radio opens. The maps, the contact lists, the charts, and the Time Machine all
read this history.
- **It measures.** It has a live trace with the reliability of each hop. It has a TX-power
sweep that goes from coarse to fine and then verifies the result. It also has a graph
that you can walk through, which shows how the mesh is really connected.
- **It talks.** You can send live messages on a channel and direct messages. You can use the
message boards of room servers. A courier stores messages and sends them when a contact
is not yet there. You can share a channel as a QR code or as a `meshcore://` link.
- **It connects in the same way as your companion.** It supports serial (USB), Bluetooth
LE, and TCP (a Wi-Fi board or a network proxy). It finds the serial and Bluetooth
devices automatically, and you select one from a list. You type the name of a network
device. MeshTerm reconnects live when a link of any kind drops. On Windows and Linux,
MeshTerm pairs with a Bluetooth companion that has a PIN. When a connection fails,
MeshTerm says which step failed and what to do. If you have no radio, a built-in
simulator is available for development.
- **It works offline.** The street map caches OpenStreetMap tiles on disk. When there is no
network, it shows a blank grid. MeshTerm never needs the internet to run.
## Install
### Download a build
One file, no Python needed — Windows, macOS (Intel and Apple silicon), and Linux (x64 and
ARM64, so the uConsole is covered). Every command below fetches the
A build is one file. You do not need Python. Builds are available for Windows, macOS (Intel
and Apple silicon), and Linux (x64 and ARM64, so the uConsole is included). Each command
below gets the
**[⬇ latest release](https://github.com/jpmartineau/MeshTerm/releases/latest)**, whichever
one that is, so none of them goes stale.
release that is. Thus the commands do not become old.
MeshTerm is a terminal program, so **open a terminal and run it from there.** You can
double-click it and it will work, but you'll get whatever console your system picks, and
if anything goes wrong at startup the window closes before you can read why.
double-click the file and it will work. But then your system selects the console, and you
cannot choose it. If an error occurs at startup, the window closes before you can read the
error.
#### macOS — download it with `curl`
#### macOS: download it with `curl`
**Don't use your browser.** A browser flags the file as downloaded, and macOS then refuses
to open it at all — on Sequoia and later, right-click → Open no longer gets you past that
either. `curl` flags nothing, so what it hands you simply runs. Open Terminal and paste:
**Do not use your browser.** A browser marks the file as downloaded, and then macOS does not
let you open it. On Sequoia and later, right-click → Open does not help. `curl` does not
mark the file, so the file runs. Open Terminal and paste:
```bash
curl -fL -o meshterm https://github.com/jpmartineau/MeshTerm/releases/latest/download/meshterm-macos-arm64
@@ -93,13 +98,13 @@ chmod +x meshterm
./meshterm
```
That's the build for Apple silicon Macs (M1 and later). On an **Intel** Mac, swap `arm64`
for `x64`. And if you already downloaded it with a browser, you don't have to start over:
`xattr -d com.apple.quarantine meshterm` clears the flag.
This is the build for Apple silicon Macs (M1 and later). On an **Intel** Mac, replace
`arm64` with `x64`. If you already downloaded the file with a browser, you do not have to
start again. Run `xattr -d com.apple.quarantine meshterm` to clear the mark.
#### Linux — the same three lines
#### Linux: the same three lines
`linux-x64` on a PC, `linux-arm64` on a uConsole or another 64-bit ARM handheld.
Use `linux-x64` on a PC. Use `linux-arm64` on a uConsole or another 64-bit ARM handheld.
```bash
curl -fL -o meshterm https://github.com/jpmartineau/MeshTerm/releases/latest/download/meshterm-linux-x64
@@ -110,52 +115,54 @@ chmod +x meshterm
#### Windows
Download
**[meshterm-windows-x64.exe](https://github.com/jpmartineau/MeshTerm/releases/latest/download/meshterm-windows-x64.exe)**,
then open Windows Terminal or PowerShell, `cd` to your downloads, and run it:
**[meshterm-windows-x64.exe](https://github.com/jpmartineau/MeshTerm/releases/latest/download/meshterm-windows-x64.exe)**.
Then open Windows Terminal or PowerShell, use `cd` to go to your downloads folder, and run
the file:
```powershell
.\meshterm-windows-x64.exe
```
Windows says *"Windows protected your PC"* the first time. Click **More info → Run anyway**.
The first time, Windows says *"Windows protected your PC"*. Click **More info → Run anyway**.
Whichever you took, you now have a single file: `meshterm` on macOS and Linux,
`meshterm-windows-x64.exe` on Windows. Put it somewhere on your `PATH` (renamed to
`meshterm.exe` on Windows) and it's just `meshterm` from anywhere.
After these steps, you have one file: `meshterm` on macOS and Linux, or
`meshterm-windows-x64.exe` on Windows. Put the file in a folder that is in your `PATH`. On
Windows, rename it to `meshterm.exe`. Then you can run `meshterm` from any folder.
> **These builds aren't code-signed yet**, which is why macOS and Windows both push back
> the first time. **Windows signing is on its way.** MeshTerm has applied to the
> **These builds are not code-signed yet.** For this reason, macOS and Windows both show a
> warning the first time. **Windows signing will come.** MeshTerm applied to the
> [SignPath Foundation](https://signpath.org), which signs open-source projects for free.
> Once that's set up, every Windows download will be signed. The
> [code signing policy](#code-signing-policy) below explains how. Mac builds will stay
> unsigned, because Apple charges for signing and this is a free side project.
> Every release ships a `SHA256SUMS` file if you'd rather check what you got, and
> [installing with pip or pipx](#or-install-with-pip) sidesteps the whole business, since
> nothing arrives as a downloaded binary.
> After the setup is complete, each Windows download will be signed. The
> [code signing policy](#code-signing-policy) below gives the details. The Mac builds will
> stay unsigned, because Apple charges a fee for signing and this is a free side project.
> Each release has a `SHA256SUMS` file. Use it to check the file that you got.
> [Installing with pip or pipx](#or-install-with-pip) avoids the problem, because you do not
> download a binary.
>
> Each release page also carries these commands written out for **that exact version**,
> next to its own downloads.
> The page of each release also has these commands, written for **that exact version**, next
> to its own downloads.
### If your terminal draws empty boxes
MeshTerm is drawn with emoji icons, braille charts, and powerline path chips, and whether
you see them is up to your *terminal* rather than your font. Windows Terminal, VS Code's
terminal, and anything modern on macOS and Linux all draw them correctly.
MeshTerm draws emoji icons, braille charts, and powerline path chips. Your *terminal*
decides if you see them, not your font. Windows Terminal, the terminal of VS Code, and each
modern terminal on macOS and Linux draw them correctly.
The classic Windows console — the black `cmd.exe` window you get from a double-click —
does not. So when MeshTerm lands there it **moves to Windows Terminal**, says so, and
opens there; nothing is installed and nothing is changed. Where there's no Windows
Terminal to move to, it offers the charts and marks without the icons instead, and will
install Microsoft's [Cascadia Mono PL](https://github.com/microsoft/cascadia-code) just
for you — no administrator rights, nothing downloaded.
The classic Windows console does not draw them. This is the black `cmd.exe` window that
opens when you double-click the file. When MeshTerm starts there, it **moves to Windows
Terminal**, tells you, and opens there. It does not install anything and does not change
anything. If Windows Terminal is not available, MeshTerm offers the charts and marks without
the icons. It also installs Microsoft's [Cascadia Mono PL](https://github.com/microsoft/cascadia-code)
for you. This needs no administrator rights, and it downloads nothing.
Preferences → Display → Console setup turns all of this off if you'd rather stay put.
[Terminals, icons, and the Windows console](docs/guide/terminals.md) explains why any of it is
necessary.
To stay in the same console, turn off these actions in Preferences → Display → Console
setup. [Terminals, icons, and the Windows console](docs/guide/terminals.md) explains why
they are necessary.
> **Trying a build without touching your real data.** MeshTerm keeps everything in
> `~/.meshterm` — your history database, contacts, channel keys — and *every* copy of
> MeshTerm uses that same folder. Set `MESHTERM_HOME` to try one in isolation:
> **Try a build without a change to your real data.** MeshTerm keeps all its data in
> `~/.meshterm`. This includes your history database, your contacts, and your channel
> keys. *Each* copy of MeshTerm uses this same folder. To try a copy in isolation, set
> `MESHTERM_HOME`:
>
> ```bash
> MESHTERM_HOME=~/meshterm-test ./meshterm # macOS, Linux
@@ -164,53 +171,54 @@ necessary.
> $env:MESHTERM_HOME = "$HOME\meshterm-test"; .\meshterm-windows-x64.exe
> ```
>
> Same variable if you run two radios and want them kept apart.
> Use the same variable if you run two radios and you want to keep them separate.
### Or install with pip
You need **Python 3.10 or newer**. That's the only requirement — MeshTerm pulls in
everything else itself.
You need **Python 3.10 or newer**. This is the only requirement. MeshTerm installs all
other parts by itself.
```bash
pip install git+https://github.com/jpmartineau/MeshTerm
meshterm
```
`meshterm` opens the full-screen menu. That's the whole install.
`meshterm` opens the full-screen menu. This is the complete installation.
**Rather not touch your system Python?** [pipx](https://pipx.pypa.io/) puts the app in its
own private environment and still gives you a plain `meshterm` command:
**Do you not want to change your system Python?** [pipx](https://pipx.pypa.io/) puts the
app in its own private environment. It also gives you a normal `meshterm` command:
```bash
pipx install git+https://github.com/jpmartineau/MeshTerm
```
**No radio yet?** `meshterm --mock` runs the entire app against a simulated mesh, so you
can have a look around before you buy anything.
**Do you not have a radio yet?** `meshterm --mock` runs the complete app on a simulated
mesh. You can look at the app before you buy anything.
### Coming
**`pip install mesh-term`**, once the package is published. It isn't yet — the name
question is still being sorted out ([why](https://github.com/pypi/support/issues/12162)).
**`pip install mesh-term`** will work after the package is published. It is not published
yet, because the question of the name is not resolved
([read why](https://github.com/pypi/support/issues/12162)).
Setting up for development instead? That's in [CONTRIBUTING.md](CONTRIBUTING.md).
To set up a development environment, refer to [CONTRIBUTING.md](CONTRIBUTING.md).
## Documentation
**[The documentation index](docs/README.md)** lists everything, including the code survey
kept as a historical record. The short version:
**[The documentation index](docs/README.md)** lists all the documents. This includes the
code survey, which we keep as a historical record. This table is a short version:
| Read | For |
| --- | --- |
| **[The command line](docs/cli/README.md)** | Every subcommand and option, what each prints on the plain and JSON faces, the exit statuses, recipes. |
| **[What MeshTerm does](docs/guide/features.md)** | Every screen the menu offers, and the command that does the same job without it. |
| [The CLI cookbook](docs/cli/cookbook.md) | Common one-liners, by the thing you're trying to do. |
| [Configuring MeshTerm](docs/guide/configuration.md) | Preferences, device profiles, and everything kept under `~/.meshterm`. |
| [When a companion won't connect](docs/guide/connecting.md) | Bluetooth pairing on each system, finding the PIN, USB permissions, and every connection error, with what to do about it. |
| [MeshTerm on hardware](docs/devices/README.md) | Which handheld manual is yours — the [PicoCalc](docs/devices/picocalc-lyra.md) build, or the [uConsole](docs/devices/uconsole.md), whose LoRa chip MeshTerm can drive directly. The [Cardputer Zero](docs/devices/cardputer-zero.md) is **not supported yet**. |
| [How the code is laid out](docs/development/architecture.md) | The layering, and where a new feature goes. |
| **[The command line](docs/cli/README.md)** | Each subcommand and option, what each prints on the plain face and on the JSON face, the exit statuses, and recipes. |
| **[What MeshTerm does](docs/guide/features.md)** | Each screen that the menu has, and the command that does the same job without the menu. |
| [The CLI cookbook](docs/cli/cookbook.md) | Common one-line commands, in groups by the task that you want to do. |
| [Configuring MeshTerm](docs/guide/configuration.md) | The preferences, the device profiles, and all the files that MeshTerm keeps in `~/.meshterm`. |
| [When a companion does not connect](docs/guide/connecting.md) | Bluetooth pairing on each system, how to find the PIN, USB permissions, and each connection error, with the action to take. |
| [MeshTerm on hardware](docs/devices/README.md) | Which handheld manual is the correct one for you: the [PicoCalc](docs/devices/picocalc-lyra.md) build, or the [uConsole](docs/devices/uconsole.md), whose LoRa chip MeshTerm can control directly. The [Cardputer Zero](docs/devices/cardputer-zero.md) is **not supported yet**. |
| [How the code is laid out](docs/development/architecture.md) | The layers of the code, and where to put a new feature. |
| [CONTRIBUTING.md](CONTRIBUTING.md) | The development setup and the house rules. |
| [CHANGELOG.md](CHANGELOG.md) | What changed, newest first. |
| [CHANGELOG.md](CHANGELOG.md) | What changed, with the newest change first. |
## Quick start
@@ -237,40 +245,43 @@ meshterm --mock --platform picocalc-lyra
meshterm specimen
```
Putting MeshTerm on a handheld has its own manuals. **[MeshTerm on the
PicoCalc](docs/devices/picocalc-lyra.md)** takes a stock PicoCalc from the shopping list to a
Linux handheld with a LoRa radio soldered inside; **[MeshTerm on the
uConsole](docs/devices/uconsole.md)** drives a uConsole's SPI LoRa board directly with
`--spi`, or through a small bridge if you want the node on the mesh all the time. M5Stack's
Cardputer Zero has a page too, but it is **[not supported yet](docs/devices/cardputer-zero.md)**.
The device-side scripts the manuals run are in [`scripts/`](scripts/); not sure which
manual is yours? [`docs/devices/README.md`](docs/devices/README.md) says.
MeshTerm has its own manuals for handhelds. **[MeshTerm on the
PicoCalc](docs/devices/picocalc-lyra.md)** goes from the shopping list for a stock PicoCalc
to a Linux handheld that has a LoRa radio soldered inside. **[MeshTerm on the
uConsole](docs/devices/uconsole.md)** shows how to control the SPI LoRa board of a uConsole
directly with `--spi`. It also shows how to use a small bridge, if you want the node to be
on the mesh at all times. M5Stack's Cardputer Zero also has a page, but it is **[not
supported yet](docs/devices/cardputer-zero.md)**. The scripts that the manuals run on the
device are in [`scripts/`](scripts/). If you do not know which manual is correct for you,
refer to [`docs/devices/README.md`](docs/devices/README.md).
A companion that won't connect? **[When a companion won't
connect](docs/guide/connecting.md)** covers Bluetooth pairing on Windows, Linux, and macOS, the
`dialout` group and ModemManager for USB on Linux, and every error message MeshTerm gives.
Does a companion not connect? **[When a companion does not
connect](docs/guide/connecting.md)** explains Bluetooth pairing on Windows, Linux, and
macOS. It also explains the `dialout` group and ModemManager for USB on Linux, and each
error message that MeshTerm gives.
## What it does
The menu asks one question — *what would you like to do?* — and answers it in two halves.
Three sections name a doing: **Message** is chat, channels, the courier outbox, and
contacts; **Watch** is the dashboard, the live feed, the watchtower, and the time machine;
**Explore** is the map, the mesh walk, tracing, and the trophy case. Three name whose it is:
**This node** is the radio in your hand, **Other nodes** is someone else's over the mesh,
and **This app** is MeshTerm itself — preferences, diagnostics, and the written pages.
The menu asks one question: *what would you like to do?* It gives the answer in two halves.
Three sections name an activity. **Message** has chat, channels, the courier outbox, and
contacts. **Watch** has the dashboard, the live feed, the watchtower, and the time machine.
**Explore** has the map, the mesh walk, tracing, and the trophy case. The other three
sections name the owner. **This node** is the radio in your hand. **Other nodes** is the
someone else's radio, which you reach over the mesh. **This app** is MeshTerm itself, with
the preferences, the diagnostics, and the written pages.
Nearly every one of them has a mirror `meshterm` subcommand — the same registry builds
the menu and the CLI, so they can't drift apart. The live screens, such as the map and
the dashboard, are only in the menu.
Almost all of these have a mirror `meshterm` subcommand. The same registry builds the menu
and the CLI, so they do not become different from one another. The live screens, such as the
map and the dashboard, are only in the menu.
**[What MeshTerm does](docs/guide/features.md)** is the full catalogue: every screen, what it
does, and its scripted equivalent where one exists.
**[What MeshTerm does](docs/guide/features.md)** is the complete catalogue. It shows each
screen, what the screen does, and the scripted equivalent where there is one.
## Scripting it
Nearly every command speaks `--json`, and the global options — `--profile/-p`, `--port`,
`--ble`, `--ble-pin`, `--tcp`, `--spi`, `--mock`, `--db`, `--json`, `--absolute`, `--quiet/-q`,
`--platform` — may be typed before or after the subcommand.
Almost every command accepts `--json`. The global options are `--profile/-p`, `--port`,
`--ble`, `--ble-pin`, `--tcp`, `--spi`, `--mock`, `--db`, `--json`, `--absolute`,
`--quiet/-q`, and `--platform`. You can type them before or after the subcommand.
```bash
# Every repeater's public key, for a script
@@ -288,44 +299,46 @@ meshterm config set radio_sf 9
meshterm monitor --seconds 60
```
**[The command line](docs/cli/README.md)** is the manual — every command and option, what each
prints on both faces, and the exit statuses.
**[The CLI cookbook](docs/cli/cookbook.md)** has more one-liners like these.
**[The command line](docs/cli/README.md)** is the manual. It shows each command and option,
what each prints on both faces, and the exit statuses.
**[The CLI cookbook](docs/cli/cookbook.md)** has more one-line commands like these.
## How this project is run
MeshTerm is one person working evenings and weekends. I'd rather tell you that up front
than have you guess from how long things take.
One person works on MeshTerm, in the evenings and at weekends. I want to tell you this at
the start, so that you do not have to guess from the time that things take.
**Issues are welcome — all of them.** Bugs, questions, "is this supposed to do that". The
[bug form](https://github.com/jpmartineau/MeshTerm/issues/new?template=bug.yml) asks for a
fair bit, and that's on purpose: how well a problem is described really does decide whether
I can do anything with it. If I can reproduce it, I'll usually chase it. If I can't, I'm
mostly guessing.
**We welcome all issues.** This includes bugs, questions, and "is this a fault or is it
correct". The
[bug form](https://github.com/jpmartineau/MeshTerm/issues/new?template=bug.yml) asks for
much information. This is intentional. The quality of the description of a problem
decides if I can do something about it. If I can reproduce a problem, I will usually work
on it. If I cannot reproduce it, I can only guess.
**Ask before you write a pull request.** Join the [Discord](https://discord.gg/AZwe5Uvb3S)
and say hello in `#contributing`. Tell us what you'd like to change. Then wait for a yes.
That goes for small fixes too. I'm not being precious. MeshTerm has firm house rules
about how screens get built (they're in [CLAUDE.md](CLAUDE.md)), and I'd hate for you to
spend a weekend on something I then ask you to rewrite. A quick conversation first saves
us both. AI tools are fine, as long as you say you used them.
[CONTRIBUTING.md](CONTRIBUTING.md) has the details.
and say hello in `#contributing`. Tell us what you want to change. Then wait for a yes.
This applies to small corrections also. This is not because I am too careful. MeshTerm has
firm house rules for how to build screens (they are in [CLAUDE.md](CLAUDE.md)). I do not
want you to spend a weekend on a change that I then ask you to write again. A short
conversation at the start saves time for both of us. You can use AI tools, but you must say
that you used them. [CONTRIBUTING.md](CONTRIBUTING.md) has the details.
**I can't promise timelines.** Some things get fixed the same night. Some sit for a month
because life happened. If your issue goes quiet, please give it a nudge. That helps me.
**I cannot promise a time.** Some problems are corrected the same night. Some wait for a
month, because I had other tasks in my life. If your issue is quiet for a long time, please
remind me. This helps me.
**Where to report things.** Either works:
**Where to report a problem.** You can use one of these two places:
- **[GitHub issues](https://github.com/jpmartineau/MeshTerm/issues)** if you have an
account. This is where things get tracked and fixed, so it's the shortest path.
- **[Discord](https://discord.gg/AZwe5Uvb3S)** if you don't, or if you're not sure it's a
bug yet. Ask in the support forum, or drop reproducible defects in `#bugs`. I move the
real ones over to GitHub myself.
- **[GitHub issues](https://github.com/jpmartineau/MeshTerm/issues)**, if you have an
account. We track and correct problems here, so this is the shortest path.
- **[Discord](https://discord.gg/AZwe5Uvb3S)**, if you do not have an account, or if you
are not sure that the problem is a bug. Ask in the support forum, or put defects that you
can reproduce in `#bugs`. I move the real bugs to GitHub myself.
Don't worry about picking the wrong one. Getting told about a problem beats filing it
tidily.
Do not worry about the choice of the wrong place. It is better that you tell us about a
problem than that you file it in a tidy way.
Also here: [CONTRIBUTING.md](CONTRIBUTING.md) ·
More documents: [CONTRIBUTING.md](CONTRIBUTING.md) ·
[SECURITY.md](SECURITY.md) ·
[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) ·
[CHANGELOG.md](CHANGELOG.md)
@@ -335,29 +348,33 @@ Also here: [CONTRIBUTING.md](CONTRIBUTING.md) ·
Free code signing provided by [SignPath.io](https://signpath.io), certificate by
[SignPath Foundation](https://signpath.org).
This is for the Windows download. It's being set up now, so releases up to and including
0.10.2 are still unsigned.
This policy is for the Windows download. The setup is in progress, so the releases up to
and including 0.10.2 are still unsigned.
**How a build gets signed.** Every Windows build is made by GitHub Actions, straight from
the public source code in this repository. SignPath only signs a file it can trace back to
one of those builds, and only after the maintainer approves that release. A file built on
anyone's own computer can't be signed this way, not even the maintainer's.
**How a build is signed.** GitHub Actions makes each Windows build, directly from the public
source code in this repository. SignPath signs only a file that it can trace to one of
these builds, and only after the maintainer approves that release. This process cannot sign
a file that was built on any person's own computer, and this includes the maintainer's
computer.
**Who does what.** MeshTerm has one maintainer,
[Jean-Pierre Martineau](https://github.com/jpmartineau), who holds all three roles:
[Jean-Pierre Martineau](https://github.com/jpmartineau), who has all three roles:
- **Author:** writes the code and commits it.
- **Reviewer:** reviews every change from anyone else before it goes in.
- **Approver:** approves each release before it gets signed.
- **Reviewer:** reviews each change from anyone else before it goes in.
- **Approver:** approves each release before it is signed.
**Privacy.** This program will not transfer any information to other networked systems
unless specifically requested by the user or the person installing or operating it.
In practice, MeshTerm goes online for one thing: map tiles. It downloads them from
[OpenFreeMap](https://openfreemap.org/) when you open a screen that shows a map, and
OpenFreeMap's [privacy policy](https://openfreemap.org/privacy) covers those downloads. It
collects nothing about you and doesn't check for updates. Over the radio, it sends only
what you ask it to send.
In practice, MeshTerm goes online for only one purpose: map tiles. It downloads them from
[OpenFreeMap](https://openfreemap.org/) when you open a screen that shows a map. The
[privacy policy](https://openfreemap.org/privacy) of OpenFreeMap applies to these
downloads. MeshTerm collects no information about you, and it does not check for updates.
Over the radio, MeshTerm sends only what you tell it to send.
<!-- ste: off -->
<!-- Legal text: attribution and licence. It is not in ASD-STE100, on purpose. -->
## Acknowledgements & attribution
@@ -429,4 +446,6 @@ Each standalone build is a single file with `LICENSE`, `NOTICE`, and a generated
`THIRD-PARTY-NOTICES.txt` (every dependency's own license text) bundled inside it. The same
three files are also attached to each release on the
[releases page](https://github.com/jpmartineau/MeshTerm/releases), so you can read them
without running anything.
without running anything.
<!-- ste: on -->
+37 -37
View File
@@ -2,61 +2,61 @@
## Reporting a vulnerability
Use GitHub's private vulnerability reporting: go to the **Security** tab of this
repository and choose **Report a vulnerability**. That opens a private thread visible only
to you and the maintainer, so nothing is disclosed while it is being fixed.
Use the private vulnerability reporting of GitHub. Go to the **Security** tab of this
repository and choose **Report a vulnerability**. This opens a private thread. Only you and
the maintainer can see it, so nothing is disclosed while we correct the problem.
If that is unavailable to you for any reason, email <johnputer@meshterm.net> instead.
If you cannot use this for any reason, send an email to <johnputer@meshterm.net> instead.
**Please do not open a public issue for a security problem.**
**Do not open a public issue for a security problem.**
## What to expect
MeshTerm is maintained by one person in his spare time, so this promises only what can
actually be kept:
One person maintains MeshTerm in his spare time. For this reason, this policy promises only
what he can keep:
- **An acknowledgement within a week.** Not a fix within a week — an answer saying the
report was read and what happens next.
- **A fix or a decision within 90 days** for anything confirmed. If it will take longer,
you will be told why.
- **Credit in the release notes**, unless you would rather not be named.
- **An acknowledgement within a week.** This is not a correction within a week. It is an
answer that says that he read the report and what happens next.
- **A correction or a decision within 90 days** for each confirmed problem. If it will take
more time, you will be told why.
- **Credit in the release notes**, unless you prefer that we do not use your name.
A slow reply is not a dismissal. If a week passes with silence, a nudge on the same thread
is welcome.
A slow reply is not a rejection. If you hear nothing for a week, you are welcome to remind
us on the same thread.
## Supported versions
While the major version is `0`, only the **latest release** is supported. There are no
backports; a fix ships in the next version.
While the major version is `0`, only the **latest release** is supported. We do not make
backports. A correction is in the next version.
## Scope
In scope — anything in this repository:
In scope: anything in this repository.
- Code execution, path traversal, or file overwrite triggered by data MeshTerm reads:
packets overheard from the mesh, a companion's responses, a config or preferences file,
a downloaded map tile.
- Anything that discloses a **channel key**, a **repeater admin password**, or a node's
private key material — including into the local database, a log, an export, or the
screen when it should not be there.
- Code execution, path traversal, or file overwrite that data which MeshTerm reads can
cause. This data includes packets overheard from the mesh, the responses of a companion,
a config file or a preferences file, and a downloaded map tile.
- Anything that discloses a **channel key**, a **repeater admin password**, or the private
key material of a node. This includes disclosure into the local database, a log, an
export, or the screen, when the material must not be there.
- Anything that lets a crafted packet corrupt or delete the observation database.
- Weaknesses in how MeshTerm stores credentials on disk.
Out of scope, though still worth telling us about somewhere:
Out of scope, but we still want you to tell us somewhere:
- Vulnerabilities in the **MeshCore firmware** or in the companion device itself. Those
belong upstream at the MeshCore project.
- Vulnerabilities in the `meshcore` Python library, `bleak`, or any other dependency —
report those to their maintainers. If a MeshTerm change can mitigate one, we want to
know.
- The fact that anyone within radio range can hear a public channel. That is how a mesh
works, not a defect.
- Anything requiring an attacker to already have write access to your filesystem or your
companion device.
- Vulnerabilities in the **MeshCore firmware** or in the companion device itself. These
belong upstream, with the MeshCore project.
- Vulnerabilities in the `meshcore` Python library, `bleak`, or any other dependency.
Report these to their maintainers. If a change in MeshTerm can reduce the risk of one,
we want to know.
- The fact that everyone within radio range can hear a public channel. This is how a
mesh works. It is not a defect.
- Anything that needs an attacker who already has write access to your filesystem or to
your companion device.
## A note on what MeshTerm handles
MeshTerm decrypts channel traffic using keys **you** give it, and it records everything it
overhears to a local SQLite database. That database is not encrypted. Treat it as you would
any file holding your mesh's history, and remember that a backup of it carries the same
weight.
MeshTerm decrypts channel traffic with keys that **you** give it. It stores everything that
it overhears in a local SQLite database. This database is not encrypted. Treat it in the
same way as each file that holds the history of your mesh. A backup of the database has the
same importance.
+43 -37
View File
@@ -1,28 +1,32 @@
# MeshTerm configuration: where things live, and which device to talk to.
# Copy to ~/.meshterm/config.toml (or pass --db / --port on the CLI).
# MeshTerm configuration: where things are, and which device to talk to.
# Copy this file to ~/.meshterm/config.toml (or give --db / --port on the CLI).
#
# How MeshTerm itself behaves — transmit cooldown, message retries, how much history it
# keeps — is *not* here. Those are preferences: they live in ~/.meshterm/preferences.toml,
# and are edited on the Preferences page (or with `meshterm preferences set <key> <value>`).
# Every one has a built-in default, so that file only ever lists what you have changed.
# This file does not have the settings for how MeshTerm itself behaves. These are the
# transmit cooldown, the message retries, and how much history MeshTerm keeps. They are
# preferences. They are in ~/.meshterm/preferences.toml, and you edit them on the
# Preferences page (or with `meshterm preferences set <key> <value>`).
# Each preference has a built-in default, so that file lists only what you changed.
#
# Profiles are optional. With no profile, no --port/--ble/--tcp, MeshTerm discovers attached
# serial devices and nearby Bluetooth companions, lets you pick one (interactively) or uses the
# single one present, and remembers the last device that connected as the default for next time.
# A network (TCP) companion isn't discoverable — name it with --tcp host:port, a TCP profile, or
# the picker's "add a network device" prompt. Define profiles below only to give your hardware
# stable aliases (e.g. --profile yagi, --profile handheld for Bluetooth, --profile wifi for TCP).
# Profiles are optional. When you give no profile and none of --port, --ble, or --tcp,
# MeshTerm discovers the serial devices that are connected and the Bluetooth companions that
# are near. It lets you select one (interactively), or it uses the one device that is
# there. It remembers the last device that connected, and uses it as the default the next
# time. A network (TCP) companion is not discoverable. To name one, use --tcp host:port, a
# TCP profile, or the "add a network device" prompt of the picker. Define profiles below
# only to give your hardware names that do not change (for example --profile yagi,
# --profile handheld for Bluetooth, or --profile wifi for TCP).
# Uncomment and name one of your own profiles below to make it the default. Left
# commented, MeshTerm finds the device itself, as described above.
# To make one of your own profiles the default, remove the comment mark below and write
# the name of the profile. While the line is a comment, MeshTerm finds the device by
# itself, as described above.
# default_profile = "s3"
# Open the radio link as soon as the menu appears. False opens it lazily instead — only
# once monitoring is turned on or a tool first needs the radio.
# Open the radio link when the menu appears. When the value is false, MeshTerm opens the
# link later: when you turn on monitoring, or when a tool first needs the radio.
connect_on_start = true
# db_path = "C:/Users/you/meshterm/meshterm.db"
# One [profiles.<name>] block per physical companion device.
# Make one [profiles.<name>] block for each physical companion device.
[profiles.s3]
port = "COM5"
baudrate = 115200
@@ -40,44 +44,46 @@ description = "Local-range repeater"
port = "COM8"
description = "XIAO observer running a bot"
# A Bluetooth LE companion: give it an `address` (from `meshterm devices`) instead of a port.
# The `transport = "ble"` line is optional — a bare `address` already implies Bluetooth.
# A Bluetooth LE companion: give an `address` (from `meshterm devices`) and not a port.
# The line `transport = "ble"` is optional. An `address` alone means Bluetooth.
[profiles.handheld]
address = "AA:BB:CC:DD:EE:FF"
# transport = "ble"
# ble_pin = "123456" # only if your device requires a pairing PIN
# ble_pin = "123456" # only if your device needs a pairing PIN
description = "Pocket handheld over Bluetooth"
# A network (TCP) companion — a WiFi board, or a meshcored-style proxy — reachable at a
# host:port. Give it a `host` (and optional `tcp_port`, default 5000); a bare `host` already
# implies TCP, so the `transport = "tcp"` line is optional. A TCP companion isn't discoverable,
# so a profile (or --tcp host:port, or the picker's "add a network device" prompt) is how you
# reach one.
# A network (TCP) companion, such as a WiFi board or a meshcored-style proxy, at a
# host:port. Give a `host` (and an optional `tcp_port`, which is 5000 by default). A `host`
# alone means TCP, so the line `transport = "tcp"` is optional. A TCP companion is not
# discoverable. To reach one, use a profile, or --tcp host:port, or the "add a network
# device" prompt of the picker.
[profiles.wifi]
host = "192.168.1.50"
# tcp_port = 5000
# transport = "tcp"
description = "Basestation over WiFi"
# A radio on this machine's own SPI bus: MeshTerm runs its node while it is connected.
# The pins default to the uConsole AIO v1's, which needs nothing more than this (and is
# listed on the device screen anyway, whenever /dev/spidev1.0 exists).
# A radio on the own SPI bus of this machine: MeshTerm runs its node while the radio is
# connected. The default pins are the pins of the uConsole AIO v1. That board needs only
# this profile. The device screen also lists it, when /dev/spidev1.0 exists.
[profiles.aio]
transport = "spi"
# [profiles.aio.spi]
# en_pins = [27] # the AIO v2 powers its radio from pin 27
#
# Which /dev/gpiochip<n> carries the pins is not a fact about the board: a uConsole's CM4
# has its 40-pin header on chip 0, a CM5 had it on chip 4 under its early kernels, chip 0
# once Raspberry Pi aliased it there, and another number again on some later kernels. So
# gpio_chip defaults to -1, which finds the chip by its label (pinctrl-rp1 on a CM5,
# pinctrl-bcm2711 on a CM4; `gpiodetect` lists them) and falls back to chip 0 where none
# carries either. Give it a number only to override that:
# The number of the /dev/gpiochip<n> that has the pins does not depend on the board. A
# uConsole CM4 has its 40-pin header on chip 0. A CM5 had it on chip 4 with its early
# kernels, and on chip 0 after Raspberry Pi made an alias there. Some later kernels use
# another number. For this reason, gpio_chip is -1 by default. This value finds the chip by
# its label (pinctrl-rp1 on a CM5, pinctrl-bcm2711 on a CM4, and `gpiodetect` lists them).
# If no chip has one of these labels, MeshTerm uses chip 0. Give a number only to
# override this:
# gpio_chip = 4
# Any other board states its whole wiring — every pin and both switches, because a key
# left out keeps the AIO's value. Read them off the board's meshtasticd config; the
# values here are an illustration only. docs/guide/configuration.md has the key-by-key mapping.
# Any other board must give its complete wiring: each pin and both switches. A key that
# you omit keeps the value of the AIO. Read the values from the meshtasticd config of the
# board. The values here are only an example. docs/guide/configuration.md shows which
# key matches which value.
# [profiles.hat.spi]
# bus_id = 0 # /dev/spidev0.0
# cs_id = 0
+31 -29
View File
@@ -1,64 +1,66 @@
# MeshTerm documentation
Everything that didn't belong on the front page. The [README](../README.md) is the front
door — what MeshTerm is, how to install it, and enough to get a first session running.
This folder is the rest, in four parts:
This folder has everything that does not belong on the front page. The
[README](../README.md) is the front door. It says what MeshTerm is, how to install it, and
how to start a first session. This folder has the other documents, in four parts:
| Folder | What's in it |
| Folder | What is in it |
| --- | --- |
| [`guide/`](#using-meshterm) | Using MeshTerm: what each screen does, configuring it, connecting a companion, terminals. |
| [`cli/`](#the-command-line) | The command line manual, and a cookbook of one-liners. |
| [`devices/`](#running-it-on-a-handheld) | Running MeshTerm on a handheld's own screen, one manual per device. |
| [`development/`](#reading-the-code) | How the code is laid out, and a historical survey. |
| [`guide/`](#using-meshterm) | How to use MeshTerm: what each screen does, how to configure it, how to connect a companion, and terminals. |
| [`cli/`](#the-command-line) | The manual of the command line, and a cookbook of one-line commands. |
| [`devices/`](#running-it-on-a-handheld) | How to run MeshTerm on a handheld's own screen, with one manual for each device. |
| [`development/`](#reading-the-code) | How the code is laid out, how we write, and a historical survey. |
## Using MeshTerm
| Read | For |
| --- | --- |
| **[What MeshTerm does](guide/features.md)** | Every screen the menu offers, grouped the way the menu groups them, with the command that does the same job without it. |
| [Configuring MeshTerm](guide/configuration.md) | Preferences versus config, device profiles, and everything kept under `~/.meshterm`. |
| [When a companion won't connect](guide/connecting.md) | Bluetooth pairing on each system, finding the PIN, and every connection error MeshTerm reports, with what to do about it. |
| [Terminals, icons, and the Windows console](guide/terminals.md) | Why the icons and charts draw on some terminals and not others, and what MeshTerm does about it on Windows. |
| **[What MeshTerm does](guide/features.md)** | Each screen that the menu has, in the same groups as the menu, with the command that does the same job without the menu. |
| [Configuring MeshTerm](guide/configuration.md) | The difference between preferences and config, the device profiles, and all the files that MeshTerm keeps in `~/.meshterm`. |
| [When a companion does not connect](guide/connecting.md) | Bluetooth pairing on each system, how to find the PIN, and each connection error that MeshTerm reports, with the action to take. |
| [Terminals, icons, and the Windows console](guide/terminals.md) | Why the icons and charts draw correctly on some terminals and not on others, and what MeshTerm does about it on Windows. |
## The command line
| Read | For |
| --- | --- |
| **[The command line](cli/README.md)** | The manual. Every subcommand and option, what each prints on the plain and JSON faces, the exit statuses, and recipes built on `jq`. |
| [The CLI cookbook](cli/cookbook.md) | Common one-liners by task, for when you want the shape of a command rather than its specification. |
| **[The command line](cli/README.md)** | The manual. Each subcommand and option, what each prints on the plain face and on the JSON face, the exit statuses, and recipes that use `jq`. |
| [The CLI cookbook](cli/cookbook.md) | Common one-line commands, in groups by task. Use it when you want the form of a command and not its specification. |
## Running it on a handheld
Only if you want MeshTerm running on a handheld's own screen. A MeshCore companion on
USB, Bluetooth, or Wi-Fi needs none of this.
Read these documents only if you want to run MeshTerm on a handheld's own screen. A
MeshCore companion on USB, Bluetooth, or Wi-Fi does not need them.
| Read | For |
| --- | --- |
| **[MeshTerm on hardware](devices/README.md)** | Start here — which manual below is yours, and what the device-side scripts do. |
| [MeshTerm on the PicoCalc](devices/picocalc-lyra.md) | A stock ClockworkPi PicoCalc to a Linux handheld with a LoRa radio soldered inside: the shopping list, the swap to a Luckfox Lyra core, Calculinux, and the radio. |
| [MeshTerm on the uConsole](devices/uconsole.md) | A uConsole (CM4 or CM5) with the hackergadgets AIO LoRa board, whose SX1262 sits on the computer's own SPI bus — MeshTerm drives it directly with `--spi`, or a small bridge fronts it as a companion for an always-on node. |
| **[MeshTerm on hardware](devices/README.md)** | Start here. It says which of the manuals below is the correct one for you, and what the scripts on the device do. |
| [MeshTerm on the PicoCalc](devices/picocalc-lyra.md) | How to change a stock ClockworkPi PicoCalc into a Linux handheld that has a LoRa radio soldered inside: the shopping list, the change to a Luckfox Lyra core, Calculinux, and the radio. |
| [MeshTerm on the uConsole](devices/uconsole.md) | A uConsole (CM4 or CM5) with the hackergadgets AIO LoRa board. Its SX1262 is on the computer's own SPI bus. MeshTerm can control it directly with `--spi`. Or a small bridge can make it a companion for a node that is always on. |
| [MeshTerm on the Cardputer Zero](devices/cardputer-zero.md) | ⚠️ **Not supported yet.** What exists for M5Stack's Linux Cardputer, and how to try it in the desktop emulator. |
## Reading the code
| Read | For |
| --- | --- |
| [How the code is laid out](development/architecture.md) | The layering, and where a new feature goes. |
| [CLAUDE.md](../CLAUDE.md) | The actual style guide — terminology, UX standards, and the reusable building blocks. Written for AI assistants, and the house rules for everyone. |
| [CONTRIBUTING.md](../CONTRIBUTING.md) | How to propose a change, the rules on AI use, the dev install, the three checks a change must pass, and what happens to your contribution's licensing. |
| [How the code is laid out](development/architecture.md) | The layers of the code, and where to put a new feature. |
| [How we write](development/writing-style.md) | How we write: the ASD-STE100 rules and the glossary for docstrings, comments, and documents. |
| [CLAUDE.md](../CLAUDE.md) | The real style guide: the terms, the UX standards, and the building blocks that you can use again. It is written for AI assistants, and it has the house rules for everyone. |
| [CONTRIBUTING.md](../CONTRIBUTING.md) | How to propose a change, the rules for the use of AI, the dev install, the three checks that a change must pass, and what happens to the licensing of your contribution. |
### Historical record
A survey of the code, kept because a project that writes down its own problems is easier
to trust than one that doesn't — but it is a **snapshot of a date, not a to-do list**.
Some of what it describes has been fixed since, and it does not mark which.
This is a survey of the code. We keep it, because a project that writes down its own
problems is easier to trust than a project that does not. But it is a **snapshot of one
date, not a list of tasks**. Some of the problems that it describes are corrected now, and
the survey does not say which.
| Read | For |
| --- | --- |
| [UI consistency survey](development/survey-ui-consistency.md) | Everything else in the CLAUDE.md UX standard — six sweeps found nothing, which is the useful part. |
| [UI consistency survey](development/survey-ui-consistency.md) | Each other point of the CLAUDE.md UX standard. Six sweeps found nothing, and this is the useful result. |
## Also here
`screenshots/` holds the captures the README shows and the ones
[meshterm.net](https://meshterm.net) builds its landing page from, so a file in there is
referenced from more than one place.
`screenshots/` has the captures that the README shows. It also has the captures from which
[meshterm.net](https://meshterm.net) builds its landing page. Thus more than one place
refers to each file in this folder.
+8 -9
View File
@@ -1,11 +1,11 @@
# How the code is laid out
A map of the package for anyone reading it for the first time. The house rules that
govern what goes where are in [CLAUDE.md](../../CLAUDE.md), which is the real style guide;
[CONTRIBUTING.md](../../CONTRIBUTING.md) has the dev install and the gates a change has to
pass.
This page is a map of the package for a person who reads it for the first time.
[CLAUDE.md](../../CLAUDE.md) has the house rules that decide what goes where. It is the real
style guide. [CONTRIBUTING.md](../../CONTRIBUTING.md) has the dev install and the checks
that a change must pass.
MeshTerm is layered so a new feature is one file in `meshterm/tools/`:
MeshTerm is divided into layers. Thus a new feature is one file in `meshterm/tools/`:
```
cli.py Typer app; no subcommand -> interactive menu
@@ -17,7 +17,6 @@ persistence/ SQLite schema, repository, structured logging
ui/ Rich theme/widgets + the full-screen TUI (screens, dialogs, map, chat)
```
Adding a feature means subclassing `Tool`, decorating it with `@register`, and
implementing `run()`. The same registry builds both the CLI and the menu, and the base
class wraps every execution in a logged `runs` row automatically.
To add a feature, make a subclass of `Tool`, add the `@register` decorator to it, and
write the `run()` method. The same registry builds the CLI and the menu. The base class
automatically writes a logged `runs` row for each execution.
+144 -125
View File
@@ -1,23 +1,25 @@
# UI consistency survey — what is off, and what turned out to be fine
# UI consistency survey: what is off, and what turned out to be fine
> **A historical record.** This was a snapshot of the code on the date below, kept
> because a project that writes down its own problems is easier to trust than one
> that doesn't. Some of what's here has since been fixed and some hasn't; nothing in
> it is a to-do list. Read it as "what this looked like then", not "what is wrong now".
> **A historical record.** This was a snapshot of the code on the date below. We keep it,
> because a project that writes down its own problems is easier to trust than a project
> that does not. We have corrected some of these problems since then, and we have not
> corrected others. Nothing in it is a list of tasks. Read it as "what the code looked like
> then", not as "what is wrong now".
Written 2026-08-22 against `main` @ `4b29248`. It covers the CLAUDE.md UX standard apart
from navigation. A catalogue, not a change.
The survey was written on 2026-08-22 against `main` @ `4b29248`. It covers the UX standard
of CLAUDE.md, except navigation. It is a catalogue. It is not a change.
**The short version: the standard is very well kept.** Six sweeps found nothing at all;
what remains is nine small, specific things. That is the useful result — it means the list
below is close to complete rather than a sample.
**The short version: the code follows the standard very well.** Six sweeps found nothing at
all. Nine small, specific points remain. This is the useful result. It means that the list
below is almost complete and is not a sample.
---
## Part 1 — the clean sweeps
## Part 1: the clean sweeps
Each of these was checked mechanically across the whole package, not spot-checked. The scripts
are one-offs and were not kept; each is a dozen lines of `ast.walk` and is trivial to redo.
We checked each of these with a program across the whole package. We did not check them by
hand on a sample. We did not keep the scripts. Each script is a dozen lines of `ast.walk`,
and it is easy to write again.
| Rule (CLAUDE.md) | How it was checked | Result |
|---|---|---|
@@ -27,31 +29,34 @@ are one-offs and were not kept; each is a dozen lines of `ast.walk` and is trivi
| Filter atom is always `type to filter` | regex over all hints | **0 deviations** |
| No emoji in a screen or dialog title | 86 title literals vs an emoji range | **0** (one hit was a `Choice` row title, where icons belong) |
| Sentence case in titles | capitalised non-leading word, minus proper nouns | **0** (5 hits were markup-wrapped panel headings) |
| Lexicon: "heard" not "seen"; "contacts" not "nodes list" | regex over all non-docstring literals | **0** |
| Lexicon: "heard" and not "seen", "contacts" and not "nodes list" | regex over all non-docstring literals | **0** |
| Status marks: never `✔ ✖ ✅ ❌` | scan of every source line | **0** (the 3 hits are the fold table that *normalizes* them, and a docstring warning against them) |
| `…` on rows that open a prompt, none on rows that act | 40 row labels via the builders | **1 arguable** — see §2.7 |
| `…` on rows that open a prompt, none on rows that act | 40 row labels via the builders | **1 arguable** (refer to §2.7) |
| Empty state never parenthesised | scan of 53 empty-state-shaped strings | **0** |
| `render.query_line` is the one query line | import graph | **exactly** the four screens CLAUDE.md names: select list, path composer, map, mesh walk |
Two more worth calling out because they are the kind of rule that usually rots:
Two more points are important, because rules of this kind usually decay:
- **`Del` on a row is gated correctly.** The hint atom only appears when the highlighted row
is actually deletable ([`select.py:448`](../../meshterm/ui/tui/select.py#L448)) — a real
instance of "never advertise a key that would do nothing", implemented rather than assumed.
- **The PicoCalc lane's "a chord earns a chip" rule holds.** The one bare-letter shortcut in
the app — Time Machine's `w` — has its F3 chip, with the reasoning written out in full at
- **`Del` on a row is correctly gated.** The hint atom is visible only when the highlighted
row can be deleted ([`select.py:448`](../../meshterm/ui/tui/select.py#L448)). This is a
real example of "never advertise a key that would do nothing". The code implements the
rule, and the survey did not only assume it.
- **The rule "a chord earns a chip" on the lane of the PicoCalc holds.** The app has one
bare-letter shortcut, which is `w` in the Time Machine. It has its F3 chip. The reason is
written in full at
[`timemachine_screen.py:227-242`](../../meshterm/ui/timemachine_screen.py#L227).
---
## Part 2 — the findings
## Part 2: the findings
Ordered by how visible each is to someone using the app.
The findings are in order of how visible each one is to a person who uses the app.
### 2.1 Empty-state voice: four sites shout (rule: lowercase, muted, `— explanation`, no full stop)
The house voice is `no contacts yet — receive an advert first`. These four break it, and they
are the empty states of four whole screens, so each is the *only* thing on screen when it shows:
The house voice is `no contacts yet — receive an advert first`. These four sites do not
follow it. They are the empty states of four complete screens. Thus each one is the *only*
thing on the screen when it shows:
| Site | Current | House voice |
|---|---|---|
@@ -60,158 +65,172 @@ are the empty states of four whole screens, so each is the *only* thing on scree
| [`timemachine_screen.py:335-336`](../../meshterm/ui/timemachine_screen.py#L335) | `Nothing recorded in this window.` + `Press w to widen it.` | lowercase, no full stop |
| [`timemachine_screen.py:453-454`](../../meshterm/ui/timemachine_screen.py#L453) | `Nothing sent in this window.` + `Press w to widen it.` | ditto |
The two `Press w to widen it.` lines are a second question: they are *hints*, sitting in the
body, on a screen whose F-key lane and footer already advertise `w`. Either the empty state
absorbs it (`nothing recorded in this window — press w to widen it`) or it goes.
The two `Press w to widen it.` lines raise a second question. They are *hints*, and they are
in the body of a screen where the F-key lane and the footer already advertise `w`. There are
two options. The empty state can include the hint (`nothing recorded in this window — press w to widen it`).
Or we can remove the hint.
Deliberately **not** on this list: `Never heard` ([`contacts_screen.py:258`](../../meshterm/ui/contacts_screen.py#L258))
is a purge-ladder *row label*, and `No revisits` ([`records.py:237`](../../meshterm/services/records.py#L237))
is a discipline *title*. Both correctly capitalised.
These two sites are **not** on this list on purpose. `Never heard`
([`contacts_screen.py:258`](../../meshterm/ui/contacts_screen.py#L258)) is the *row label* of
a purge ladder. `No revisits` ([`records.py:237`](../../meshterm/services/records.py#L237))
is the *title* of a discipline. Both are correctly capitalised.
### 2.2 One dialog inverts the button convention
The rule: *safe way out on the left, committing verb on the right and default*.
The rule is: *the safe way out is on the left, and the committing verb is on the right and
is the default*.
[`config_editor.py:538-541`](../../meshterm/ui/config_editor.py#L538) — the Location dialog:
This is the Location dialog, at
[`config_editor.py:538-541`](../../meshterm/ui/config_editor.py#L538):
```python
[("Pick on map", "map"), ("Type coordinates", "type"), ("Clear", "clear")],
title="Location",
```
No `Cancel`, no `default=`. So the leftmost slot — reserved for the safe way out — holds an
action, the rightmost — reserved for the default commit — holds the *destructive* option
(`Clear`), and the default falls to index 0. Esc still cancels, so nothing is lost; but it is
the only dialog in the app shaped this way, out of 49 call sites.
It has no `Cancel` and no `default=`. Thus the leftmost slot, which is for the safe way out,
holds an action. The rightmost slot, which is for the default commit, holds the *destructive*
option (`Clear`). The default is index 0. Esc still cancels, so the user loses nothing. But
this is the only dialog in the app with this shape, out of 49 call sites.
### 2.3 One dialog defaults to the middle button
[`config_editor.py:988-992`](../../meshterm/ui/config_editor.py#L988) — `[Cancel, Preview, Apply]`
with `default=1`, so Enter lands on **Preview**, not the rightmost `Apply`.
This is at [`config_editor.py:988-992`](../../meshterm/ui/config_editor.py#L988). The buttons
are `[Cancel, Preview, Apply]` with `default=1`. Thus Enter selects **Preview**, and not the
rightmost button `Apply`.
This is very likely deliberate — previewing before overwriting a whole device config is the
cautious path — but the standard as written says the rightmost verb is the default, and this
is the only three-button dialog with a real Cancel. Either the rule grows a clause ("with
three buttons, the *cautious* commit defaults") or this changes.
This is very probably intentional. Before it overwrites the complete config of a device, the
dialog gives the user a preview, which is the careful path. But the standard, as it is
written, says that the rightmost verb is the default. This is the only dialog with three
buttons and a real Cancel. There are two options. The rule can get a clause ("with three
buttons, the *careful* commit is the default"). Or this dialog changes.
### 2.4 Two dialog *shapes* the standard doesn't describe
### 2.4 Two dialog *shapes* that the standard does not describe
Beyond confirms, the app has two other dialog shapes, both used consistently but neither
written down:
The app has two dialog shapes in addition to the confirm. The app uses both consistently,
but the standard does not describe either:
- **The toggle picker** — `[Off, On]` with the default tracking the current value
- **The toggle picker.** It has `[Off, On]`, and the default follows the current value
([`config_editor.py:441`](../../meshterm/ui/config_editor.py#L441),
[`repeater_admin.py:313`](../../meshterm/ui/repeater_admin.py#L313)). No Cancel; `Esc keep`
carries it, which is exactly the footer verb CLAUDE.md reserves for a value picker. Correct
in practice, undescribed in the standard.
- **The notification with an onward door** — `[Trophy case, Close]`, `default=1`
([`trace_screen.py:637-639`](../../meshterm/ui/trace_screen.py#L637)). Appears unbidden after a
record-setting trace. `Close` is the default so a stray Enter dismisses rather than
navigating — the inverse of the usual rule, and right for a dialog the user did not ask for.
[`repeater_admin.py:313`](../../meshterm/ui/repeater_admin.py#L313)). It has no Cancel.
`Esc keep` does that job. This is the footer verb that CLAUDE.md reserves for a value
picker. It is correct in practice, but the standard does not describe it.
- **The notification with a door to a next screen.** It has `[Trophy case, Close]` with
`default=1`
([`trace_screen.py:637-639`](../../meshterm/ui/trace_screen.py#L637)). It opens without a
request from the user, after a trace that sets a record. `Close` is the default. Thus an
Enter that the user pressed by mistake closes the dialog and does not open another screen.
This is the opposite of the usual rule, and it is correct for a dialog that the user did
not ask for.
Both are good designs. The gap is that CLAUDE.md describes only the confirm, so a future
dialog of either shape has nothing to copy.
Both designs are good. The gap is that CLAUDE.md describes only the confirm. Thus a future
dialog of one of these two shapes has no example to copy.
### 2.5 Two full screens have no F-key lane of their own — one of them is Trace
### 2.5 Two full screens have no F-key lane of their own, and one of them is Trace
Twelve classes declare `floating = False` (they are full screens, not dialogs). Ten override
`fkey_lane`. Two do not, and so inherit `Screen.fkey_lane` → the raw `DEFAULT_LANE`
([`fkeys.py:116`](../../meshterm/ui/tui/fkeys.py#L116)):
Twelve classes declare `floating = False`. These are full screens and not dialogs. Ten of
them override `fkey_lane`. Two do not override it. They use `Screen.fkey_lane`, which gives
the raw `DEFAULT_LANE` ([`fkeys.py:116`](../../meshterm/ui/tui/fkeys.py#L116)):
| Screen | |
|---|---|
| [`TraceScreen`](../../meshterm/ui/trace_screen.py#L345) (`trace_screen.py:345`) | the app's busiest screen |
| [`TraceScreen`](../../meshterm/ui/trace_screen.py#L345) (`trace_screen.py:345`) | the busiest screen of the app |
| [`TxSweepScreen`](../../meshterm/ui/tx_screen.py#L93) (`tx_screen.py:93`) | |
`DEFAULT_LANE` is the *always-enabled* pager. The ten others build from
`default_lane(nav=self.content_overflows)` ([`fkeys.py:131`](../../meshterm/ui/tui/fkeys.py#L131)),
which dims the chips when the body already fits. So on PicoCalc both screens advertise live
`Page ↑` / `Page ↓` chips whether or not there is anything to page — the one thing the lane's
standing rule forbids.
`DEFAULT_LANE` is the pager that is *always enabled*. The other ten screens build their lane
from `default_lane(nav=self.content_overflows)`
([`fkeys.py:131`](../../meshterm/ui/tui/fkeys.py#L131)). This dims the chips when the body
fits on the screen. Thus on the PicoCalc, both screens show live `Page ↑` and `Page ↓`
chips, also when there is nothing to page. The standing rule of the lane forbids exactly
this.
Trace is the one worth a proper look rather than a one-liner. Its footer
Trace needs a proper examination, and not only a one-line change. Its footer
([`trace_screen.py:498-505`](../../meshterm/ui/trace_screen.py#L498)) reads
`↑↓ actions · Enter run · PgUp/PgDn scroll · Esc back` — every key physical on the PicoCalc, so
nothing is *undiscoverable*. But it is the screen where F1–F3 would earn their keep most
(re-run, flip the path, open the composer are all one cursor move away today), and it is
currently the only central screen that hasn't been through the lane pass. Worth deciding what
its three free slots should say, not just gating the pager.
`↑↓ actions · Enter run · PgUp/PgDn scroll · Esc back`. Each of these keys is a physical key
on the PicoCalc, so no function is *hidden*. But Trace is the screen where F1 to F3 are most
useful. To run again, to flip the path, and to open the composer, the user now needs only
one move of the highlight. Trace is also the only central screen that has not had the lane
pass. We must decide what its three free slots say, and not only gate the pager.
`TxSweepScreen` genuinely is one line: `return default_lane(nav=self.content_overflows)`.
`TxSweepScreen` really is one line: `return default_lane(nav=self.content_overflows)`.
(The About page reads as an exception in a grep but isn't: it passes `floating=False` as a
constructor argument rather than a class attribute, and it does define its own lane at
[`about.py:115`](../../meshterm/ui/about.py#L115).)
(The About page looks like an exception in a grep, but it is not. It gives `floating=False`
as an argument of the constructor and not as a class attribute. It does define its own lane
at [`about.py:115`](../../meshterm/ui/about.py#L115).)
### 2.6 Dialogs that scroll but declare no lane
CLAUDE.md says `EMPTY_LANE` is for "a screen with none at all (**every dialog**)". Five
surfaces set it explicitly — the confirms, the text prompt, the busy spinner, the reorder
list, one trace dialog. But two floating dialogs that genuinely *do* page inherit
CLAUDE.md says that `EMPTY_LANE` is for "a screen with none at all (**every dialog**)". Five
surfaces set it explicitly: the confirms, the text prompt, the busy spinner, the reorder
list, and one trace dialog. But two floating dialogs that really *do* page use
`DEFAULT_LANE` instead:
- `RecordDialog` ([`records_screen.py:146`](../../meshterm/ui/records_screen.py#L146)) — a record's
story, which scrolls
- `PathComposerScreen` ([`path_composer.py`](../../meshterm/ui/path_composer.py)) — a windowed
list of hop suggestions
- `RecordDialog` ([`records_screen.py:146`](../../meshterm/ui/records_screen.py#L146)). It
shows the story of a record, and it scrolls.
- `PathComposerScreen` ([`path_composer.py`](../../meshterm/ui/path_composer.py)). It is a
list of hop suggestions, which shows a window of the list.
Both arguably *want* the pager, which would make the doc's "every dialog" too strong. The
question is whether the rule is "dialogs get no lane" (then these two need `EMPTY_LANE` and
lose paging chips) or "a surface gets the pager iff it pages" (then the doc's parenthetical
should read *every dialog that doesn't scroll*).
It is possible that both *need* the pager. If so, "every dialog" in the document is too
strong. We must decide between two rules. The first rule is "dialogs get no lane". Then
these two dialogs need `EMPTY_LANE`, and they lose their paging chips. The second rule is
"a surface gets the pager if and only if it pages". Then the parenthetical text in the
document must say *every dialog that does not scroll*.
### 2.7 One row label that may want an ellipsis
### 2.7 One row label that possibly needs an ellipsis
[`config_editor.py:787`](../../meshterm/ui/config_editor.py#L787) — `Share QR / URI`. The rule is
"a row that opens further prompts ends with `…`". This one opens a *view* (the QR popup), not
a prompt — so it turns on whether "further prompts" means "any further surface". Every other
row in the app is unambiguous; this is the only edge.
This is at [`config_editor.py:787`](../../meshterm/ui/config_editor.py#L787), and the label
is `Share QR / URI`. The rule is "a row that opens further prompts ends with `…`". This row
opens a *view* (the QR dialog), and it does not open a prompt. Thus the answer depends on the
meaning of "further prompts": is it "each further surface"? Each other row in the app is
clear. This is the only boundary case.
Also worth a glance while there: `Share QR / URI` is the only row label in the app using ` / `
as a separator.
Also note: `Share QR / URI` is the only row label in the app that uses ` / ` as a separator.
### 2.8 `★` carries two meanings
### 2.8 `★` has two meanings
CLAUDE.md itself lists it twice: `★ you (yellow)` under node types, and `★ best/winner` under
concept icons. In practice both are live — the map and every path line draw `★` for our own
node ([`pathline.SELF_GLYPH`](../../meshterm/ui/pathline.py)), while the trophy case and the
new-record dialog draw `★` in accent for a record
CLAUDE.md itself lists it two times: `★ you (yellow)` under node types, and `★ best/winner`
under concept icons. In practice, both are in use. The map and each path line draw `★` for
our node ([`pathline.SELF_GLYPH`](../../meshterm/ui/pathline.py)). The trophy case and
the dialog of a new record draw `★` in the accent colour for a record
([`trace_screen.py:635-636`](../../meshterm/ui/trace_screen.py#L635)).
They are never on the same surface, and the colours differ (yellow vs accent), so nothing is
actually ambiguous today. Flagged only because it is the single glyph in the whole marks table
with two entries, and the "one glyph per concept" heading promises otherwise. If a record ever
needs to appear next to a path line, this is where it bites — `🏆` is already reserved for the
trophy case and would carry it.
The two uses are never on the same screen, and the colours are different (yellow and
accent). Thus nothing is unclear today. We note it only because it is the single glyph in
the whole table of marks that has two entries, and the heading "one glyph per concept" says
that this does not occur. If a record must appear next to a path line in the future, this is
where a problem will occur. `🏆` is already reserved for the trophy case, and it can carry
the record.
### 2.9 `node.unknown` vs `muted`, unverified
### 2.9 `node.unknown` and `muted`, not verified
CLAUDE.md is emphatic that an unidentified node takes `node.unknown` and that this is
CLAUDE.md is clear that an unidentified node takes `node.unknown`, and that this is
*deliberately not* `muted` ("muted is chrome and may sit a step darker, while an unidentified
node is content you can still act on"). `node.unknown` appears at 11 sites. There are also 26
`style="muted"` uses inside [`pathline.py`](../../meshterm/ui/pathline.py) and
[`widgets.py`](../../meshterm/ui/widgets.py) — the two modules that draw nodes.
node is content you can still act on"). `node.unknown` is used at 11 sites. There are also 26
uses of `style="muted"` in [`pathline.py`](../../meshterm/ui/pathline.py) and
[`widgets.py`](../../meshterm/ui/widgets.py). These are the two modules that draw nodes.
Most of those 26 are certainly chrome (separators, labels, the un-lit part of a key, which the
standard explicitly says stays muted). But this is the one rule in the standard that a
mechanical sweep cannot settle: it needs a human to look at each and say "that's chrome" or
"that's an unnamed node". Listed here as the one unaudited corner, not as a finding.
Most of these 26 uses are certainly chrome (separators, labels, and the part of a key that
is not lit, which the standard says must stay muted). But a program cannot decide this rule
in a sweep. A person must look at each use and decide: "this is chrome" or "this is a node
without a name". We list it here as the one corner that we did not audit. It is not a
finding.
---
## Part 3 — shortlist
## Part 3: shortlist
| # | Finding | Sites | Effort | Weight |
|---|---|---|---|---|
| 2.1 | Empty states shout on four screens | 4 (6 strings) | trivial | **high** — it is the whole screen |
| 2.2 | Location dialog has no Cancel and defaults to an action | 1 | small | medium |
| 2.5 | Trace and TX advertise a dead pager on PicoCalc; Trace has never had a lane pass | 2 | one line + one design pass | **medium-high** |
| 2.3 | Restore dialog defaults to the middle button | 1 | decision, not code | low |
| 2.6 | Two scrolling dialogs inherit the pager the doc says they shouldn't have | 2 | decision | low |
| 2.4 | Two dialog shapes used consistently but undocumented | — | doc only | low |
| 2.7 | `Share QR / URI` — ellipsis? separator? | 1 | trivial | low |
| 2.8 | `★` has two entries in a "one glyph per concept" table | — | doc only | low |
| 2.9 | `node.unknown` vs `muted` in the two node-drawing modules | ~26 to eyeball | manual | unknown |
| 2.1 | Empty states shout on four screens | 4 (6 strings) | trivial | **high**, because it is the whole screen |
| 2.2 | The Location dialog has no Cancel and its default is an action | 1 | small | medium |
| 2.5 | Trace and TX advertise a dead pager on the PicoCalc. Trace has never had a lane pass | 2 | one line and one design pass | **medium-high** |
| 2.3 | The Restore dialog defaults to the middle button | 1 | a decision, not code | low |
| 2.6 | Two scrolling dialogs inherit the pager that the document says they must not have | 2 | a decision | low |
| 2.4 | Two dialog shapes that the app uses consistently but that no document describes | none | document only | low |
| 2.7 | `Share QR / URI`: an ellipsis or a different separator? | 1 | trivial | low |
| 2.8 | `★` has two entries in a table of "one glyph per concept" | none | document only | low |
| 2.9 | `node.unknown` and `muted` in the two modules that draw nodes | approximately 26 to examine | manual | unknown |
Items 2.1, 2.2, 2.5 and 2.7 are code. The rest are decisions about the standard itself, and
2.3, 2.4 and 2.8 may well resolve as "the standard should say what the code already does".
Items 2.1, 2.2, 2.5, and 2.7 are code. The other items are decisions about the standard
itself. For items 2.3, 2.4, and 2.8, the decision can be "the standard must say what the code
already does".
+36 -32
View File
@@ -1,15 +1,15 @@
[build-system]
# 1.26 is where hatchling started writing the PEP 639 fields below (`License-Expression`,
# `License-File`) into the metadata; an older one builds a wheel that says nothing about
# the licence at all, silently.
# Hatchling 1.26 is the first version that writes the PEP 639 fields below
# (`License-Expression`, `License-File`) into the metadata. An older version builds a
# wheel that says nothing about the licence, and it gives no warning.
requires = ["hatchling>=1.26"]
build-backend = "hatchling.build"
[project]
# The distribution name only. The import package and the console script both stay
# `meshterm` — PyPI prohibits that name (see pypi/support#12162), and nothing about the
# app changes because of it. `[tool.hatch.build.targets.wheel]` below is what keeps the
# wheel shipping the `meshterm/` package under this different name.
# This is only the name of the distribution. The import package and the console script stay
# `meshterm`. PyPI prohibits that name (refer to pypi/support#12162), but the app does not
# change because of this. The `[tool.hatch.build.targets.wheel]` section below makes the
# wheel contain the `meshterm/` package under this different name.
name = "mesh-term"
dynamic = ["version"]
description = "MeshTerm is a full-featured TUI MeshCore client for your terminal. Connects to a companion over USB, Bluetooth, or TCP. For Windows, macOS, and Linux."
@@ -19,7 +19,7 @@ authors = [{ name = "Jean-Pierre Martineau" }]
license = "Apache-2.0"
license-files = ["LICENSE", "NOTICE"]
keywords = ["meshcore", "lora", "mesh", "radio", "cli"]
# No `License ::` entry: PEP 639 forbids one alongside the SPDX expression above.
# There is no `License ::` entry, because PEP 639 does not permit one with the SPDX expression above.
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
@@ -42,7 +42,7 @@ dependencies = [
"pycryptodome>=3.20", # AES/HMAC for decrypting overheard channel packets
"pyserial>=3.5", # serial-port enumeration for device discovery
"bleak>=0.22", # Bluetooth LE scanning + companion connection
"typer>=0.12", # scripted CLI; subcommands mirror the menu
"typer>=0.12", # scripted CLI. The subcommands mirror the menu
"rich>=13.7", # modern console rendering (progress, tables, panels)
"markdown-it-py>=3.0", # CommonMark parser behind the written About pages
"prompt_toolkit>=3.0", # full-screen interactive TUI (terminal, input, resize)
@@ -55,16 +55,17 @@ dependencies = [
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
# Pinned to a minor line, not floated. A formatter that drifts between machines is
# not a gate: 0.16 formats Python inside Markdown and 0.15 does not, so an
# unpinned `ruff>=0.4` had CI and a laptop disagreeing about a clean tree.
# The version is pinned to one minor line. A formatter that is different on different
# machines is not a gate. Ruff 0.16 formats Python inside Markdown and 0.15 does not.
# With an unpinned `ruff>=0.4`, CI and a laptop disagreed about a tree with no errors.
"ruff>=0.16.6,<0.17",
# The SPI radio's node, run for real in tests/test_radionode_live.py on a fake radio.
# The bare library is pure Python everywhere; only the `hardware` extra is Linux-bound.
# The node of the SPI radio. tests/test_radionode_live.py runs it with a fake radio.
# The library alone is pure Python on all systems. Only the `hardware` extra needs Linux.
"openhop-core>=1.1.3",
]
# A LoRa chip on this machine's own SPI bus (the uConsole AIO), driven by a node MeshTerm
# runs itself — see meshterm/core/spiradio.py. Linux only, and nowhere else worth carrying.
# A LoRa chip on this machine's own SPI bus (the uConsole AIO). A node that MeshTerm runs
# itself controls it (refer to meshterm/core/spiradio.py). It works only on Linux, so
# the other systems do not need it.
spi = [
"openhop-core[hardware]>=1.1.3",
]
@@ -78,20 +79,21 @@ Changelog = "https://github.com/jpmartineau/MeshTerm/blob/main/CHANGELOG.md"
[project.scripts]
meshterm = "meshterm.cli:main"
# The one place a version is written; `[project] version` is dynamic so the package
# attribute and the distribution metadata cannot drift apart.
# This is the one place where we write the version. `[project] version` is dynamic, so the
# package attribute and the distribution metadata cannot become different.
[tool.hatch.version]
path = "meshterm/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["meshterm"]
# The sdist is the package's source, not the repository's: what someone needs to build
# and test the wheel. Hatchling's default is everything git does not ignore, which swept
# in the Claude skills, the GitHub configuration, the PyInstaller spec, the device-side
# shell scripts (one of which carries data under another licence) and the developer
# environment template — none of which builds, tests, or documents the package. Kept:
# the package, the tests and their fixture, the manual, and the files a build reads.
# The sdist is the source of the package. It is not the source of the repository. It has
# what a person needs to build and test the wheel. By default, Hatchling includes each file
# that git does not ignore. This included the Claude skills, the GitHub configuration, the
# PyInstaller spec, the shell scripts for the device (one of which has data under another
# licence), and the template of the developer environment. None of these builds, tests, or
# documents the package. The sdist keeps the package, the tests and their fixture, the
# manual, and the files that a build reads.
[tool.hatch.build.targets.sdist]
exclude = [
"/.claude",
@@ -108,23 +110,25 @@ exclude = [
[tool.ruff]
line-length = 100
target-version = "py310"
# Helper scripts for the Claude skills; they are not part of the package. And `docs/`,
# which holds dated survey documents: ruff formats Python blocks inside Markdown, and the
# code they quote is an excerpt of how a file looked on a past date. Reformatting a
# quotation edits the record — and the excerpts are call fragments, not whole programs, so
# the formatter's idea of "fixed" turns them into something that was never there.
# The helper scripts for the Claude skills are not part of the package. Also exclude `docs/`.
# It has survey documents with dates. Ruff formats Python blocks inside Markdown, and the
# code in these documents is an excerpt that shows how a file looked on a past date. If
# ruff formats a quotation, it changes the record. The excerpts are fragments of calls and
# not complete programs. Thus the formatter would change them into something that never
# existed.
extend-exclude = [".claude", "docs"]
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "D"]
ignore = ["D203", "D213"] # pick one blank-line-before-class / multi-line-summary style
ignore = ["D203", "D213"] # select one style for the blank line before a class and for the multi-line summary
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-bugbear]
# Typer declares CLI parameters by calling these in the default; the objects they
# return are inert sentinels the command never mutates, which is what B008 guards.
# Typer declares CLI parameters when the default value calls these functions. The objects
# that they return are inert markers, and the command never changes them. B008 guards
# against this kind of change.
extend-immutable-calls = ["typer.Argument", "typer.Option"]
[tool.pytest.ini_options]
+28 -24
View File
@@ -1,42 +1,46 @@
# scripts/
Repo-side tools. None of these ship in the wheel.
These are tools for the repository. None of them is in the wheel.
| Script | What it is for |
|---|---|
| `basemap-doctor.py` | Why a user's map has no basemap — certificates, DNS, proxy, or colour depth |
| `cardputer-zero/` | `launcher-entry.sh` puts a source checkout on the Cardputer Zero's app launcher (and `remove` takes it off), until there is a store package; `meshterm.png` is its icon, the website's logo at the launcher's 100×100. `type-text.py` types a pasted string (an API key, a Wi-Fi password) on the Cardputer's keyboard from another computer, over SSH |
| `picocalc-lyra/` | Calculinux device setup: console font, palette, Wi-Fi; `xiao-radio/` is the XIAO radio kit (firmware build, flashing, UART setup) |
| `uconsole/` | The uConsole's always-on SPI bridge service (MeshTerm can also drive that radio directly, with no script at all — see `docs/devices/uconsole.md`) |
| `basemap-doctor.py` | Finds why a user's map has no basemap: certificates, DNS, proxy, or colour depth |
| `cardputer-zero/` | `launcher-entry.sh` puts a source checkout in the Cardputer Zero's app launcher. `remove` takes it out. This is for the time before there is a store package. `meshterm.png` is the icon: the website's logo, at the launcher's 100×100 size. `type-text.py` types a pasted string (an API key, a Wi-Fi password) on the Cardputer's keyboard from another computer, over SSH |
| `picocalc-lyra/` | Setup of the Calculinux device: console font, palette, Wi-Fi. `xiao-radio/` is the kit for the XIAO radio (firmware build, flashing, UART setup) |
| `uconsole/` | The uConsole's always-on SPI bridge service. MeshTerm can also control that radio directly, with no script (refer to `docs/devices/uconsole.md`) |
## basemap-doctor.py
The map is the only networked part of MeshTerm, and every fetch failure on that path is
logged at DEBUG while `log_level` defaults to WARNING — so a reader whose tiles never
arrive sees a blank ground and no explanation. This prints the explanation and the fix.
The map is the only part of MeshTerm that uses the network. MeshTerm logs each fetch
failure on this path at DEBUG, and `log_level` is WARNING by default. Thus a user whose
tiles do not arrive sees a blank ground and no explanation. This script prints the
explanation and the correction.
Send a user this, to run **on the machine with the problem**:
To use it, tell the user to run this **on the machine that has the problem**:
```bash
curl -fsSL https://raw.githubusercontent.com/jpmartineau/MeshTerm/main/scripts/basemap-doctor.py -o /tmp/basemap-doctor.py
python3 /tmp/basemap-doctor.py
```
Any `python3` will do: if that one has no MeshTerm, the script reads the interpreter out
of the installed `meshterm` command's shebang and hands itself over, so the answer always
comes from **the app's own Python** — a Mac routinely has three and only that one has the
trust store that matters. Save it to a file as above rather than piping into `python3 -`:
with no file on disk there is nothing to hand over.
Any `python3` is correct. If that `python3` does not have MeshTerm, the script reads the
interpreter from the installed `meshterm` command's shebang, and it runs again with
that interpreter. Thus the answer always comes from **the app's own Python**. A Mac
often has three Pythons, and only one of them has the trust store that is important. Save
the script to a file, as in the example. Do not pipe it into `python3 -`. When there is no
file on the disk, the script cannot run again with the other interpreter.
Have them paste the whole output back. It names the interpreter, the certificate store,
the tile source, the cached-tile count, `COLORTERM`, and then one verdict:
Ask the user to paste all the output back to you. The output names the interpreter, the
certificate store, the tile source, the number of cached tiles, and `COLORTERM`. Then it
gives one verdict:
- **curl reached it, Python did not** — the Python's certificate store is empty. A
python.org install on macOS ships that way until `Install Certificates.command` is run.
- **neither reached it** — the network path: a firewall, a proxy, a DNS filter, or an
outage. Nothing to fix in MeshTerm.
- **both reached it** — tiles are fine and the complaint is colour: an unset `COLORTERM`
means 256 colours, where the basemap's greens and blues collide. Terminal.app cannot do
24-bit colour at all.
- **curl reached it, Python did not.** The Python's certificate store is empty. A
python.org install on macOS has an empty store until the user runs
`Install Certificates.command`.
- **neither reached it.** The problem is in the network path: a firewall, a proxy, a DNS
filter, or an outage. There is nothing to correct in MeshTerm.
- **both reached it.** The tiles are correct. The problem is colour. When `COLORTERM` is not
set, the terminal uses 256 colours, and the greens and blues of the basemap are too
similar. Terminal.app cannot do 24-bit colour.
Exit status is 0 when tiles arrive and 1 when they do not, so it can be scripted.
The exit status is 0 when the tiles arrive and 1 when they do not. Thus a script can use it.
+17 -15
View File
@@ -1,19 +1,20 @@
# PicoCalc mesh radio — XIAO nRF52840 over UART
# PicoCalc mesh radio: XIAO nRF52840 over UART
A Seeed XIAO nRF52840 + Wio-SX1262 wired to the Luckfox Lyra's UART1, running MeshCore
companion firmware on `Serial1`, so MeshTerm on the PicoCalc opens `/dev/ttyS1` like any
other serial companion. No BLE, no USB host, no SD-slot sacrifice.
This is a Seeed XIAO nRF52840 with a Wio-SX1262. It is wired to UART1 of the Luckfox Lyra,
and it runs the MeshCore companion firmware on `Serial1`. Thus MeshTerm on the PicoCalc
opens `/dev/ttyS1` in the same way as for each other serial companion. This design does not
use BLE, does not use a USB host, and does not use the SD slot.
**The step-by-step guide is [`docs/devices/picocalc-lyra.md`, Phase 3](../../../docs/devices/picocalc-lyra.md#phase-3--add-the-radio)** — parts,
the firmware build and the patch explained, flashing, wiring, the Lyra setup, and what has
only been proven on one bench. What the register poke does is in `uart1-mux.py`'s own
header. This page is the bench card.
**The step-by-step guide is [`docs/devices/picocalc-lyra.md`, Phase 3](../../../docs/devices/picocalc-lyra.md#phase-3-add-the-radio).**
It has the parts, the firmware build and an explanation of the patch, the flashing, the
wiring, the setup of the Lyra, and what we proved on only one bench. The header of
`uart1-mux.py` explains what the register write does. This page is the bench card.
**Licensing.** The firmware is MeshCore, under the MIT License. The `meshcore-uart1.patch`
modifies MeshCore source and `build-firmware.sh` produces a MeshCore binary; the licence is
[`LICENSE.MeshCore`](LICENSE.MeshCore) in this directory.
changes the source of MeshCore, and `build-firmware.sh` makes a MeshCore binary. The licence
is [`LICENSE.MeshCore`](LICENSE.MeshCore) in this directory.
Three steps on two machines:
There are three steps on two machines:
```bash
# 1. dev machine: build (needs git + PlatformIO)
@@ -29,7 +30,7 @@ sh scripts/picocalc-lyra/xiao-radio/lyra-setup.sh # MT_USER=meshterm by
meshterm
```
Four wires, TX and RX crossed:
There are four wires. Cross TX and RX:
| XIAO pad | → | Lyra header | Physical pin | Carries |
| --- | --- | --- | --- | --- |
@@ -38,6 +39,7 @@ Four wires, TX and RX crossed:
| **GND** | → | **GND** | pin **8** | common ground |
| **3V3** | → | **3V3 OUT** | pin **36** | power, so it runs without USB |
> **The Lyra does not use Raspberry Pi Pico GPIO numbering** — the header is Pico-shaped, the
> mapping is Luckfox's own. GP4 is `gpio0-0`, GP5 is `gpio0-1`. All signals are 3.3 V and
> there is **no 5 V rail**: power the XIAO from 3V3 only.
> **The Lyra does not use the GPIO numbers of the Raspberry Pi Pico.** The header has the
> shape of a Pico header, but Luckfox made its own mapping. GP4 is `gpio0-0`, and GP5 is
> `gpio0-1`. All signals are 3.3 V, and there is **no 5 V rail**. Connect the power of the
> XIAO to 3V3 only.