mirror of
https://github.com/meshtastic/python.git
synced 2025-12-26 01:17:51 -05:00
Compare commits
47 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 | ||
|
|
0fb72b8ad1 | ||
|
|
74c911cb75 | ||
|
|
579383cd5a |
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -245,6 +245,23 @@
|
||||
"module": "meshtastic",
|
||||
"justMyCode": true,
|
||||
"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
|
||||
argcomplete: Union[None, ModuleType] = None
|
||||
try:
|
||||
import argcomplete
|
||||
import argcomplete # type: ignore
|
||||
except ImportError as e:
|
||||
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}")
|
||||
|
||||
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
|
||||
|
||||
enumType = pref.enum_type
|
||||
@@ -483,6 +483,7 @@ def onConnected(interface):
|
||||
if checkChannel(interface, channelIndex):
|
||||
print(
|
||||
f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}"
|
||||
f" {'using PRIVATE_APP port' if args.private else ''}"
|
||||
)
|
||||
interface.sendText(
|
||||
args.sendtext,
|
||||
@@ -490,6 +491,7 @@ def onConnected(interface):
|
||||
wantAck=True,
|
||||
channelIndex=channelIndex,
|
||||
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:
|
||||
meshtastic.util.our_exit(
|
||||
@@ -714,12 +716,16 @@ def onConnected(interface):
|
||||
closeNow = True
|
||||
export_config(interface)
|
||||
|
||||
if args.seturl:
|
||||
if args.ch_set_url:
|
||||
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
|
||||
|
||||
if args.ch_add_url:
|
||||
closeNow = True
|
||||
interface.getNode(args.dest, **getNode_kwargs).setURL(args.ch_add_url, addOnly=True)
|
||||
|
||||
if args.ch_add:
|
||||
channelIndex = mt_config.channel_index
|
||||
if channelIndex is not None:
|
||||
@@ -921,7 +927,11 @@ def onConnected(interface):
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
print("Showing node list of a remote node is not supported.")
|
||||
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:
|
||||
closeNow = True
|
||||
@@ -1245,6 +1255,19 @@ def common():
|
||||
noProto=args.noproto,
|
||||
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:
|
||||
username = os.getlogin()
|
||||
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 += f"Error was:{ex}"
|
||||
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:
|
||||
try:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
@@ -1342,7 +1371,8 @@ def addSelectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser
|
||||
|
||||
group.add_argument(
|
||||
"--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,
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -1627,6 +1670,13 @@ def addLocalActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
||||
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
|
||||
|
||||
def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
||||
@@ -1638,15 +1688,21 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
|
||||
|
||||
group.add_argument(
|
||||
"--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",
|
||||
)
|
||||
|
||||
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(
|
||||
"--traceroute",
|
||||
help="Traceroute from connected node to a destination. "
|
||||
"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.",
|
||||
metavar="!xxxxxxxx",
|
||||
)
|
||||
@@ -1727,27 +1783,32 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
||||
|
||||
group.add_argument(
|
||||
"--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"
|
||||
)
|
||||
group.add_argument(
|
||||
"--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"
|
||||
)
|
||||
group.add_argument(
|
||||
"--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"
|
||||
)
|
||||
group.add_argument(
|
||||
"--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"
|
||||
)
|
||||
group.add_argument(
|
||||
"--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"
|
||||
)
|
||||
group.add_argument(
|
||||
|
||||
@@ -222,9 +222,42 @@ class MeshInterface: # pylint: disable=R0902
|
||||
return infos
|
||||
|
||||
def showNodes(
|
||||
self, includeSelf: bool = True
|
||||
self, includeSelf: bool = True, showFields: Optional[List[str]] = None
|
||||
) -> 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]:
|
||||
"""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 _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]] = []
|
||||
if self.nodesByNum:
|
||||
logging.debug(f"self.nodes:{self.nodes}")
|
||||
@@ -254,66 +310,60 @@ class MeshInterface: # pylint: disable=R0902
|
||||
continue
|
||||
|
||||
presumptive_id = f"!{node['num']:08x}"
|
||||
row = {
|
||||
"N": 0,
|
||||
"User": f"Meshtastic {presumptive_id[-4:]}",
|
||||
"ID": presumptive_id,
|
||||
}
|
||||
|
||||
user = node.get("user")
|
||||
if user:
|
||||
row.update(
|
||||
{
|
||||
"User": user.get("longName", "N/A"),
|
||||
"AKA": user.get("shortName", "N/A"),
|
||||
"ID": user["id"],
|
||||
"Hardware": user.get("hwModel", "UNSET"),
|
||||
"Pubkey": user.get("publicKey", "UNSET"),
|
||||
"Role": user.get("role", "N/A"),
|
||||
}
|
||||
)
|
||||
|
||||
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"
|
||||
# This allows the user to specify fields that wouldn't otherwise be included.
|
||||
fields = {}
|
||||
for field in showFields:
|
||||
if "." in field:
|
||||
raw_value = getNestedValue(node, field)
|
||||
else:
|
||||
# The "since" column is synthesized, it's not retrieved from the device. Get the
|
||||
# lastHeard value here, and then we'll format it properly below.
|
||||
if field == "since":
|
||||
raw_value = node.get("lastHeard")
|
||||
else:
|
||||
batteryString = str(batteryLevel) + "%"
|
||||
row.update({"Battery": batteryString})
|
||||
row.update(
|
||||
{
|
||||
"Channel util.": formatFloat(
|
||||
metrics.get("channelUtilization"), 2, "%"
|
||||
),
|
||||
"Tx air util.": formatFloat(
|
||||
metrics.get("airUtilTx"), 2, "%"
|
||||
),
|
||||
}
|
||||
)
|
||||
raw_value = node.get(field)
|
||||
|
||||
row.update(
|
||||
{
|
||||
"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")),
|
||||
}
|
||||
)
|
||||
formatted_value: Optional[str] = ""
|
||||
|
||||
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)
|
||||
for i, row in enumerate(rows):
|
||||
@@ -345,7 +395,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
if new_index != last_index:
|
||||
retries_left = requestChannelAttempts - 1
|
||||
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")
|
||||
n.requestChannels(startingIndex=new_index)
|
||||
last_index = new_index
|
||||
@@ -361,6 +411,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse: bool = False,
|
||||
onResponse: Optional[Callable[[dict], Any]] = None,
|
||||
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
|
||||
will also be shown on the device.
|
||||
@@ -371,12 +422,12 @@ class MeshInterface: # pylint: disable=R0902
|
||||
Keyword Arguments:
|
||||
destinationId {nodeId or nodeNum} -- where to send this
|
||||
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
|
||||
(with retries and ack/nak provided for delivery)
|
||||
wantResponse -- True if you want the service on the other side to
|
||||
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
|
||||
and can be used to track future message acks/naks.
|
||||
@@ -385,7 +436,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
return self.sendData(
|
||||
text.encode("utf-8"),
|
||||
destinationId,
|
||||
portNum=portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
||||
portNum=portNum,
|
||||
wantAck=wantAck,
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
@@ -892,8 +943,10 @@ class MeshInterface: # pylint: disable=R0902
|
||||
else:
|
||||
our_exit("Warning: No myInfo found.")
|
||||
# A simple hex style nodeid - we can parse this without needing the DB
|
||||
elif destinationId.startswith("!"):
|
||||
nodeNum = int(destinationId[1:], 16)
|
||||
elif isinstance(destinationId, str) and len(destinationId) >= 8:
|
||||
# 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:
|
||||
if self.nodes:
|
||||
node = self.nodes.get(destinationId)
|
||||
@@ -927,7 +980,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
toRadio.packet.CopyFrom(meshPacket)
|
||||
if self.noProto:
|
||||
logging.warning(
|
||||
f"Not sending packet because protocol use is disabled by noProto"
|
||||
"Not sending packet because protocol use is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
logging.debug(f"Sending packet: {stripnl(meshPacket)}")
|
||||
@@ -1116,7 +1169,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
if self.noProto:
|
||||
logging.warning(
|
||||
f"Not sending packet because protocol use is disabled by noProto"
|
||||
"Not sending packet because protocol use is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
# logging.debug(f"Sending toRadio: {stripnl(toRadio)}")
|
||||
|
||||
@@ -337,14 +337,19 @@ class Node:
|
||||
s = s.replace("=", "").replace("+", "-").replace("/", "_")
|
||||
return f"https://meshtastic.org/e/#{s}"
|
||||
|
||||
def setURL(self, url):
|
||||
def setURL(self, url: str, addOnly: bool = False):
|
||||
"""Set mesh network URL"""
|
||||
if self.localConfig is None:
|
||||
our_exit("Warning: No Config has been read")
|
||||
if self.localConfig is None or self.channels is None:
|
||||
our_exit("Warning: config or channels not loaded")
|
||||
|
||||
# URLs are of the form https://meshtastic.org/d/#{base64_channel_set}
|
||||
# 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]
|
||||
|
||||
# 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:
|
||||
our_exit("Warning: There were no settings.")
|
||||
|
||||
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
|
||||
if addOnly:
|
||||
# Add new channels with names not already present
|
||||
# Don't change existing channels
|
||||
for chs in channelSet.settings:
|
||||
channelExists = self.getChannelByName(chs.name)
|
||||
if channelExists or chs.name == "":
|
||||
print(f"Ignoring existing or empty channel \"{chs.name}\" from add URL")
|
||||
continue
|
||||
ch = self.getDisabledChannel()
|
||||
if not ch:
|
||||
our_exit("Warning: No free channels were found")
|
||||
ch.settings.CopyFrom(chs)
|
||||
ch.role = channel_pb2.Channel.Role.SECONDARY
|
||||
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.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
|
||||
WAKE_ON_TAP_OR_MOTION_FIELD_NUMBER: builtins.int
|
||||
COMPASS_ORIENTATION_FIELD_NUMBER: builtins.int
|
||||
USE_12H_CLOCK_FIELD_NUMBER: builtins.int
|
||||
screen_on_secs: builtins.int
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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__(
|
||||
self,
|
||||
*,
|
||||
@@ -1179,8 +1185,9 @@ class Config(google.protobuf.message.Message):
|
||||
heading_bold: builtins.bool = ...,
|
||||
wake_on_tap_or_motion: builtins.bool = ...,
|
||||
compass_orientation: global___Config.DisplayConfig.CompassOrientation.ValueType = ...,
|
||||
use_12h_clock: builtins.bool = ...,
|
||||
) -> 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
|
||||
class LoRaConfig(google.protobuf.message.Message):
|
||||
|
||||
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
|
||||
class CannedMessageConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
TODO: REPLACE
|
||||
Canned Messages Module Config
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
@@ -408,8 +408,8 @@ def test_main_nodes(capsys):
|
||||
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
|
||||
def mock_showNodes():
|
||||
print("inside mocked showNodes")
|
||||
def mock_showNodes(includeSelf, showFields):
|
||||
print(f"inside mocked showNodes: {includeSelf} {showFields}")
|
||||
|
||||
iface.showNodes.side_effect = mock_showNodes
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
@@ -593,10 +593,10 @@ def test_main_sendtext(capsys):
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
|
||||
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(f"{text} {dest} {wantAck} {wantResponse} {channelIndex}")
|
||||
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}")
|
||||
|
||||
iface.sendText.side_effect = mock_sendText
|
||||
|
||||
@@ -620,10 +620,10 @@ def test_main_sendtext_with_channel(capsys):
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
|
||||
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(f"{text} {dest} {wantAck} {wantResponse} {channelIndex}")
|
||||
print(f"{text} {dest} {wantAck} {wantResponse} {channelIndex} {portNum}")
|
||||
|
||||
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.value.code == 1
|
||||
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 == ""
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ def test_setURL_valid_URL_but_no_settings(capsys):
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
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 == ""
|
||||
|
||||
|
||||
|
||||
Submodule protobufs updated: 7f13df0e5f...068646653e
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "meshtastic"
|
||||
version = "2.5.11"
|
||||
version = "2.5.12"
|
||||
description = "Python API & client shell for talking to Meshtastic devices"
|
||||
authors = ["Meshtastic Developers <contact@meshtastic.org>"]
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
Reference in New Issue
Block a user