mirror of
https://github.com/meshtastic/python.git
synced 2026-01-02 21:07:55 -05:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2c9c1315e | ||
|
|
f41ef042a9 | ||
|
|
84bec5a7c4 | ||
|
|
985366c812 | ||
|
|
3954fbd404 | ||
|
|
5f174b2850 | ||
|
|
23ea19c00b | ||
|
|
acc47146c9 | ||
|
|
dd8803793d | ||
|
|
68ec588804 | ||
|
|
2f48594dc3 | ||
|
|
c7c3c69fc3 | ||
|
|
53e40d5aab | ||
|
|
dd3da6a670 | ||
|
|
e500b399f4 | ||
|
|
27ac28e300 | ||
|
|
6bc5f5e305 | ||
|
|
c844e4e0fe | ||
|
|
060df86bb6 | ||
|
|
7d87d5037e | ||
|
|
4ec7698d94 | ||
|
|
7cc65aa08a | ||
|
|
cc411ce0bb | ||
|
|
edff956f9d | ||
|
|
bd68739158 | ||
|
|
530d92ead2 | ||
|
|
60f9dc6266 | ||
|
|
f9ae021e43 | ||
|
|
317d81c983 | ||
|
|
5837bd0172 | ||
|
|
5487f7a791 | ||
|
|
c6d8a540eb | ||
|
|
0962c9b058 | ||
|
|
4f98602ac2 | ||
|
|
6ebddb67c0 | ||
|
|
82554a1f18 | ||
|
|
8c115dc636 | ||
|
|
e2fe359527 | ||
|
|
5600ce92b0 | ||
|
|
efb848adf9 | ||
|
|
0d8646189f | ||
|
|
d0023df8ca | ||
|
|
b522abf33e | ||
|
|
c086b6372e | ||
|
|
6ec506fe3b | ||
|
|
fc3b81dfde | ||
|
|
9c53ea017c | ||
|
|
1e6625d062 | ||
|
|
0fb72b8ad1 | ||
|
|
74c911cb75 | ||
|
|
579383cd5a |
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -245,6 +245,23 @@
|
|||||||
"module": "meshtastic",
|
"module": "meshtastic",
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"args": ["--debug", "--nodes"]
|
"args": ["--debug", "--nodes"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meshtastic nodes table",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "meshtastic",
|
||||||
|
"justMyCode": true,
|
||||||
|
"args": ["--nodes"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meshtastic nodes table with show-fields",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"module": "meshtastic",
|
||||||
|
"justMyCode": true,
|
||||||
|
"args": ["--nodes", "--show-fields", "AKA,Pubkey,Role,Role,Role,Latitude,Latitude,deviceMetrics.voltage"]
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from types import ModuleType
|
|||||||
import argparse
|
import argparse
|
||||||
argcomplete: Union[None, ModuleType] = None
|
argcomplete: Union[None, ModuleType] = None
|
||||||
try:
|
try:
|
||||||
import argcomplete
|
import argcomplete # type: ignore
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
pass # already set to None by default above
|
pass # already set to None by default above
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ def setPref(config, comp_name, raw_val) -> bool:
|
|||||||
logging.debug(f"valStr:{raw_val} val:{val}")
|
logging.debug(f"valStr:{raw_val} val:{val}")
|
||||||
|
|
||||||
if snake_name == "wifi_psk" and len(str(raw_val)) < 8:
|
if snake_name == "wifi_psk" and len(str(raw_val)) < 8:
|
||||||
print(f"Warning: network.wifi_psk must be 8 or more characters.")
|
print("Warning: network.wifi_psk must be 8 or more characters.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
enumType = pref.enum_type
|
enumType = pref.enum_type
|
||||||
@@ -483,6 +483,7 @@ def onConnected(interface):
|
|||||||
if checkChannel(interface, channelIndex):
|
if checkChannel(interface, channelIndex):
|
||||||
print(
|
print(
|
||||||
f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}"
|
f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}"
|
||||||
|
f" {'using PRIVATE_APP port' if args.private else ''}"
|
||||||
)
|
)
|
||||||
interface.sendText(
|
interface.sendText(
|
||||||
args.sendtext,
|
args.sendtext,
|
||||||
@@ -490,6 +491,7 @@ def onConnected(interface):
|
|||||||
wantAck=True,
|
wantAck=True,
|
||||||
channelIndex=channelIndex,
|
channelIndex=channelIndex,
|
||||||
onResponse=interface.getNode(args.dest, False, **getNode_kwargs).onAckNak,
|
onResponse=interface.getNode(args.dest, False, **getNode_kwargs).onAckNak,
|
||||||
|
portNum=portnums_pb2.PortNum.PRIVATE_APP if args.private else portnums_pb2.PortNum.TEXT_MESSAGE_APP
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
meshtastic.util.our_exit(
|
meshtastic.util.our_exit(
|
||||||
@@ -714,12 +716,16 @@ def onConnected(interface):
|
|||||||
closeNow = True
|
closeNow = True
|
||||||
export_config(interface)
|
export_config(interface)
|
||||||
|
|
||||||
if args.seturl:
|
if args.ch_set_url:
|
||||||
closeNow = True
|
closeNow = True
|
||||||
interface.getNode(args.dest, **getNode_kwargs).setURL(args.seturl)
|
interface.getNode(args.dest, **getNode_kwargs).setURL(args.ch_set_url, addOnly=False)
|
||||||
|
|
||||||
# handle changing channels
|
# handle changing channels
|
||||||
|
|
||||||
|
if args.ch_add_url:
|
||||||
|
closeNow = True
|
||||||
|
interface.getNode(args.dest, **getNode_kwargs).setURL(args.ch_add_url, addOnly=True)
|
||||||
|
|
||||||
if args.ch_add:
|
if args.ch_add:
|
||||||
channelIndex = mt_config.channel_index
|
channelIndex = mt_config.channel_index
|
||||||
if channelIndex is not None:
|
if channelIndex is not None:
|
||||||
@@ -921,7 +927,11 @@ def onConnected(interface):
|
|||||||
if args.dest != BROADCAST_ADDR:
|
if args.dest != BROADCAST_ADDR:
|
||||||
print("Showing node list of a remote node is not supported.")
|
print("Showing node list of a remote node is not supported.")
|
||||||
return
|
return
|
||||||
interface.showNodes()
|
interface.showNodes(True, args.show_fields)
|
||||||
|
|
||||||
|
if args.show_fields and not args.nodes:
|
||||||
|
print("--show-fields can only be used with --nodes")
|
||||||
|
return
|
||||||
|
|
||||||
if args.qr or args.qr_all:
|
if args.qr or args.qr_all:
|
||||||
closeNow = True
|
closeNow = True
|
||||||
@@ -1245,6 +1255,19 @@ def common():
|
|||||||
noProto=args.noproto,
|
noProto=args.noproto,
|
||||||
noNodes=args.no_nodes,
|
noNodes=args.no_nodes,
|
||||||
)
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Handle the case where the serial device is not found
|
||||||
|
message = (
|
||||||
|
f"File Not Found Error:\n"
|
||||||
|
)
|
||||||
|
message += f" The serial device at '{args.port}' was not found.\n"
|
||||||
|
message += " Please check the following:\n"
|
||||||
|
message += " 1. Is the device connected properly?\n"
|
||||||
|
message += " 2. Is the correct serial port specified?\n"
|
||||||
|
message += " 3. Are the necessary drivers installed?\n"
|
||||||
|
message += " 4. Are you using a **power-only USB cable**? A power-only cable cannot transmit data.\n"
|
||||||
|
message += " Ensure you are using a **data-capable USB cable**.\n"
|
||||||
|
meshtastic.util.our_exit(message, 1)
|
||||||
except PermissionError as ex:
|
except PermissionError as ex:
|
||||||
username = os.getlogin()
|
username = os.getlogin()
|
||||||
message = "Permission Error:\n"
|
message = "Permission Error:\n"
|
||||||
@@ -1255,6 +1278,12 @@ def common():
|
|||||||
message += " After running that command, log out and re-login for it to take effect.\n"
|
message += " After running that command, log out and re-login for it to take effect.\n"
|
||||||
message += f"Error was:{ex}"
|
message += f"Error was:{ex}"
|
||||||
meshtastic.util.our_exit(message)
|
meshtastic.util.our_exit(message)
|
||||||
|
except OSError as ex:
|
||||||
|
message = f"OS Error:\n"
|
||||||
|
message += " The serial device couldn't be opened, it might be in use by another process.\n"
|
||||||
|
message += " Please close any applications or webpages that may be using the device and try again.\n"
|
||||||
|
message += f"\nOriginal error: {ex}"
|
||||||
|
meshtastic.util.our_exit(message)
|
||||||
if client.devPath is None:
|
if client.devPath is None:
|
||||||
try:
|
try:
|
||||||
client = meshtastic.tcp_interface.TCPInterface(
|
client = meshtastic.tcp_interface.TCPInterface(
|
||||||
@@ -1342,7 +1371,8 @@ def addSelectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser
|
|||||||
|
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--dest",
|
"--dest",
|
||||||
help="The destination node id for any sent commands, if not set '^all' or '^local' is assumed as appropriate",
|
help="The destination node id for any sent commands. If not set '^all' or '^local' is assumed."
|
||||||
|
"Use the node ID with a '!' or '0x' prefix or the node number.",
|
||||||
default=None,
|
default=None,
|
||||||
metavar="!xxxxxxxx",
|
metavar="!xxxxxxxx",
|
||||||
)
|
)
|
||||||
@@ -1489,7 +1519,20 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
|||||||
"--set-ham", help="Set licensed Ham ID and turn off encryption", action="store"
|
"--set-ham", help="Set licensed Ham ID and turn off encryption", action="store"
|
||||||
)
|
)
|
||||||
|
|
||||||
group.add_argument("--seturl", help="Set a channel URL", action="store")
|
group.add_argument(
|
||||||
|
"--ch-set-url", "--seturl",
|
||||||
|
help="Set all channels and set LoRa config from a supplied URL",
|
||||||
|
metavar="URL",
|
||||||
|
action="store"
|
||||||
|
)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--ch-add-url",
|
||||||
|
help="Add secondary channels and set LoRa config from a supplied URL",
|
||||||
|
metavar="URL",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@@ -1627,6 +1670,13 @@ def addLocalActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--show-fields",
|
||||||
|
help="Specify fields to show (comma-separated) when using --nodes",
|
||||||
|
type=lambda s: s.split(','),
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
||||||
@@ -1638,15 +1688,21 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
|
|||||||
|
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--sendtext",
|
"--sendtext",
|
||||||
help="Send a text message. Can specify a destination '--dest' and/or channel index '--ch-index'.",
|
help="Send a text message. Can specify a destination '--dest', use of PRIVATE_APP port '--private', and/or channel index '--ch-index'.",
|
||||||
metavar="TEXT",
|
metavar="TEXT",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--private",
|
||||||
|
help="Optional argument for sending text messages to the PRIVATE_APP port. Use in combination with --sendtext.",
|
||||||
|
action="store_true"
|
||||||
|
)
|
||||||
|
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--traceroute",
|
"--traceroute",
|
||||||
help="Traceroute from connected node to a destination. "
|
help="Traceroute from connected node to a destination. "
|
||||||
"You need pass the destination ID as argument, like "
|
"You need pass the destination ID as argument, like "
|
||||||
"this: '--traceroute !ba4bf9d0' "
|
"this: '--traceroute !ba4bf9d0' | '--traceroute 0xba4bf9d0'"
|
||||||
"Only nodes with a shared channel can be traced.",
|
"Only nodes with a shared channel can be traced.",
|
||||||
metavar="!xxxxxxxx",
|
metavar="!xxxxxxxx",
|
||||||
)
|
)
|
||||||
@@ -1727,27 +1783,32 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
|||||||
|
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--remove-node",
|
"--remove-node",
|
||||||
help="Tell the destination node to remove a specific node from its DB, by node number or ID",
|
help="Tell the destination node to remove a specific node from its NodeDB. "
|
||||||
|
"Use the node ID with a '!' or '0x' prefix or the node number.",
|
||||||
metavar="!xxxxxxxx"
|
metavar="!xxxxxxxx"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--set-favorite-node",
|
"--set-favorite-node",
|
||||||
help="Tell the destination node to set the specified node to be favorited on the NodeDB on the devicein its DB, by number or ID",
|
help="Tell the destination node to set the specified node to be favorited on the NodeDB. "
|
||||||
|
"Use the node ID with a '!' or '0x' prefix or the node number.",
|
||||||
metavar="!xxxxxxxx"
|
metavar="!xxxxxxxx"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--remove-favorite-node",
|
"--remove-favorite-node",
|
||||||
help="Tell the destination node to set the specified node to be un-favorited on the NodeDB on the device, by number or ID",
|
help="Tell the destination node to set the specified node to be un-favorited on the NodeDB. "
|
||||||
|
"Use the node ID with a '!' or '0x' prefix or the node number.",
|
||||||
metavar="!xxxxxxxx"
|
metavar="!xxxxxxxx"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--set-ignored-node",
|
"--set-ignored-node",
|
||||||
help="Tell the destination node to set the specified node to be ignored on the NodeDB on the devicein its DB, by number or ID",
|
help="Tell the destination node to set the specified node to be ignored on the NodeDB. "
|
||||||
|
"Use the node ID with a '!' or '0x' prefix or the node number.",
|
||||||
metavar="!xxxxxxxx"
|
metavar="!xxxxxxxx"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--remove-ignored-node",
|
"--remove-ignored-node",
|
||||||
help="Tell the destination node to set the specified node to be un-ignored on the NodeDB on the device, by number or ID",
|
help="Tell the destination node to set the specified node to be un-ignored on the NodeDB. "
|
||||||
|
"Use the node ID with a '!' or '0x' prefix or the node number.",
|
||||||
metavar="!xxxxxxxx"
|
metavar="!xxxxxxxx"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
|
|||||||
@@ -222,9 +222,42 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
return infos
|
return infos
|
||||||
|
|
||||||
def showNodes(
|
def showNodes(
|
||||||
self, includeSelf: bool = True
|
self, includeSelf: bool = True, showFields: Optional[List[str]] = None
|
||||||
) -> str: # pylint: disable=W0613
|
) -> str: # pylint: disable=W0613
|
||||||
"""Show table summary of nodes in mesh"""
|
"""Show table summary of nodes in mesh
|
||||||
|
|
||||||
|
Args:
|
||||||
|
includeSelf (bool): Include ourself in the output?
|
||||||
|
showFields (List[str]): List of fields to show in output
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_human_readable(name):
|
||||||
|
name_map = {
|
||||||
|
"user.longName": "User",
|
||||||
|
"user.id": "ID",
|
||||||
|
"user.shortName": "AKA",
|
||||||
|
"user.hwModel": "Hardware",
|
||||||
|
"user.publicKey": "Pubkey",
|
||||||
|
"user.role": "Role",
|
||||||
|
"position.latitude": "Latitude",
|
||||||
|
"position.longitude": "Longitude",
|
||||||
|
"position.altitude": "Altitude",
|
||||||
|
"deviceMetrics.batteryLevel": "Battery",
|
||||||
|
"deviceMetrics.channelUtilization": "Channel util.",
|
||||||
|
"deviceMetrics.airUtilTx": "Tx air util.",
|
||||||
|
"snr": "SNR",
|
||||||
|
"hopsAway": "Hops",
|
||||||
|
"channel": "Channel",
|
||||||
|
"lastHeard": "LastHeard",
|
||||||
|
"since": "Since",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if name in name_map:
|
||||||
|
return name_map.get(name) # Default to a formatted guess
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def formatFloat(value, precision=2, unit="") -> Optional[str]:
|
def formatFloat(value, precision=2, unit="") -> Optional[str]:
|
||||||
"""Format a float value with precision."""
|
"""Format a float value with precision."""
|
||||||
@@ -246,6 +279,29 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
return None # not handling a timestamp from the future
|
return None # not handling a timestamp from the future
|
||||||
return _timeago(delta_secs)
|
return _timeago(delta_secs)
|
||||||
|
|
||||||
|
def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
|
||||||
|
if key_path.index(".") < 0:
|
||||||
|
logging.debug("getNestedValue was called without a nested path.")
|
||||||
|
return None
|
||||||
|
keys = key_path.split(".")
|
||||||
|
value: Optional[Union[str, dict]] = node_dict
|
||||||
|
for key in keys:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.get(key)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
if showFields is None or len(showFields) == 0:
|
||||||
|
# The default set of fields to show (e.g., the status quo)
|
||||||
|
showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey",
|
||||||
|
"user.role", "position.latitude", "position.longitude", "position.altitude",
|
||||||
|
"deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization",
|
||||||
|
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "lastHeard", "since"]
|
||||||
|
else:
|
||||||
|
# Always at least include the row number.
|
||||||
|
showFields.insert(0, "N")
|
||||||
|
|
||||||
rows: List[Dict[str, Any]] = []
|
rows: List[Dict[str, Any]] = []
|
||||||
if self.nodesByNum:
|
if self.nodesByNum:
|
||||||
logging.debug(f"self.nodes:{self.nodes}")
|
logging.debug(f"self.nodes:{self.nodes}")
|
||||||
@@ -254,65 +310,60 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
presumptive_id = f"!{node['num']:08x}"
|
presumptive_id = f"!{node['num']:08x}"
|
||||||
row = {
|
|
||||||
"N": 0,
|
|
||||||
"User": f"Meshtastic {presumptive_id[-4:]}",
|
|
||||||
"ID": presumptive_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
user = node.get("user")
|
# This allows the user to specify fields that wouldn't otherwise be included.
|
||||||
if user:
|
fields = {}
|
||||||
row.update(
|
for field in showFields:
|
||||||
{
|
if "." in field:
|
||||||
"User": user.get("longName", "N/A"),
|
raw_value = getNestedValue(node, field)
|
||||||
"AKA": user.get("shortName", "N/A"),
|
else:
|
||||||
"ID": user["id"],
|
# The "since" column is synthesized, it's not retrieved from the device. Get the
|
||||||
"Hardware": user.get("hwModel", "UNSET"),
|
# lastHeard value here, and then we'll format it properly below.
|
||||||
"Pubkey": user.get("publicKey", "UNSET"),
|
if field == "since":
|
||||||
}
|
raw_value = node.get("lastHeard")
|
||||||
)
|
|
||||||
|
|
||||||
pos = node.get("position")
|
|
||||||
if pos:
|
|
||||||
row.update(
|
|
||||||
{
|
|
||||||
"Latitude": formatFloat(pos.get("latitude"), 4, "°"),
|
|
||||||
"Longitude": formatFloat(pos.get("longitude"), 4, "°"),
|
|
||||||
"Altitude": formatFloat(pos.get("altitude"), 0, " m"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
metrics = node.get("deviceMetrics")
|
|
||||||
if metrics:
|
|
||||||
batteryLevel = metrics.get("batteryLevel")
|
|
||||||
if batteryLevel is not None:
|
|
||||||
if batteryLevel == 0:
|
|
||||||
batteryString = "Powered"
|
|
||||||
else:
|
else:
|
||||||
batteryString = str(batteryLevel) + "%"
|
raw_value = node.get(field)
|
||||||
row.update({"Battery": batteryString})
|
|
||||||
row.update(
|
|
||||||
{
|
|
||||||
"Channel util.": formatFloat(
|
|
||||||
metrics.get("channelUtilization"), 2, "%"
|
|
||||||
),
|
|
||||||
"Tx air util.": formatFloat(
|
|
||||||
metrics.get("airUtilTx"), 2, "%"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
row.update(
|
formatted_value: Optional[str] = ""
|
||||||
{
|
|
||||||
"SNR": formatFloat(node.get("snr"), 2, " dB"),
|
|
||||||
"Hops": node.get("hopsAway", "?"),
|
|
||||||
"Channel": node.get("channel", 0),
|
|
||||||
"LastHeard": getLH(node.get("lastHeard")),
|
|
||||||
"Since": getTimeAgo(node.get("lastHeard")),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
rows.append(row)
|
# Some of these need special formatting or processing.
|
||||||
|
if field == "channel":
|
||||||
|
if raw_value is None:
|
||||||
|
formatted_value = "0"
|
||||||
|
elif field == "deviceMetrics.channelUtilization":
|
||||||
|
formatted_value = formatFloat(raw_value, 2, "%")
|
||||||
|
elif field == "deviceMetrics.airUtilTx":
|
||||||
|
formatted_value = formatFloat(raw_value, 2, "%")
|
||||||
|
elif field == "deviceMetrics.batteryLevel":
|
||||||
|
if raw_value in (0, 101):
|
||||||
|
formatted_value = "Powered"
|
||||||
|
else:
|
||||||
|
formatted_value = formatFloat(raw_value, 0, "%")
|
||||||
|
elif field == "lastHeard":
|
||||||
|
formatted_value = getLH(raw_value)
|
||||||
|
elif field == "position.latitude":
|
||||||
|
formatted_value = formatFloat(raw_value, 4, "°")
|
||||||
|
elif field == "position.longitude":
|
||||||
|
formatted_value = formatFloat(raw_value, 4, "°")
|
||||||
|
elif field == "position.altitude":
|
||||||
|
formatted_value = formatFloat(raw_value, 0, "m")
|
||||||
|
elif field == "since":
|
||||||
|
formatted_value = getTimeAgo(raw_value) or "N/A"
|
||||||
|
elif field == "snr":
|
||||||
|
formatted_value = formatFloat(raw_value, 0, " dB")
|
||||||
|
elif field == "user.shortName":
|
||||||
|
formatted_value = raw_value if raw_value is not None else f'Meshtastic {presumptive_id[-4:]}'
|
||||||
|
elif field == "user.id":
|
||||||
|
formatted_value = raw_value if raw_value is not None else presumptive_id
|
||||||
|
else:
|
||||||
|
formatted_value = raw_value # No special formatting
|
||||||
|
|
||||||
|
fields[field] = formatted_value
|
||||||
|
|
||||||
|
# Filter out any field in the data set that was not specified.
|
||||||
|
filteredData = {get_human_readable(k): v for k, v in fields.items() if k in showFields}
|
||||||
|
filteredData.update({get_human_readable(k): v for k, v in fields.items()})
|
||||||
|
rows.append(filteredData)
|
||||||
|
|
||||||
rows.sort(key=lambda r: r.get("LastHeard") or "0000", reverse=True)
|
rows.sort(key=lambda r: r.get("LastHeard") or "0000", reverse=True)
|
||||||
for i, row in enumerate(rows):
|
for i, row in enumerate(rows):
|
||||||
@@ -344,7 +395,7 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
if new_index != last_index:
|
if new_index != last_index:
|
||||||
retries_left = requestChannelAttempts - 1
|
retries_left = requestChannelAttempts - 1
|
||||||
if retries_left <= 0:
|
if retries_left <= 0:
|
||||||
our_exit(f"Error: Timed out waiting for channels, giving up")
|
our_exit("Error: Timed out waiting for channels, giving up")
|
||||||
print("Timed out trying to retrieve channel info, retrying")
|
print("Timed out trying to retrieve channel info, retrying")
|
||||||
n.requestChannels(startingIndex=new_index)
|
n.requestChannels(startingIndex=new_index)
|
||||||
last_index = new_index
|
last_index = new_index
|
||||||
@@ -360,6 +411,7 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
wantResponse: bool = False,
|
wantResponse: bool = False,
|
||||||
onResponse: Optional[Callable[[dict], Any]] = None,
|
onResponse: Optional[Callable[[dict], Any]] = None,
|
||||||
channelIndex: int = 0,
|
channelIndex: int = 0,
|
||||||
|
portNum: portnums_pb2.PortNum.ValueType = portnums_pb2.PortNum.TEXT_MESSAGE_APP
|
||||||
):
|
):
|
||||||
"""Send a utf8 string to some other node, if the node has a display it
|
"""Send a utf8 string to some other node, if the node has a display it
|
||||||
will also be shown on the device.
|
will also be shown on the device.
|
||||||
@@ -370,12 +422,12 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
Keyword Arguments:
|
Keyword Arguments:
|
||||||
destinationId {nodeId or nodeNum} -- where to send this
|
destinationId {nodeId or nodeNum} -- where to send this
|
||||||
message (default: {BROADCAST_ADDR})
|
message (default: {BROADCAST_ADDR})
|
||||||
portNum -- the application portnum (similar to IP port numbers)
|
|
||||||
of the destination, see portnums.proto for a list
|
|
||||||
wantAck -- True if you want the message sent in a reliable manner
|
wantAck -- True if you want the message sent in a reliable manner
|
||||||
(with retries and ack/nak provided for delivery)
|
(with retries and ack/nak provided for delivery)
|
||||||
wantResponse -- True if you want the service on the other side to
|
wantResponse -- True if you want the service on the other side to
|
||||||
send an application layer response
|
send an application layer response
|
||||||
|
portNum -- the application portnum (similar to IP port numbers)
|
||||||
|
of the destination, see portnums.proto for a list
|
||||||
|
|
||||||
Returns the sent packet. The id field will be populated in this packet
|
Returns the sent packet. The id field will be populated in this packet
|
||||||
and can be used to track future message acks/naks.
|
and can be used to track future message acks/naks.
|
||||||
@@ -384,7 +436,7 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
return self.sendData(
|
return self.sendData(
|
||||||
text.encode("utf-8"),
|
text.encode("utf-8"),
|
||||||
destinationId,
|
destinationId,
|
||||||
portNum=portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
portNum=portNum,
|
||||||
wantAck=wantAck,
|
wantAck=wantAck,
|
||||||
wantResponse=wantResponse,
|
wantResponse=wantResponse,
|
||||||
onResponse=onResponse,
|
onResponse=onResponse,
|
||||||
@@ -891,8 +943,10 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
else:
|
else:
|
||||||
our_exit("Warning: No myInfo found.")
|
our_exit("Warning: No myInfo found.")
|
||||||
# A simple hex style nodeid - we can parse this without needing the DB
|
# A simple hex style nodeid - we can parse this without needing the DB
|
||||||
elif destinationId.startswith("!"):
|
elif isinstance(destinationId, str) and len(destinationId) >= 8:
|
||||||
nodeNum = int(destinationId[1:], 16)
|
# assuming some form of node id string such as !1234578 or 0x12345678
|
||||||
|
# always grab the last 8 items of the hexadecimal id str and parse to integer
|
||||||
|
nodeNum = int(destinationId[-8:], 16)
|
||||||
else:
|
else:
|
||||||
if self.nodes:
|
if self.nodes:
|
||||||
node = self.nodes.get(destinationId)
|
node = self.nodes.get(destinationId)
|
||||||
@@ -926,7 +980,7 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
toRadio.packet.CopyFrom(meshPacket)
|
toRadio.packet.CopyFrom(meshPacket)
|
||||||
if self.noProto:
|
if self.noProto:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Not sending packet because protocol use is disabled by noProto"
|
"Not sending packet because protocol use is disabled by noProto"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.debug(f"Sending packet: {stripnl(meshPacket)}")
|
logging.debug(f"Sending packet: {stripnl(meshPacket)}")
|
||||||
@@ -1115,7 +1169,7 @@ class MeshInterface: # pylint: disable=R0902
|
|||||||
"""Send a ToRadio protobuf to the device"""
|
"""Send a ToRadio protobuf to the device"""
|
||||||
if self.noProto:
|
if self.noProto:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Not sending packet because protocol use is disabled by noProto"
|
"Not sending packet because protocol use is disabled by noProto"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# logging.debug(f"Sending toRadio: {stripnl(toRadio)}")
|
# logging.debug(f"Sending toRadio: {stripnl(toRadio)}")
|
||||||
|
|||||||
@@ -337,14 +337,19 @@ class Node:
|
|||||||
s = s.replace("=", "").replace("+", "-").replace("/", "_")
|
s = s.replace("=", "").replace("+", "-").replace("/", "_")
|
||||||
return f"https://meshtastic.org/e/#{s}"
|
return f"https://meshtastic.org/e/#{s}"
|
||||||
|
|
||||||
def setURL(self, url):
|
def setURL(self, url: str, addOnly: bool = False):
|
||||||
"""Set mesh network URL"""
|
"""Set mesh network URL"""
|
||||||
if self.localConfig is None:
|
if self.localConfig is None or self.channels is None:
|
||||||
our_exit("Warning: No Config has been read")
|
our_exit("Warning: config or channels not loaded")
|
||||||
|
|
||||||
# URLs are of the form https://meshtastic.org/d/#{base64_channel_set}
|
# URLs are of the form https://meshtastic.org/d/#{base64_channel_set}
|
||||||
# Split on '/#' to find the base64 encoded channel settings
|
# Split on '/#' to find the base64 encoded channel settings
|
||||||
splitURL = url.split("/#")
|
if addOnly:
|
||||||
|
splitURL = url.split("/?add=true#")
|
||||||
|
else:
|
||||||
|
splitURL = url.split("/#")
|
||||||
|
if len(splitURL) == 1:
|
||||||
|
our_exit(f"Warning: Invalid URL '{url}'")
|
||||||
b64 = splitURL[-1]
|
b64 = splitURL[-1]
|
||||||
|
|
||||||
# We normally strip padding to make for a shorter URL, but the python parser doesn't like
|
# We normally strip padding to make for a shorter URL, but the python parser doesn't like
|
||||||
@@ -361,20 +366,36 @@ class Node:
|
|||||||
if len(channelSet.settings) == 0:
|
if len(channelSet.settings) == 0:
|
||||||
our_exit("Warning: There were no settings.")
|
our_exit("Warning: There were no settings.")
|
||||||
|
|
||||||
i = 0
|
if addOnly:
|
||||||
for chs in channelSet.settings:
|
# Add new channels with names not already present
|
||||||
ch = channel_pb2.Channel()
|
# Don't change existing channels
|
||||||
ch.role = (
|
for chs in channelSet.settings:
|
||||||
channel_pb2.Channel.Role.PRIMARY
|
channelExists = self.getChannelByName(chs.name)
|
||||||
if i == 0
|
if channelExists or chs.name == "":
|
||||||
else channel_pb2.Channel.Role.SECONDARY
|
print(f"Ignoring existing or empty channel \"{chs.name}\" from add URL")
|
||||||
)
|
continue
|
||||||
ch.index = i
|
ch = self.getDisabledChannel()
|
||||||
ch.settings.CopyFrom(chs)
|
if not ch:
|
||||||
self.channels[ch.index] = ch
|
our_exit("Warning: No free channels were found")
|
||||||
logging.debug(f"Channel i:{i} ch:{ch}")
|
ch.settings.CopyFrom(chs)
|
||||||
self.writeChannel(ch.index)
|
ch.role = channel_pb2.Channel.Role.SECONDARY
|
||||||
i = i + 1
|
print(f"Adding new channel '{chs.name}' to device")
|
||||||
|
self.writeChannel(ch.index)
|
||||||
|
else:
|
||||||
|
i = 0
|
||||||
|
for chs in channelSet.settings:
|
||||||
|
ch = channel_pb2.Channel()
|
||||||
|
ch.role = (
|
||||||
|
channel_pb2.Channel.Role.PRIMARY
|
||||||
|
if i == 0
|
||||||
|
else channel_pb2.Channel.Role.SECONDARY
|
||||||
|
)
|
||||||
|
ch.index = i
|
||||||
|
ch.settings.CopyFrom(chs)
|
||||||
|
self.channels[ch.index] = ch
|
||||||
|
logging.debug(f"Channel i:{i} ch:{ch}")
|
||||||
|
self.writeChannel(ch.index)
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
p = admin_pb2.AdminMessage()
|
p = admin_pb2.AdminMessage()
|
||||||
p.set_config.lora.CopyFrom(channelSet.lora_config)
|
p.set_config.lora.CopyFrom(channelSet.lora_config)
|
||||||
|
|||||||
54
meshtastic/protobuf/config_pb2.py
generated
54
meshtastic/protobuf/config_pb2.py
generated
File diff suppressed because one or more lines are too long
9
meshtastic/protobuf/config_pb2.pyi
generated
9
meshtastic/protobuf/config_pb2.pyi
generated
@@ -1118,6 +1118,7 @@ class Config(google.protobuf.message.Message):
|
|||||||
HEADING_BOLD_FIELD_NUMBER: builtins.int
|
HEADING_BOLD_FIELD_NUMBER: builtins.int
|
||||||
WAKE_ON_TAP_OR_MOTION_FIELD_NUMBER: builtins.int
|
WAKE_ON_TAP_OR_MOTION_FIELD_NUMBER: builtins.int
|
||||||
COMPASS_ORIENTATION_FIELD_NUMBER: builtins.int
|
COMPASS_ORIENTATION_FIELD_NUMBER: builtins.int
|
||||||
|
USE_12H_CLOCK_FIELD_NUMBER: builtins.int
|
||||||
screen_on_secs: builtins.int
|
screen_on_secs: builtins.int
|
||||||
"""
|
"""
|
||||||
Number of seconds the screen stays on after pressing the user button or receiving a message
|
Number of seconds the screen stays on after pressing the user button or receiving a message
|
||||||
@@ -1165,6 +1166,11 @@ class Config(google.protobuf.message.Message):
|
|||||||
"""
|
"""
|
||||||
Indicates how to rotate or invert the compass output to accurate display on the display.
|
Indicates how to rotate or invert the compass output to accurate display on the display.
|
||||||
"""
|
"""
|
||||||
|
use_12h_clock: builtins.bool
|
||||||
|
"""
|
||||||
|
If false (default), the device will display the time in 24-hour format on screen.
|
||||||
|
If true, the device will display the time in 12-hour format on screen.
|
||||||
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1179,8 +1185,9 @@ class Config(google.protobuf.message.Message):
|
|||||||
heading_bold: builtins.bool = ...,
|
heading_bold: builtins.bool = ...,
|
||||||
wake_on_tap_or_motion: builtins.bool = ...,
|
wake_on_tap_or_motion: builtins.bool = ...,
|
||||||
compass_orientation: global___Config.DisplayConfig.CompassOrientation.ValueType = ...,
|
compass_orientation: global___Config.DisplayConfig.CompassOrientation.ValueType = ...,
|
||||||
|
use_12h_clock: builtins.bool = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "use_12h_clock", b"use_12h_clock", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
|
||||||
|
|
||||||
@typing.final
|
@typing.final
|
||||||
class LoRaConfig(google.protobuf.message.Message):
|
class LoRaConfig(google.protobuf.message.Message):
|
||||||
|
|||||||
16
meshtastic/protobuf/mesh_pb2.py
generated
16
meshtastic/protobuf/mesh_pb2.py
generated
File diff suppressed because one or more lines are too long
10
meshtastic/protobuf/mesh_pb2.pyi
generated
10
meshtastic/protobuf/mesh_pb2.pyi
generated
@@ -397,6 +397,11 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
|||||||
Mesh-Tab, esp32 based
|
Mesh-Tab, esp32 based
|
||||||
https://github.com/valzzu/Mesh-Tab
|
https://github.com/valzzu/Mesh-Tab
|
||||||
"""
|
"""
|
||||||
|
MESHLINK: _HardwareModel.ValueType # 87
|
||||||
|
"""
|
||||||
|
MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog
|
||||||
|
https://www.loraitalia.it
|
||||||
|
"""
|
||||||
PRIVATE_HW: _HardwareModel.ValueType # 255
|
PRIVATE_HW: _HardwareModel.ValueType # 255
|
||||||
"""
|
"""
|
||||||
------------------------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -777,6 +782,11 @@ MESH_TAB: HardwareModel.ValueType # 86
|
|||||||
Mesh-Tab, esp32 based
|
Mesh-Tab, esp32 based
|
||||||
https://github.com/valzzu/Mesh-Tab
|
https://github.com/valzzu/Mesh-Tab
|
||||||
"""
|
"""
|
||||||
|
MESHLINK: HardwareModel.ValueType # 87
|
||||||
|
"""
|
||||||
|
MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog
|
||||||
|
https://www.loraitalia.it
|
||||||
|
"""
|
||||||
PRIVATE_HW: HardwareModel.ValueType # 255
|
PRIVATE_HW: HardwareModel.ValueType # 255
|
||||||
"""
|
"""
|
||||||
------------------------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
2
meshtastic/protobuf/module_config_pb2.pyi
generated
2
meshtastic/protobuf/module_config_pb2.pyi
generated
@@ -927,7 +927,7 @@ class ModuleConfig(google.protobuf.message.Message):
|
|||||||
@typing.final
|
@typing.final
|
||||||
class CannedMessageConfig(google.protobuf.message.Message):
|
class CannedMessageConfig(google.protobuf.message.Message):
|
||||||
"""
|
"""
|
||||||
TODO: REPLACE
|
Canned Messages Module Config
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||||
|
|||||||
32
meshtastic/protobuf/telemetry_pb2.py
generated
32
meshtastic/protobuf/telemetry_pb2.py
generated
File diff suppressed because one or more lines are too long
28
meshtastic/protobuf/telemetry_pb2.pyi
generated
28
meshtastic/protobuf/telemetry_pb2.pyi
generated
@@ -163,6 +163,10 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
|||||||
"""
|
"""
|
||||||
High accuracy current and voltage
|
High accuracy current and voltage
|
||||||
"""
|
"""
|
||||||
|
DFROBOT_RAIN: _TelemetrySensorType.ValueType # 35
|
||||||
|
"""
|
||||||
|
DFRobot Gravity tipping bucket rain gauge
|
||||||
|
"""
|
||||||
|
|
||||||
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
|
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
|
||||||
"""
|
"""
|
||||||
@@ -309,6 +313,10 @@ INA226: TelemetrySensorType.ValueType # 34
|
|||||||
"""
|
"""
|
||||||
High accuracy current and voltage
|
High accuracy current and voltage
|
||||||
"""
|
"""
|
||||||
|
DFROBOT_RAIN: TelemetrySensorType.ValueType # 35
|
||||||
|
"""
|
||||||
|
DFRobot Gravity tipping bucket rain gauge
|
||||||
|
"""
|
||||||
global___TelemetrySensorType = TelemetrySensorType
|
global___TelemetrySensorType = TelemetrySensorType
|
||||||
|
|
||||||
@typing.final
|
@typing.final
|
||||||
@@ -394,6 +402,8 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
|||||||
WIND_GUST_FIELD_NUMBER: builtins.int
|
WIND_GUST_FIELD_NUMBER: builtins.int
|
||||||
WIND_LULL_FIELD_NUMBER: builtins.int
|
WIND_LULL_FIELD_NUMBER: builtins.int
|
||||||
RADIATION_FIELD_NUMBER: builtins.int
|
RADIATION_FIELD_NUMBER: builtins.int
|
||||||
|
RAINFALL_1H_FIELD_NUMBER: builtins.int
|
||||||
|
RAINFALL_24H_FIELD_NUMBER: builtins.int
|
||||||
temperature: builtins.float
|
temperature: builtins.float
|
||||||
"""
|
"""
|
||||||
Temperature measured
|
Temperature measured
|
||||||
@@ -468,6 +478,14 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
|||||||
"""
|
"""
|
||||||
Radiation in µR/h
|
Radiation in µR/h
|
||||||
"""
|
"""
|
||||||
|
rainfall_1h: builtins.float
|
||||||
|
"""
|
||||||
|
Rainfall in the last hour in mm
|
||||||
|
"""
|
||||||
|
rainfall_24h: builtins.float
|
||||||
|
"""
|
||||||
|
Rainfall in the last 24 hours in mm
|
||||||
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -489,9 +507,11 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
|||||||
wind_gust: builtins.float | None = ...,
|
wind_gust: builtins.float | None = ...,
|
||||||
wind_lull: builtins.float | None = ...,
|
wind_lull: builtins.float | None = ...,
|
||||||
radiation: builtins.float | None = ...,
|
radiation: builtins.float | None = ...,
|
||||||
|
rainfall_1h: builtins.float | None = ...,
|
||||||
|
rainfall_24h: builtins.float | None = ...,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
def HasField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_relative_humidity", b"_relative_humidity", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "radiation", b"radiation", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> builtins.bool: ...
|
def HasField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_rainfall_1h", b"_rainfall_1h", "_rainfall_24h", b"_rainfall_24h", "_relative_humidity", b"_relative_humidity", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "radiation", b"radiation", "rainfall_1h", b"rainfall_1h", "rainfall_24h", b"rainfall_24h", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> builtins.bool: ...
|
||||||
def ClearField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_relative_humidity", b"_relative_humidity", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "radiation", b"radiation", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> None: ...
|
def ClearField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_rainfall_1h", b"_rainfall_1h", "_rainfall_24h", b"_rainfall_24h", "_relative_humidity", b"_relative_humidity", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "radiation", b"radiation", "rainfall_1h", b"rainfall_1h", "rainfall_24h", b"rainfall_24h", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> None: ...
|
||||||
@typing.overload
|
@typing.overload
|
||||||
def WhichOneof(self, oneof_group: typing.Literal["_barometric_pressure", b"_barometric_pressure"]) -> typing.Literal["barometric_pressure"] | None: ...
|
def WhichOneof(self, oneof_group: typing.Literal["_barometric_pressure", b"_barometric_pressure"]) -> typing.Literal["barometric_pressure"] | None: ...
|
||||||
@typing.overload
|
@typing.overload
|
||||||
@@ -509,6 +529,10 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
|||||||
@typing.overload
|
@typing.overload
|
||||||
def WhichOneof(self, oneof_group: typing.Literal["_radiation", b"_radiation"]) -> typing.Literal["radiation"] | None: ...
|
def WhichOneof(self, oneof_group: typing.Literal["_radiation", b"_radiation"]) -> typing.Literal["radiation"] | None: ...
|
||||||
@typing.overload
|
@typing.overload
|
||||||
|
def WhichOneof(self, oneof_group: typing.Literal["_rainfall_1h", b"_rainfall_1h"]) -> typing.Literal["rainfall_1h"] | None: ...
|
||||||
|
@typing.overload
|
||||||
|
def WhichOneof(self, oneof_group: typing.Literal["_rainfall_24h", b"_rainfall_24h"]) -> typing.Literal["rainfall_24h"] | None: ...
|
||||||
|
@typing.overload
|
||||||
def WhichOneof(self, oneof_group: typing.Literal["_relative_humidity", b"_relative_humidity"]) -> typing.Literal["relative_humidity"] | None: ...
|
def WhichOneof(self, oneof_group: typing.Literal["_relative_humidity", b"_relative_humidity"]) -> typing.Literal["relative_humidity"] | None: ...
|
||||||
@typing.overload
|
@typing.overload
|
||||||
def WhichOneof(self, oneof_group: typing.Literal["_temperature", b"_temperature"]) -> typing.Literal["temperature"] | None: ...
|
def WhichOneof(self, oneof_group: typing.Literal["_temperature", b"_temperature"]) -> typing.Literal["temperature"] | None: ...
|
||||||
|
|||||||
@@ -408,8 +408,8 @@ def test_main_nodes(capsys):
|
|||||||
|
|
||||||
iface = MagicMock(autospec=SerialInterface)
|
iface = MagicMock(autospec=SerialInterface)
|
||||||
|
|
||||||
def mock_showNodes():
|
def mock_showNodes(includeSelf, showFields):
|
||||||
print("inside mocked showNodes")
|
print(f"inside mocked showNodes: {includeSelf} {showFields}")
|
||||||
|
|
||||||
iface.showNodes.side_effect = mock_showNodes
|
iface.showNodes.side_effect = mock_showNodes
|
||||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||||
@@ -593,10 +593,10 @@ def test_main_sendtext(capsys):
|
|||||||
iface = MagicMock(autospec=SerialInterface)
|
iface = MagicMock(autospec=SerialInterface)
|
||||||
|
|
||||||
def mock_sendText(
|
def mock_sendText(
|
||||||
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0
|
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0
|
||||||
):
|
):
|
||||||
print("inside mocked sendText")
|
print("inside mocked sendText")
|
||||||
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex}")
|
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}")
|
||||||
|
|
||||||
iface.sendText.side_effect = mock_sendText
|
iface.sendText.side_effect = mock_sendText
|
||||||
|
|
||||||
@@ -620,10 +620,10 @@ def test_main_sendtext_with_channel(capsys):
|
|||||||
iface = MagicMock(autospec=SerialInterface)
|
iface = MagicMock(autospec=SerialInterface)
|
||||||
|
|
||||||
def mock_sendText(
|
def mock_sendText(
|
||||||
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0
|
text, dest, wantAck=False, wantResponse=False, onResponse=None, channelIndex=0, portNum=0
|
||||||
):
|
):
|
||||||
print("inside mocked sendText")
|
print("inside mocked sendText")
|
||||||
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex}")
|
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}")
|
||||||
|
|
||||||
iface.sendText.side_effect = mock_sendText
|
iface.sendText.side_effect = mock_sendText
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ def test_setURL_empty_url(capsys):
|
|||||||
assert pytest_wrapped_e.type == SystemExit
|
assert pytest_wrapped_e.type == SystemExit
|
||||||
assert pytest_wrapped_e.value.code == 1
|
assert pytest_wrapped_e.value.code == 1
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert re.search(r"Warning: There were no settings.", out, re.MULTILINE)
|
assert re.search(r"Warning: config or channels not loaded", out, re.MULTILINE)
|
||||||
assert err == ""
|
assert err == ""
|
||||||
|
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ def test_setURL_valid_URL_but_no_settings(capsys):
|
|||||||
assert pytest_wrapped_e.type == SystemExit
|
assert pytest_wrapped_e.type == SystemExit
|
||||||
assert pytest_wrapped_e.value.code == 1
|
assert pytest_wrapped_e.value.code == 1
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert re.search(r"Warning: There were no settings", out, re.MULTILINE)
|
assert re.search(r"Warning: config or channels not loaded", out, re.MULTILINE)
|
||||||
assert err == ""
|
assert err == ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Submodule protobufs updated: 76f806e1bb...068646653e
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "meshtastic"
|
name = "meshtastic"
|
||||||
version = "2.5.10"
|
version = "2.5.12"
|
||||||
description = "Python API & client shell for talking to Meshtastic devices"
|
description = "Python API & client shell for talking to Meshtastic devices"
|
||||||
authors = ["Meshtastic Developers <contact@meshtastic.org>"]
|
authors = ["Meshtastic Developers <contact@meshtastic.org>"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|||||||
Reference in New Issue
Block a user