Skip to content

Commit 20d75d9

Browse files
authored
Merge branch 'master' into wls_add_types2
2 parents e335f12 + 0deb1d7 commit 20d75d9

18 files changed

+1821
-1497
lines changed

meshtastic/__init__.py

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,44 @@
22
# A library for the Meshtastic Client API
33
44
Primary interfaces: SerialInterface, TCPInterface, BLEInterface
5+
56
Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)"
7+
68
Source code on [github](https://github.com/meshtastic/python)
79
810
notable properties of interface classes:
911
10-
- nodes - The database of received nodes. Includes always up-to-date location and username information for each
12+
- `nodes` - The database of received nodes. Includes always up-to-date location and username information for each
1113
node in the mesh. This is a read-only datastructure.
12-
- nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId
13-
- myInfo & metadata - Contain read-only information about the local radio device (software version, hardware version, etc)
14-
- localNode - Pointer to a node object for the local node
14+
- `nodesByNum` - like "nodes" but keyed by nodeNum instead of nodeId. As such, includes "unknown" nodes which haven't seen a User packet yet
15+
- `myInfo` & `metadata` - Contain read-only information about the local radio device (software version, hardware version, etc)
16+
- `localNode` - Pointer to a node object for the local node
1517
1618
notable properties of nodes:
17-
- localConfig - Current radio settings, can be written to the radio with the `writeConfig` method.
18-
- moduleConfig - Current module settings, can be written to the radio with the `writeConfig` method.
19-
- channels - The node's channels, keyed by index.
19+
20+
- `localConfig` - Current radio settings, can be written to the radio with the `writeConfig` method.
21+
- `moduleConfig` - Current module settings, can be written to the radio with the `writeConfig` method.
22+
- `channels` - The node's channels, keyed by index.
2023
2124
# Published PubSub topics
2225
2326
We use a [publish-subscribe](https://pypubsub.readthedocs.io/en/v4.0.3/) model to communicate asynchronous events. Available
2427
topics:
2528
26-
- meshtastic.connection.established - published once we've successfully connected to the radio and downloaded the node DB
27-
- meshtastic.connection.lost - published once we've lost our link to the radio
28-
- meshtastic.receive.text(packet) - delivers a received packet as a dictionary, if you only care about a particular
29+
- `meshtastic.connection.established` - published once we've successfully connected to the radio and downloaded the node DB
30+
- `meshtastic.connection.lost` - published once we've lost our link to the radio
31+
- `meshtastic.receive.text(packet)` - delivers a received packet as a dictionary, if you only care about a particular
2932
type of packet, you should subscribe to the full topic name. If you want to see all packets, simply subscribe to "meshtastic.receive".
30-
- meshtastic.receive.position(packet)
31-
- meshtastic.receive.user(packet)
32-
- meshtastic.receive.data.portnum(packet) (where portnum is an integer or well known PortNum enum)
33-
- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
34-
- meshtastic.log.line(line) - a raw unparsed log line from the radio
35-
36-
We receive position, user, or data packets from the mesh. You probably only care about meshtastic.receive.data. The first argument for
37-
that publish will be the packet. Text or binary data packets (from sendData or sendText) will both arrive this way. If you print packet
38-
you'll see the fields in the dictionary. decoded.data.payload will contain the raw bytes that were sent. If the packet was sent with
39-
sendText, decoded.data.text will **also** be populated with the decoded string. For ASCII these two strings will be the same, but for
33+
- `meshtastic.receive.position(packet)`
34+
- `meshtastic.receive.user(packet)`
35+
- `meshtastic.receive.data.portnum(packet)` (where portnum is an integer or well known PortNum enum)
36+
- `meshtastic.node.updated(node = NodeInfo)` - published when a node in the DB changes (appears, location changed, username changed, etc...)
37+
- `meshtastic.log.line(line)` - a raw unparsed log line from the radio
38+
39+
We receive position, user, or data packets from the mesh. You probably only care about `meshtastic.receive.data`. The first argument for
40+
that publish will be the packet. Text or binary data packets (from `sendData` or `sendText`) will both arrive this way. If you print packet
41+
you'll see the fields in the dictionary. `decoded.data.payload` will contain the raw bytes that were sent. If the packet was sent with
42+
`sendText`, `decoded.data.text` will **also** be populated with the decoded string. For ASCII these two strings will be the same, but for
4043
unicode scripts they can be different.
4144
4245
# Example Usage
@@ -131,19 +134,21 @@ class ResponseHandler(NamedTuple):
131134
"""A pending response callback, waiting for a response to one of our messages"""
132135

133136
# requestId: int - used only as a key
137+
#: a callable to call when a response is received
134138
callback: Callable
139+
#: Whether ACKs and NAKs should be passed to this handler
135140
ackPermitted: bool = False
136141
# FIXME, add timestamp and age out old requests
137142

138143

139144
class KnownProtocol(NamedTuple):
140145
"""Used to automatically decode known protocol payloads"""
141146

147+
#: A descriptive name (e.g. "text", "user", "admin")
142148
name: str
143-
# portnum: int, now a key
144-
# If set, will be called to prase as a protocol buffer
149+
#: If set, will be called to parse as a protocol buffer
145150
protobufFactory: Optional[Callable] = None
146-
# If set, invoked as onReceive(interface, packet)
151+
#: If set, invoked as onReceive(interface, packet)
147152
onReceive: Optional[Callable] = None
148153

149154

@@ -194,13 +199,31 @@ def _onNodeInfoReceive(iface, asDict):
194199
def _onTelemetryReceive(iface, asDict):
195200
"""Automatically update device metrics on received packets"""
196201
logging.debug(f"in _onTelemetryReceive() asDict:{asDict}")
197-
deviceMetrics = asDict.get("decoded", {}).get("telemetry", {}).get("deviceMetrics")
198-
if "from" in asDict and deviceMetrics is not None:
199-
node = iface._getOrCreateByNum(asDict["from"])
200-
newMetrics = node.get("deviceMetrics", {})
201-
newMetrics.update(deviceMetrics)
202-
logging.debug(f"updating metrics for {asDict['from']} to {newMetrics}")
203-
node["deviceMetrics"] = newMetrics
202+
if "from" not in asDict:
203+
return
204+
205+
toUpdate = None
206+
207+
telemetry = asDict.get("decoded", {}).get("telemetry", {})
208+
node = iface._getOrCreateByNum(asDict["from"])
209+
if "deviceMetrics" in telemetry:
210+
toUpdate = "deviceMetrics"
211+
elif "environmentMetrics" in telemetry:
212+
toUpdate = "environmentMetrics"
213+
elif "airQualityMetrics" in telemetry:
214+
toUpdate = "airQualityMetrics"
215+
elif "powerMetrics" in telemetry:
216+
toUpdate = "powerMetrics"
217+
elif "localStats" in telemetry:
218+
toUpdate = "localStats"
219+
else:
220+
return
221+
222+
updateObj = telemetry.get(toUpdate)
223+
newMetrics = node.get(toUpdate, {})
224+
newMetrics.update(updateObj)
225+
logging.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}")
226+
node[toUpdate] = newMetrics
204227

205228
def _receiveInfoUpdate(iface, asDict):
206229
if "from" in asDict:

meshtastic/__main__.py

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,22 @@ def checkChannel(interface: MeshInterface, channelIndex: int) -> bool:
8787

8888
def getPref(node, comp_name) -> bool:
8989
"""Get a channel or preferences value"""
90+
def _printSetting(config_type, uni_name, pref_value, repeated):
91+
"""Pretty print the setting"""
92+
if repeated:
93+
pref_value = [meshtastic.util.toStr(v) for v in pref_value]
94+
else:
95+
pref_value = meshtastic.util.toStr(pref_value)
96+
print(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}")
97+
logging.debug(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}")
9098

9199
name = splitCompoundName(comp_name)
92100
wholeField = name[0] == name[1] # We want the whole field
93101

94102
camel_name = meshtastic.util.snake_to_camel(name[1])
95103
# Note: protobufs has the keys in snake_case, so snake internally
96104
snake_name = meshtastic.util.camel_to_snake(name[1])
105+
uni_name = camel_name if mt_config.camel_case else snake_name
97106
logging.debug(f"snake_name:{snake_name} camel_name:{camel_name}")
98107
logging.debug(f"use camel:{mt_config.camel_case}")
99108

@@ -112,14 +121,9 @@ def getPref(node, comp_name) -> bool:
112121
break
113122

114123
if not found:
115-
if mt_config.camel_case:
116-
print(
117-
f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have an attribute {snake_name}."
118-
)
119-
else:
120-
print(
121-
f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have attribute {snake_name}."
122-
)
124+
print(
125+
f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have attribute {uni_name}."
126+
)
123127
print("Choices are...")
124128
printConfig(localConfig)
125129
printConfig(moduleConfig)
@@ -131,19 +135,12 @@ def getPref(node, comp_name) -> bool:
131135
config_values = getattr(config, config_type.name)
132136
if not wholeField:
133137
pref_value = getattr(config_values, pref.name)
134-
if mt_config.camel_case:
135-
print(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}")
136-
logging.debug(
137-
f"{str(config_type.name)}.{camel_name}: {str(pref_value)}"
138-
)
139-
else:
140-
print(f"{str(config_type.name)}.{snake_name}: {str(pref_value)}")
141-
logging.debug(
142-
f"{str(config_type.name)}.{snake_name}: {str(pref_value)}"
143-
)
138+
repeated = pref.label == pref.LABEL_REPEATED
139+
_printSetting(config_type, uni_name, pref_value, repeated)
144140
else:
145-
print(f"{str(config_type.name)}:\n{str(config_values)}")
146-
logging.debug(f"{str(config_type.name)}: {str(config_values)}")
141+
for field in config_values.ListFields():
142+
repeated = field[0].label == field[0].LABEL_REPEATED
143+
_printSetting(config_type, field[0].name, field[1], repeated)
147144
else:
148145
# Always show whole field for remote node
149146
node.requestConfig(config_type)
@@ -168,18 +165,19 @@ def traverseConfig(config_root, config, interface_config) -> bool:
168165
if isinstance(config[pref], dict):
169166
traverseConfig(pref_name, config[pref], interface_config)
170167
else:
171-
setPref(interface_config, pref_name, str(config[pref]))
168+
setPref(interface_config, pref_name, config[pref])
172169

173170
return True
174171

175172

176-
def setPref(config, comp_name, valStr) -> bool:
173+
def setPref(config, comp_name, raw_val) -> bool:
177174
"""Set a channel or preferences value"""
178175

179176
name = splitCompoundName(comp_name)
180177

181178
snake_name = meshtastic.util.camel_to_snake(name[-1])
182179
camel_name = meshtastic.util.snake_to_camel(name[-1])
180+
uni_name = camel_name if mt_config.camel_case else snake_name
183181
logging.debug(f"snake_name:{snake_name}")
184182
logging.debug(f"camel_name:{camel_name}")
185183

@@ -201,10 +199,13 @@ def setPref(config, comp_name, valStr) -> bool:
201199
if (not pref) or (not config_type):
202200
return False
203201

204-
val = meshtastic.util.fromStr(valStr)
205-
logging.debug(f"valStr:{valStr} val:{val}")
202+
if isinstance(raw_val, str):
203+
val = meshtastic.util.fromStr(raw_val)
204+
else:
205+
val = raw_val
206+
logging.debug(f"valStr:{raw_val} val:{val}")
206207

207-
if snake_name == "wifi_psk" and len(valStr) < 8:
208+
if snake_name == "wifi_psk" and len(str(raw_val)) < 8:
208209
print(f"Warning: network.wifi_psk must be 8 or more characters.")
209210
return False
210211

@@ -216,14 +217,9 @@ def setPref(config, comp_name, valStr) -> bool:
216217
if e:
217218
val = e.number
218219
else:
219-
if mt_config.camel_case:
220-
print(
221-
f"{name[0]}.{camel_name} does not have an enum called {val}, so you can not set it."
222-
)
223-
else:
224-
print(
225-
f"{name[0]}.{snake_name} does not have an enum called {val}, so you can not set it."
226-
)
220+
print(
221+
f"{name[0]}.{uni_name} does not have an enum called {val}, so you can not set it."
222+
)
227223
print(f"Choices in sorted order are:")
228224
names = []
229225
for f in enumType.values:
@@ -244,24 +240,26 @@ def setPref(config, comp_name, valStr) -> bool:
244240
except TypeError:
245241
# The setter didn't like our arg type guess try again as a string
246242
config_values = getattr(config_part, config_type.name)
247-
setattr(config_values, pref.name, valStr)
243+
setattr(config_values, pref.name, str(val))
244+
elif type(val) == list:
245+
new_vals = [meshtastic.util.fromStr(x) for x in val]
246+
config_values = getattr(config, config_type.name)
247+
getattr(config_values, pref.name)[:] = new_vals
248248
else:
249249
config_values = getattr(config, config_type.name)
250250
if val == 0:
251251
# clear values
252252
print(f"Clearing {pref.name} list")
253253
del getattr(config_values, pref.name)[:]
254254
else:
255-
print(f"Adding '{val}' to the {pref.name} list")
255+
print(f"Adding '{raw_val}' to the {pref.name} list")
256256
cur_vals = [x for x in getattr(config_values, pref.name) if x not in [0, "", b""]]
257257
cur_vals.append(val)
258258
getattr(config_values, pref.name)[:] = cur_vals
259+
return True
259260

260261
prefix = f"{'.'.join(name[0:-1])}." if config_type.message_type is not None else ""
261-
if mt_config.camel_case:
262-
print(f"Set {prefix}{camel_name} to {valStr}")
263-
else:
264-
print(f"Set {prefix}{snake_name} to {valStr}")
262+
print(f"Set {prefix}{uni_name} to {raw_val}")
265263

266264
return True
267265

@@ -475,13 +473,22 @@ def onConnected(interface):
475473
else:
476474
channelIndex = mt_config.channel_index or 0
477475
if checkChannel(interface, channelIndex):
476+
telemMap = {
477+
"device": "device_metrics",
478+
"environment": "environment_metrics",
479+
"air_quality": "air_quality_metrics",
480+
"airquality": "air_quality_metrics",
481+
"power": "power_metrics",
482+
}
483+
telemType = telemMap.get(args.request_telemetry, "device_metrics")
478484
print(
479-
f"Sending telemetry request to {args.dest} on channelIndex:{channelIndex} (this could take a while)"
485+
f"Sending {telemType} telemetry request to {args.dest} on channelIndex:{channelIndex} (this could take a while)"
480486
)
481487
interface.sendTelemetry(
482488
destinationId=args.dest,
483489
wantResponse=True,
484490
channelIndex=channelIndex,
491+
telemetryType=telemType,
485492
)
486493

487494
if args.request_position:
@@ -622,19 +629,15 @@ def onConnected(interface):
622629

623630
if "alt" in configuration["location"]:
624631
alt = int(configuration["location"]["alt"] or 0)
625-
localConfig.position.fixed_position = True
626632
print(f"Fixing altitude at {alt} meters")
627633
if "lat" in configuration["location"]:
628634
lat = float(configuration["location"]["lat"] or 0)
629-
localConfig.position.fixed_position = True
630635
print(f"Fixing latitude at {lat} degrees")
631636
if "lon" in configuration["location"]:
632637
lon = float(configuration["location"]["lon"] or 0)
633-
localConfig.position.fixed_position = True
634638
print(f"Fixing longitude at {lon} degrees")
635639
print("Setting device position")
636-
interface.sendPosition(lat, lon, alt)
637-
interface.localNode.writeConfig("position")
640+
interface.localNode.setFixedPosition(lat, lon, alt)
638641

639642
if "config" in configuration:
640643
localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig
@@ -1029,6 +1032,15 @@ def export_config(interface) -> str:
10291032
prefs[meshtastic.util.snake_to_camel(pref)] = config[pref]
10301033
else:
10311034
prefs[pref] = config[pref]
1035+
# mark base64 encoded fields as such
1036+
if pref == "security":
1037+
if 'privateKey' in prefs[pref]:
1038+
prefs[pref]['privateKey'] = 'base64:' + prefs[pref]['privateKey']
1039+
if 'publicKey' in prefs[pref]:
1040+
prefs[pref]['publicKey'] = 'base64:' + prefs[pref]['publicKey']
1041+
if 'adminKey' in prefs[pref]:
1042+
for i in range(len(prefs[pref]['adminKey'])):
1043+
prefs[pref]['adminKey'][i] = 'base64:' + prefs[pref]['adminKey'][i]
10321044
if mt_config.camel_case:
10331045
configObj["config"] = config #Identical command here and 2 lines below?
10341046
else:
@@ -1593,10 +1605,14 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
15931605

15941606
group.add_argument(
15951607
"--request-telemetry",
1596-
help="Request telemetry from a node. "
1608+
help="Request telemetry from a node. With an argument, requests that specific type of telemetry. "
15971609
"You need to pass the destination ID as argument with '--dest'. "
15981610
"For repeaters, the nodeNum is required.",
1599-
action="store_true",
1611+
action="store",
1612+
nargs="?",
1613+
default=None,
1614+
const="device",
1615+
metavar="TYPE",
16001616
)
16011617

16021618
group.add_argument(
@@ -1843,7 +1859,7 @@ def initParser():
18431859

18441860
power_group.add_argument(
18451861
"--slog",
1846-
help="Store structured-logs (slogs) for this run, optionally you can specifiy a destination directory",
1862+
help="Store structured-logs (slogs) for this run, optionally you can specify a destination directory",
18471863
nargs="?",
18481864
default=None,
18491865
const="default",

meshtastic/ble_interface.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def close(self) -> None:
222222
logging.error(f"Error closing mesh interface: {e}")
223223

224224
if self._want_receive:
225-
self.want_receive = False # Tell the thread we want it to stop
225+
self._want_receive = False # Tell the thread we want it to stop
226226
if self._receiveThread:
227227
self._receiveThread.join(
228228
timeout=2
@@ -234,6 +234,7 @@ def close(self) -> None:
234234
self.client.disconnect()
235235
self.client.close()
236236
self.client = None
237+
self._disconnected() # send the disconnected indicator up to clients
237238

238239

239240
class BLEClient:

0 commit comments

Comments
 (0)