Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions examples/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Contributing Example Scripts

Use this guide when adding or updating scripts in `examples/`.

## Must-have checklist before opening a PR

1. Script teaches one clear thing (its primary learning goal).
2. File name matches that goal.
3. Top docstring states purpose, transport scope, behavior, and expected output.
4. Script has safe shutdown (`with ...` or `finally`) and graceful `KeyboardInterrupt` handling.
5. Argument handling is clear (`argparse` preferred).
6. Errors are explicit (no bare `except:`).
7. The script is not a near-duplicate of an existing example.

## Choose the right teaching goal

Each example should have one primary lesson. Keep it focused.

- Good: "Send one text message over serial."
- Good: "Print inbound text messages."
- Avoid: discovery + chat + config mutation all in one script unless that combined flow is the lesson.

## Transport scope must be explicit

State exactly what transports are supported and why.

- Serial-only when that keeps the example simplest.
- Multi-transport (Serial/TCP/BLE) only when transport selection is part of the lesson.
- If TCP/BLE are supported, expose explicit flags (`--host`, `--ble`) and document defaults.

## Behavior and output should be predictable

Readers should know if the script sends, receives, mutates config, or combines those.

- Receive examples: subscribe to the narrowest pubsub topic that matches the lesson.
- Send examples: clarify destination behavior (broadcast default vs explicit destination).
- Mutation examples: clearly document side effects.

Output should make success easy to confirm:

- Print concise, stable status/event lines.
- Avoid noisy debug output unless the script is specifically diagnostic-focused.

## Cleanup and error handling

- Use context managers where practical; otherwise close interfaces in `finally`.
- Handle `KeyboardInterrupt` cleanly.
- Exit non-zero for invalid args, connection/setup failures, or command failures.

## Naming guidance

Use descriptive names tied to the teaching goal.

- Prefer names like `tcp_connection_info_once.py` over `pub_sub_example.py`.
- Prefer names like `tcp_pubsub_send_and_receive.py` over `pub_sub_example2.py`.
- Avoid generic names such as `example2.py`.

Keep existing filenames only when compatibility or discoverability outweighs clarity.

## New script vs extending an existing one

Create a new script when:

- The learning goal is genuinely distinct.
- Combining behaviors would make either example harder to understand.

Extend an existing script when:

- The change deepens the same lesson.
- The resulting script remains focused and readable.
51 changes: 36 additions & 15 deletions examples/get_hw.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/get_hw.py
"""Print the local node hardware model.

Purpose: show the narrowest read-only local hardware lookup.
Transport scope: Serial only.
Behavior: reads local node metadata and prints hwModel.
Expected output: one hardware model line, if available.
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
"""

import argparse
import sys

import meshtastic
import meshtastic.serial_interface

# simple arg check
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Print the hardware model for the local node.")
sys.exit(3)

iface = meshtastic.serial_interface.SerialInterface()
if iface.nodes:
for n in iface.nodes.values():
if n["num"] == iface.myInfo.my_node_num:
print(n["user"]["hwModel"])
iface.close()

def main() -> int:
"""Print the hardware model for the local node."""
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Print the hardware model for the local node.")
return 3

parser = argparse.ArgumentParser(description="Print local Meshtastic hardware model")
parser.parse_args()

try:
with meshtastic.serial_interface.SerialInterface() as iface:
if iface.nodes:
for node in iface.nodes.values():
if node["num"] == iface.myInfo.my_node_num:
print(node["user"]["hwModel"])
break
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read hardware model: {exc}")
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
43 changes: 31 additions & 12 deletions examples/hello_world_serial.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/hello_world_serial.py
"""Send one text message over serial.

Purpose: minimal send-only example.
Transport scope: Serial only.
Behavior: sends one message and exits.
Expected output: no output on success.
Cleanup/error handling: exits with code 3 for bad args, closes interface on exit.
"""

import argparse
import sys

import meshtastic
import meshtastic.serial_interface

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

# By default will try to find a meshtastic device,
# otherwise provide a device path like /dev/ttyUSB0
iface = meshtastic.serial_interface.SerialInterface()
iface.sendText(sys.argv[1])
iface.close()
def main() -> int:
"""Parse arguments and send one text message."""
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} message")
return 3

parser = argparse.ArgumentParser(description="Send one Meshtastic text message over serial")
parser.add_argument("message", help="Message text to broadcast")
args = parser.parse_args()

try:
with meshtastic.serial_interface.SerialInterface() as iface:
iface.sendText(args.message)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not send message: {exc}")
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
50 changes: 38 additions & 12 deletions examples/info_example.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/info.py
"""Show a concise local node summary over serial.

Purpose: read local node identity and metadata in one place.
Transport scope: Serial only.
Behavior: reads node database, prints local node ID/name/hardware model.
Expected output: 1-3 summary lines describing the local node.
Cleanup/error handling: closes interface on exit and prints clear errors on failure.
"""

import meshtastic
import meshtastic.serial_interface

iface = meshtastic.serial_interface.SerialInterface()

# call showInfo() just to ensure values are populated
# info = iface.showInfo()
def main() -> int:
"""Print local node summary fields."""
try:
with meshtastic.serial_interface.SerialInterface() as iface:
local_num = iface.myInfo.my_node_num
local_node = None
if iface.nodes:
for node in iface.nodes.values():
if node["num"] == local_num:
local_node = node
break

if not local_node:
print(f"Local node not found in node database (node num: {local_num}).")
return 1

user = local_node.get("user", {})
print(f"Node number: {local_num}")
print(f"Node ID: {local_node.get('id', 'unknown')}")
print(
"Name: "
f"{user.get('longName', 'unknown')} ({user.get('shortName', 'unknown')})"
)
print(f"Hardware model: {user.get('hwModel', 'unknown')}")
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not read local node summary: {exc}")
return 1
return 0

if iface.nodes:
for n in iface.nodes.values():
if n["num"] == iface.myInfo.my_node_num:
print(n["user"]["hwModel"])
break

iface.close()
if __name__ == "__main__":
raise SystemExit(main())
64 changes: 64 additions & 0 deletions examples/meshtastic_serial_message_reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Passively monitor incoming text messages over serial.

Purpose: receive-only monitor for text messages.
Transport scope: Serial only.
Behavior: subscribes to text receive events and prints timestamp/channel/sender/text.
Expected output: one line per received text message.
Cleanup/error handling: graceful Ctrl+C exit and explicit connection errors.
"""

import argparse
import time
from datetime import datetime
from typing import Any, Optional

from pubsub import pub
import meshtastic.serial_interface

_TZ_NAME = time.tzname[time.localtime().tm_isdst > 0]


def on_receive(packet: dict[str, Any], interface: Any) -> None: # pylint: disable=unused-argument
"""Print a compact line for each received text packet."""
decoded = packet.get("decoded", {})
if decoded.get("portnum") != "TEXT_MESSAGE_APP":
return

message = decoded.get("text")
if not message:
return

channel_num = packet.get("channel", 0)
sender_id = packet.get("fromId", "unknown")
message_time = datetime.now().strftime(f"%a %b %d %Y %H:%M:%S {_TZ_NAME}")
print(f"{message_time} : {channel_num} : {sender_id} : {message}")


def main() -> int:
"""Connect over serial and print inbound text messages."""
parser = argparse.ArgumentParser(description="Read incoming Meshtastic text over serial")
parser.add_argument("--port", default=None, help="Serial port path (default: auto-detect)")
args = parser.parse_args()

pub.subscribe(on_receive, "meshtastic.receive")

iface: Optional[meshtastic.serial_interface.SerialInterface] = None
try:
iface = meshtastic.serial_interface.SerialInterface(devPath=args.port)
print("Connected. Listening for text messages. Press Ctrl+C to exit.")
while True:
time.sleep(1)
except KeyboardInterrupt:
return 0
except Exception as exc:
print(f"Error: Could not monitor serial messages: {exc}")
return 1
finally:
if iface:
iface.close()
return 0


if __name__ == "__main__":
raise SystemExit(main())
30 changes: 0 additions & 30 deletions examples/pub_sub_example.py

This file was deleted.

42 changes: 0 additions & 42 deletions examples/pub_sub_example2.py

This file was deleted.

Loading
Loading