Skip to content

Commit 125f634

Browse files
authored
Merge branch 'master' into client-notifications
2 parents f5fa30c + cad5d18 commit 125f634

41 files changed

Lines changed: 2187 additions & 310 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/meshtastic_logo.png

105 KB
Loading
File renamed without changes.

README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
# Meshtastic Python
1+
<div align="center" markdown="1">
2+
3+
<img src=".github/meshtastic_logo.png" alt="Meshtastic Logo" width="80"/>
4+
5+
<h1 align="center"> Meshtastic Python
6+
</h1>
7+
<p style="font-size:15px;" align="center">A Python library and client for use with Meshtastic devices. </p>
28

39
[![codecov](https://codecov.io/gh/meshtastic/python/branch/master/graph/badge.svg?token=TIWPJL73KV)](https://codecov.io/gh/meshtastic/python)
410
![PyPI - Downloads](https://img.shields.io/pypi/dm/meshtastic)
@@ -7,17 +13,20 @@
713
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
814
![GPL-3.0](https://img.shields.io/badge/License-GPL%20v3-blue.svg)
915

16+
</div>
17+
18+
<div align="center">
19+
<a href="https://meshtastic.org/docs/software/python/cli/installation">Getting Started Guide</a>
20+
-
21+
<a href="https://python.meshtastic.org">API Documentation</a>
22+
</div>
23+
1024
## Overview
1125

12-
A Python client for use with Meshtastic devices.
1326
This small library (and example application) provides an easy API for sending and receiving messages over mesh radios.
1427
It also provides access to any of the operations/data available in the device user interface or the Android application.
1528
Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in.
1629

17-
**[Getting Started Guide](https://meshtastic.org/docs/software/python/cli/installation)**
18-
19-
**[API Documentation](https://python.meshtastic.org)**
20-
2130
## Call for Contributors
2231

2332
This library and CLI has gone without a consistent maintainer for a while, and there's many improvements that could be made. We're all volunteers here and help is extremely appreciated, whether in implementing your own needs or helping maintain the library and CLI in general.

bin/regen-protobufs.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ set -e
66
#gsed -i 's/import "\//import ".\//g' ./protobufs/meshtastic/*
77
#gsed -i 's/package meshtastic;//g' ./protobufs/meshtastic/*
88

9+
POETRYDIR=$(poetry env info --path)
10+
11+
if [[ -z "${POETRYDIR}" ]]; then
12+
poetry install
13+
fi
14+
915
# protoc looks for mypy plugin in the python path
1016
source $(poetry env info --path)/bin/activate
1117

@@ -22,6 +28,7 @@ OUTDIR=${TMPDIR}/out
2228
PYIDIR=${TMPDIR}/out
2329
mkdir -p "${OUTDIR}" "${INDIR}" "${PYIDIR}"
2430
cp ./protobufs/meshtastic/*.proto "${INDIR}"
31+
cp ./protobufs/nanopb.proto "${INDIR}"
2532

2633
# OS-X sed is apparently a little different and expects an arg for -i
2734
if [[ $OSTYPE == 'darwin'* ]]; then
@@ -36,6 +43,8 @@ $SEDCMD 's/^package meshtastic;/package meshtastic.protobuf;/' "${INDIR}/"*.prot
3643
# fix the imports to match
3744
$SEDCMD 's/^import "meshtastic\//import "meshtastic\/protobuf\//' "${INDIR}/"*.proto
3845

46+
$SEDCMD 's/^import "nanopb.proto"/import "meshtastic\/protobuf\/nanopb.proto"/' "${INDIR}/"*.proto
47+
3948
# Generate the python files
4049
./nanopb-0.4.8/generator-bin/protoc -I=$TMPDIR/in --python_out "${OUTDIR}" "--mypy_out=${PYIDIR}" $INDIR/*.proto
4150

meshtastic/__main__.py

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
""" Main Meshtastic
22
"""
3-
# pylint: disable=C0302
3+
4+
# We just hit the 1600 line limit for main.py, but I currently have a huge set of powermon/structured logging changes
5+
# later we can have a separate changelist to refactor main.py into smaller files
6+
# pylint: disable=R0917,C0302
47

58
from typing import List, Optional, Union
69
from types import ModuleType
@@ -339,6 +342,18 @@ def onConnected(interface):
339342
if args.set_owner or args.set_owner_short:
340343
closeNow = True
341344
waitForAckNak = True
345+
346+
# Validate owner names before connecting to device
347+
if args.set_owner is not None:
348+
stripped_long_name = args.set_owner.strip()
349+
if not stripped_long_name:
350+
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
351+
352+
if hasattr(args, 'set_owner_short') and args.set_owner_short is not None:
353+
stripped_short_name = args.set_owner_short.strip()
354+
if not stripped_short_name:
355+
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
356+
342357
if args.set_owner and args.set_owner_short:
343358
print(f"Setting device owner to {args.set_owner} and short name to {args.set_owner_short}")
344359
elif args.set_owner:
@@ -399,6 +414,8 @@ def onConnected(interface):
399414
print(" ".join(fieldNames))
400415

401416
if args.set_ham:
417+
if not args.set_ham.strip():
418+
meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters")
402419
closeNow = True
403420
print(f"Setting Ham ID to {args.set_ham} and turning off encryption")
404421
interface.getNode(args.dest, **getNode_kwargs).setOwner(args.set_ham, is_licensed=True)
@@ -594,6 +611,7 @@ def onConnected(interface):
594611

595612
# Handle the int/float/bool arguments
596613
pref = None
614+
fields = set()
597615
for pref in args.set:
598616
found = False
599617
field = splitCompoundName(pref[0].lower())[0]
@@ -606,11 +624,19 @@ def onConnected(interface):
606624
)
607625
found = setPref(config, pref[0], pref[1])
608626
if found:
627+
fields.add(field)
609628
break
610629

611630
if found:
612631
print("Writing modified preferences to device")
613-
node.writeConfig(field)
632+
if len(fields) > 1:
633+
print("Using a configuration transaction")
634+
node.beginSettingsTransaction()
635+
for field in fields:
636+
print(f"Writing {field} configuration to device")
637+
node.writeConfig(field)
638+
if len(fields) > 1:
639+
node.commitSettingsTransaction()
614640
else:
615641
if mt_config.camel_case:
616642
print(
@@ -632,35 +658,52 @@ def onConnected(interface):
632658
interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction()
633659

634660
if "owner" in configuration:
661+
# Validate owner name before setting
662+
owner_name = str(configuration["owner"]).strip()
663+
if not owner_name:
664+
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
635665
print(f"Setting device owner to {configuration['owner']}")
636666
waitForAckNak = True
637667
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(configuration["owner"])
668+
time.sleep(0.5)
638669

639670
if "owner_short" in configuration:
671+
# Validate owner short name before setting
672+
owner_short_name = str(configuration["owner_short"]).strip()
673+
if not owner_short_name:
674+
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
640675
print(
641676
f"Setting device owner short to {configuration['owner_short']}"
642677
)
643678
waitForAckNak = True
644679
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
645680
long_name=None, short_name=configuration["owner_short"]
646681
)
682+
time.sleep(0.5)
647683

648684
if "ownerShort" in configuration:
685+
# Validate owner short name before setting
686+
owner_short_name = str(configuration["ownerShort"]).strip()
687+
if not owner_short_name:
688+
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
649689
print(
650690
f"Setting device owner short to {configuration['ownerShort']}"
651691
)
652692
waitForAckNak = True
653693
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
654694
long_name=None, short_name=configuration["ownerShort"]
655695
)
696+
time.sleep(0.5)
656697

657698
if "channel_url" in configuration:
658699
print("Setting channel url to", configuration["channel_url"])
659700
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channel_url"])
701+
time.sleep(0.5)
660702

661703
if "channelUrl" in configuration:
662704
print("Setting channel url to", configuration["channelUrl"])
663705
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channelUrl"])
706+
time.sleep(0.5)
664707

665708
if "location" in configuration:
666709
alt = 0
@@ -679,6 +722,7 @@ def onConnected(interface):
679722
print(f"Fixing longitude at {lon} degrees")
680723
print("Setting device position")
681724
interface.localNode.setFixedPosition(lat, lon, alt)
725+
time.sleep(0.5)
682726

683727
if "config" in configuration:
684728
localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig
@@ -689,6 +733,7 @@ def onConnected(interface):
689733
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
690734
meshtastic.util.camel_to_snake(section)
691735
)
736+
time.sleep(0.5)
692737

693738
if "module_config" in configuration:
694739
moduleConfig = interface.getNode(args.dest, **getNode_kwargs).moduleConfig
@@ -701,6 +746,7 @@ def onConnected(interface):
701746
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
702747
meshtastic.util.camel_to_snake(section)
703748
)
749+
time.sleep(0.5)
704750

705751
interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction()
706752
print("Writing modified configuration to device")
@@ -709,9 +755,20 @@ def onConnected(interface):
709755
if args.dest != BROADCAST_ADDR:
710756
print("Exporting configuration of remote nodes is not supported.")
711757
return
712-
# export the configuration (the opposite of '--configure')
758+
713759
closeNow = True
714-
export_config(interface)
760+
config_txt = export_config(interface)
761+
762+
if args.export_config == "-":
763+
# Output to stdout (preserves legacy use of `> file.yaml`)
764+
print(config_txt)
765+
else:
766+
try:
767+
with open(args.export_config, "w", encoding="utf-8") as f:
768+
f.write(config_txt)
769+
print(f"Exported configuration to {args.export_config}")
770+
except Exception as e:
771+
meshtastic.util.our_exit(f"ERROR: Failed to write config file: {e}")
715772

716773
if args.ch_set_url:
717774
closeNow = True
@@ -1076,6 +1133,7 @@ def export_config(interface) -> str:
10761133
configObj["location"]["alt"] = alt
10771134

10781135
config = MessageToDict(interface.localNode.localConfig) #checkme - Used as a dictionary here and a string below
1136+
#was used as a string here and a Dictionary above
10791137
if config:
10801138
# Convert inner keys to correct snake/camelCase
10811139
prefs = {}
@@ -1113,7 +1171,6 @@ def export_config(interface) -> str:
11131171
config_txt = "# start of Meshtastic configure yaml\n" #checkme - "config" (now changed to config_out)
11141172
#was used as a string here and a Dictionary above
11151173
config_txt += yaml.dump(configObj)
1116-
print(config_txt)
11171174
return config_txt
11181175

11191176

@@ -1170,6 +1227,22 @@ def common():
11701227
meshtastic.util.support_info()
11711228
meshtastic.util.our_exit("", 0)
11721229

1230+
# Early validation for owner names before attempting device connection
1231+
if hasattr(args, 'set_owner') and args.set_owner is not None:
1232+
stripped_long_name = args.set_owner.strip()
1233+
if not stripped_long_name:
1234+
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
1235+
1236+
if hasattr(args, 'set_owner_short') and args.set_owner_short is not None:
1237+
stripped_short_name = args.set_owner_short.strip()
1238+
if not stripped_short_name:
1239+
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
1240+
1241+
if hasattr(args, 'set_ham') and args.set_ham is not None:
1242+
stripped_ham_name = args.set_ham.strip()
1243+
if not stripped_ham_name:
1244+
meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters")
1245+
11731246
if have_powermon:
11741247
create_power_meter()
11751248

@@ -1397,8 +1470,10 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
13971470
)
13981471
group.add_argument(
13991472
"--export-config",
1400-
help="Export the configuration in yaml(.yml) format.",
1401-
action="store_true",
1473+
nargs="?",
1474+
const="-", # default to "-" if no value provided
1475+
metavar="FILE",
1476+
help="Export device config as YAML (to stdout if no file given)"
14021477
)
14031478
return parser
14041479

@@ -1414,7 +1489,7 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
14141489
"--get",
14151490
help=(
14161491
"Get a preferences field. Use an invalid field such as '0' to get a list of all fields."
1417-
" Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')"
1492+
" Can use either snake_case or camelCase format. (ex: 'power.ls_secs' or 'power.lsSecs')"
14181493
),
14191494
nargs=1,
14201495
action="append",
@@ -1423,7 +1498,11 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
14231498

14241499
group.add_argument(
14251500
"--set",
1426-
help="Set a preferences field. Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')",
1501+
help=(
1502+
"Set a preferences field. Can use either snake_case or camelCase format."
1503+
" (ex: 'power.ls_secs' or 'power.lsSecs'). May be less reliable when"
1504+
" setting properties from more than one configuration section."
1505+
),
14271506
nargs=2,
14281507
action="append",
14291508
metavar=("FIELD", "VALUE"),

meshtastic/ble_interface.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ def __init__(
8383
# Note: the on disconnected callback will call our self.close which will make us nicely wait for threads to exit
8484
self._exit_handler = atexit.register(self.client.disconnect)
8585

86+
def __repr__(self):
87+
rep = f"BLEInterface(address={self.client.address if self.client else None!r}"
88+
if self.debugOut is not None:
89+
rep += f", debugOut={self.debugOut!r}"
90+
if self.noProto:
91+
rep += ", noProto=True"
92+
if self.noNodes:
93+
rep += ", noNodes=True"
94+
rep += ")"
95+
return rep
96+
8697
def from_num_handler(self, _, b: bytes) -> None: # pylint: disable=C0116
8798
"""Handle callbacks for fromnum notify.
8899
Note: this method does not need to be async because it is just setting a bool.
@@ -163,7 +174,7 @@ def connect(self, address: Optional[str] = None) -> "BLEClient":
163174

164175
# Bleak docs recommend always doing a scan before connecting (even if we know addr)
165176
device = self.find_device(address)
166-
client = BLEClient(device.address, disconnected_callback=lambda _: self.close)
177+
client = BLEClient(device.address, disconnected_callback=lambda _: self.close())
167178
client.connect()
168179
client.discover()
169180
return client

0 commit comments

Comments
 (0)