Skip to content

Commit d1f3552

Browse files
authored
Merge pull request #928 from ianmcorvidae/examples
Make examples more regularized and focused, and add contribution guidelines for the examples folder
2 parents ff26f97 + 81ae8b6 commit d1f3552

16 files changed

Lines changed: 521 additions & 201 deletions

examples/CONTRIBUTING.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Contributing Example Scripts
2+
3+
Use this guide when adding or updating scripts in `examples/`.
4+
5+
## Must-have checklist before opening a PR
6+
7+
1. Script teaches one clear thing (its primary learning goal).
8+
2. File name matches that goal.
9+
3. Top docstring states purpose, transport scope, behavior, and expected output.
10+
4. Script has safe shutdown (`with ...` or `finally`) and graceful `KeyboardInterrupt` handling.
11+
5. Argument handling is clear (`argparse` preferred).
12+
6. Errors are explicit (no bare `except:`).
13+
7. The script is not a near-duplicate of an existing example.
14+
15+
## Choose the right teaching goal
16+
17+
Each example should have one primary lesson. Keep it focused.
18+
19+
- Good: "Send one text message over serial."
20+
- Good: "Print inbound text messages."
21+
- Avoid: discovery + chat + config mutation all in one script unless that combined flow is the lesson.
22+
23+
## Transport scope must be explicit
24+
25+
State exactly what transports are supported and why.
26+
27+
- Serial-only when that keeps the example simplest.
28+
- Multi-transport (Serial/TCP/BLE) only when transport selection is part of the lesson.
29+
- If TCP/BLE are supported, expose explicit flags (`--host`, `--ble`) and document defaults.
30+
31+
## Behavior and output should be predictable
32+
33+
Readers should know if the script sends, receives, mutates config, or combines those.
34+
35+
- Receive examples: subscribe to the narrowest pubsub topic that matches the lesson.
36+
- Send examples: clarify destination behavior (broadcast default vs explicit destination).
37+
- Mutation examples: clearly document side effects.
38+
39+
Output should make success easy to confirm:
40+
41+
- Print concise, stable status/event lines.
42+
- Avoid noisy debug output unless the script is specifically diagnostic-focused.
43+
44+
## Cleanup and error handling
45+
46+
- Use context managers where practical; otherwise close interfaces in `finally`.
47+
- Handle `KeyboardInterrupt` cleanly.
48+
- Exit non-zero for invalid args, connection/setup failures, or command failures.
49+
50+
## Naming guidance
51+
52+
Use descriptive names tied to the teaching goal.
53+
54+
- Prefer names like `tcp_connection_info_once.py` over `pub_sub_example.py`.
55+
- Prefer names like `tcp_pubsub_send_and_receive.py` over `pub_sub_example2.py`.
56+
- Avoid generic names such as `example2.py`.
57+
58+
Keep existing filenames only when compatibility or discoverability outweighs clarity.
59+
60+
## New script vs extending an existing one
61+
62+
Create a new script when:
63+
64+
- The learning goal is genuinely distinct.
65+
- Combining behaviors would make either example harder to understand.
66+
67+
Extend an existing script when:
68+
69+
- The change deepens the same lesson.
70+
- The resulting script remains focused and readable.

examples/get_hw.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
1-
"""Simple program to demo how to use meshtastic library.
2-
To run: python examples/get_hw.py
1+
"""Print the local node hardware model.
2+
3+
Purpose: show the narrowest read-only local hardware lookup.
4+
Transport scope: Serial only.
5+
Behavior: reads local node metadata and prints hwModel.
6+
Expected output: one hardware model line, if available.
7+
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
38
"""
49

10+
import argparse
511
import sys
612

7-
import meshtastic
813
import meshtastic.serial_interface
914

10-
# simple arg check
11-
if len(sys.argv) != 1:
12-
print(f"usage: {sys.argv[0]}")
13-
print("Print the hardware model for the local node.")
14-
sys.exit(3)
15-
16-
iface = meshtastic.serial_interface.SerialInterface()
17-
if iface.nodes:
18-
for n in iface.nodes.values():
19-
if n["num"] == iface.myInfo.my_node_num:
20-
print(n["user"]["hwModel"])
21-
iface.close()
15+
16+
def main() -> int:
17+
"""Print the hardware model for the local node."""
18+
if len(sys.argv) != 1:
19+
print(f"usage: {sys.argv[0]}")
20+
print("Print the hardware model for the local node.")
21+
return 3
22+
23+
parser = argparse.ArgumentParser(description="Print local Meshtastic hardware model")
24+
parser.parse_args()
25+
26+
try:
27+
with meshtastic.serial_interface.SerialInterface() as iface:
28+
if iface.nodes:
29+
for node in iface.nodes.values():
30+
if node["num"] == iface.myInfo.my_node_num:
31+
print(node["user"]["hwModel"])
32+
break
33+
except KeyboardInterrupt:
34+
return 0
35+
except Exception as exc:
36+
print(f"Error: Could not read hardware model: {exc}")
37+
return 1
38+
return 0
39+
40+
41+
if __name__ == "__main__":
42+
raise SystemExit(main())

examples/hello_world_serial.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,38 @@
1-
"""Simple program to demo how to use meshtastic library.
2-
To run: python examples/hello_world_serial.py
1+
"""Send one text message over serial.
2+
3+
Purpose: minimal send-only example.
4+
Transport scope: Serial only.
5+
Behavior: sends one message and exits.
6+
Expected output: no output on success.
7+
Cleanup/error handling: exits with code 3 for bad args, closes interface on exit.
38
"""
49

10+
import argparse
511
import sys
612

7-
import meshtastic
813
import meshtastic.serial_interface
914

10-
# simple arg check
11-
if len(sys.argv) < 2:
12-
print(f"usage: {sys.argv[0]} message")
13-
sys.exit(3)
1415

15-
# By default will try to find a meshtastic device,
16-
# otherwise provide a device path like /dev/ttyUSB0
17-
iface = meshtastic.serial_interface.SerialInterface()
18-
iface.sendText(sys.argv[1])
19-
iface.close()
16+
def main() -> int:
17+
"""Parse arguments and send one text message."""
18+
if len(sys.argv) < 2:
19+
print(f"usage: {sys.argv[0]} message")
20+
return 3
21+
22+
parser = argparse.ArgumentParser(description="Send one Meshtastic text message over serial")
23+
parser.add_argument("message", help="Message text to broadcast")
24+
args = parser.parse_args()
25+
26+
try:
27+
with meshtastic.serial_interface.SerialInterface() as iface:
28+
iface.sendText(args.message)
29+
except KeyboardInterrupt:
30+
return 0
31+
except Exception as exc:
32+
print(f"Error: Could not send message: {exc}")
33+
return 1
34+
return 0
35+
36+
37+
if __name__ == "__main__":
38+
raise SystemExit(main())

examples/info_example.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,46 @@
1-
"""Simple program to demo how to use meshtastic library.
2-
To run: python examples/info.py
1+
"""Show a concise local node summary over serial.
2+
3+
Purpose: read local node identity and metadata in one place.
4+
Transport scope: Serial only.
5+
Behavior: reads node database, prints local node ID/name/hardware model.
6+
Expected output: 1-3 summary lines describing the local node.
7+
Cleanup/error handling: closes interface on exit and prints clear errors on failure.
38
"""
49

5-
import meshtastic
610
import meshtastic.serial_interface
711

8-
iface = meshtastic.serial_interface.SerialInterface()
912

10-
# call showInfo() just to ensure values are populated
11-
# info = iface.showInfo()
13+
def main() -> int:
14+
"""Print local node summary fields."""
15+
try:
16+
with meshtastic.serial_interface.SerialInterface() as iface:
17+
local_num = iface.myInfo.my_node_num
18+
local_node = None
19+
if iface.nodes:
20+
for node in iface.nodes.values():
21+
if node["num"] == local_num:
22+
local_node = node
23+
break
24+
25+
if not local_node:
26+
print(f"Local node not found in node database (node num: {local_num}).")
27+
return 1
1228

29+
user = local_node.get("user", {})
30+
print(f"Node number: {local_num}")
31+
print(f"Node ID: {local_node.get('id', 'unknown')}")
32+
print(
33+
"Name: "
34+
f"{user.get('longName', 'unknown')} ({user.get('shortName', 'unknown')})"
35+
)
36+
print(f"Hardware model: {user.get('hwModel', 'unknown')}")
37+
except KeyboardInterrupt:
38+
return 0
39+
except Exception as exc:
40+
print(f"Error: Could not read local node summary: {exc}")
41+
return 1
42+
return 0
1343

14-
if iface.nodes:
15-
for n in iface.nodes.values():
16-
if n["num"] == iface.myInfo.my_node_num:
17-
print(n["user"]["hwModel"])
18-
break
1944

20-
iface.close()
45+
if __name__ == "__main__":
46+
raise SystemExit(main())
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env python3
2+
"""Passively monitor incoming text messages over serial.
3+
4+
Purpose: receive-only monitor for text messages.
5+
Transport scope: Serial only.
6+
Behavior: subscribes to text receive events and prints timestamp/channel/sender/text.
7+
Expected output: one line per received text message.
8+
Cleanup/error handling: graceful Ctrl+C exit and explicit connection errors.
9+
"""
10+
11+
import argparse
12+
import time
13+
from datetime import datetime
14+
from typing import Any, Optional
15+
16+
from pubsub import pub
17+
import meshtastic.serial_interface
18+
19+
_TZ_NAME = time.tzname[time.localtime().tm_isdst > 0]
20+
21+
22+
def on_receive(packet: dict[str, Any], interface: Any) -> None: # pylint: disable=unused-argument
23+
"""Print a compact line for each received text packet."""
24+
decoded = packet.get("decoded", {})
25+
if decoded.get("portnum") != "TEXT_MESSAGE_APP":
26+
return
27+
28+
message = decoded.get("text")
29+
if not message:
30+
return
31+
32+
channel_num = packet.get("channel", 0)
33+
sender_id = packet.get("fromId", "unknown")
34+
message_time = datetime.now().strftime(f"%a %b %d %Y %H:%M:%S {_TZ_NAME}")
35+
print(f"{message_time} : {channel_num} : {sender_id} : {message}")
36+
37+
38+
def main() -> int:
39+
"""Connect over serial and print inbound text messages."""
40+
parser = argparse.ArgumentParser(description="Read incoming Meshtastic text over serial")
41+
parser.add_argument("--port", default=None, help="Serial port path (default: auto-detect)")
42+
args = parser.parse_args()
43+
44+
pub.subscribe(on_receive, "meshtastic.receive")
45+
46+
iface: Optional[meshtastic.serial_interface.SerialInterface] = None
47+
try:
48+
iface = meshtastic.serial_interface.SerialInterface(devPath=args.port)
49+
print("Connected. Listening for text messages. Press Ctrl+C to exit.")
50+
while True:
51+
time.sleep(1)
52+
except KeyboardInterrupt:
53+
return 0
54+
except Exception as exc:
55+
print(f"Error: Could not monitor serial messages: {exc}")
56+
return 1
57+
finally:
58+
if iface:
59+
iface.close()
60+
return 0
61+
62+
63+
if __name__ == "__main__":
64+
raise SystemExit(main())

examples/pub_sub_example.py

Lines changed: 0 additions & 30 deletions
This file was deleted.

examples/pub_sub_example2.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)