-
Notifications
You must be signed in to change notification settings - Fork 288
Send request broadcast userinfo #846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -568,6 +568,36 @@ def onConnected(interface): | |||||
| telemetryType=telemType, | ||||||
| ) | ||||||
|
|
||||||
| if args.request_userinfo: | ||||||
| if args.dest == BROADCAST_ADDR: | ||||||
| meshtastic.util.our_exit("Warning: Must use a destination node ID.") | ||||||
| else: | ||||||
| channelIndex = mt_config.channel_index or 0 | ||||||
| if checkChannel(interface, channelIndex): | ||||||
| print( | ||||||
| f"Sending userinfo request to {args.dest} on channelIndex:{channelIndex} (this could take a while)" | ||||||
| ) | ||||||
| interface.request_user_info(destinationId=args.dest, channelIndex=channelIndex) | ||||||
| closeNow = True | ||||||
|
|
||||||
| if args.broadcast_userinfo: | ||||||
| channelIndex = mt_config.channel_index or 0 | ||||||
| if args.dest != BROADCAST_ADDR: | ||||||
| print("Warning: --broadcast-userinfo ignores --dest and always broadcasts to all nodes") | ||||||
| if checkChannel(interface, channelIndex): | ||||||
| print(f"Broadcasting our userinfo to all nodes on channelIndex:{channelIndex}") | ||||||
| interface.request_user_info(destinationId=BROADCAST_ADDR, wantResponse=False, channelIndex=channelIndex) | ||||||
| closeNow = True | ||||||
|
|
||||||
| if args.send_userinfo: | ||||||
| channelIndex = mt_config.channel_index or 0 | ||||||
| if args.dest == BROADCAST_ADDR: | ||||||
| meshtastic.util.our_exit("Error: --send-userinfo requires a destination node ID with --dest") | ||||||
| if checkChannel(interface, channelIndex): | ||||||
| print(f"Sending our userinfo to {args.dest} on channelIndex:{channelIndex}") | ||||||
| interface.request_user_info(destinationId=args.dest, wantResponse=False, channelIndex=channelIndex) | ||||||
| closeNow = True | ||||||
|
|
||||||
| if args.request_position: | ||||||
| if args.dest == BROADCAST_ADDR: | ||||||
| meshtastic.util.our_exit("Warning: Must use a destination node ID.") | ||||||
|
|
@@ -1876,6 +1906,27 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar | |||||
| action="store_true", | ||||||
| ) | ||||||
|
|
||||||
| group.add_argument( | ||||||
| "--request-userinfo", | ||||||
| help="Request user information from a specific node. " | ||||||
| "You need to pass the destination ID as an argument with '--dest'. " | ||||||
| "For repeaters, the nodeNum is required.", | ||||||
| action="store_true", | ||||||
| ) | ||||||
|
|
||||||
| group.add_argument( | ||||||
| "--broadcast-userinfo", | ||||||
| help="Broadcast your user information to all nodes in the mesh network.", | ||||||
| action="store_true", | ||||||
| ) | ||||||
|
|
||||||
| group.add_argument( | ||||||
| "--send-userinfo", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's have this be available as
Suggested change
|
||||||
| help="Send your user information to a specific node without requesting a response. " | ||||||
| "Must be used with --dest to specify the destination node.", | ||||||
| action="store_true", | ||||||
| ) | ||||||
|
Comment on lines
+1917
to
+1928
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see a reason to have these two as separate flags; I think they can both be under |
||||||
|
|
||||||
| group.add_argument( | ||||||
| "--reply", help="Reply to received messages", action="store_true" | ||||||
| ) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -483,6 +483,60 @@ def sendAlert( | |||||||||||||||||||||
| priority=mesh_pb2.MeshPacket.Priority.ALERT | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def request_user_info( | ||||||||||||||||||||||
| self, | ||||||||||||||||||||||
| destinationId: Union[int, str], | ||||||||||||||||||||||
| wantResponse: bool = True, | ||||||||||||||||||||||
| channelIndex: int = 0, | ||||||||||||||||||||||
| ) -> mesh_pb2.MeshPacket: | ||||||||||||||||||||||
|
Comment on lines
+486
to
+491
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function should match the others here in MeshInterface. In particular, it should probably be called |
||||||||||||||||||||||
| """Request user information from another node by sending our own user info. | ||||||||||||||||||||||
| The remote node will respond with their user info when they receive this request. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Arguments: | ||||||||||||||||||||||
| destinationId {nodeId or nodeNum} -- The node to request info from | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Keyword Arguments: | ||||||||||||||||||||||
| wantResponse {bool} -- Whether to request a response with the target's user info (default: True) | ||||||||||||||||||||||
| channelIndex {int} -- The channel to use for this request (default: 0) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||
| The sent packet. The id field will be populated and can be used to track responses. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| # Get our node's user info to send in the request | ||||||||||||||||||||||
| my_node_info = self.getMyNodeInfo() | ||||||||||||||||||||||
| logger.debug(f"Local node info: {my_node_info}") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if my_node_info is None or "user" not in my_node_info: | ||||||||||||||||||||||
| raise MeshInterface.MeshInterfaceError("Could not get local node user info") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Create a User message with our info | ||||||||||||||||||||||
| user = mesh_pb2.User() | ||||||||||||||||||||||
| node_user = my_node_info["user"] | ||||||||||||||||||||||
| logger.debug(f"Local user info to send: {node_user}") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Copy fields from our node's user info, matching firmware behavior | ||||||||||||||||||||||
| user.id = node_user.get("id", "") # Set to nodeDB->getNodeId() in firmware | ||||||||||||||||||||||
| user.long_name = node_user.get("longName", "") | ||||||||||||||||||||||
| user.short_name = node_user.get("shortName", "") | ||||||||||||||||||||||
| user.hw_model = node_user.get("hwModel", 0) | ||||||||||||||||||||||
| user.is_licensed = node_user.get("is_licensed", False) | ||||||||||||||||||||||
| user.role = node_user.get("role", 0) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Handle public key - firmware strips it if node is licensed | ||||||||||||||||||||||
| if "public_key" in node_user and not user.is_licensed: | ||||||||||||||||||||||
| user.public_key = node_user["public_key"] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||
| # Send our user info to request the remote user's info | ||||||||||||||||||||||
| # Using BACKGROUND priority as per firmware default | ||||||||||||||||||||||
| return self.sendData( | ||||||||||||||||||||||
| user.SerializeToString(), | ||||||||||||||||||||||
| destinationId, | ||||||||||||||||||||||
| portNum=portnums_pb2.PortNum.NODEINFO_APP, | ||||||||||||||||||||||
| wantResponse=wantResponse, | ||||||||||||||||||||||
| channelIndex=channelIndex, | ||||||||||||||||||||||
| priority=mesh_pb2.MeshPacket.Priority.BACKGROUND | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def sendMqttClientProxyMessage(self, topic: str, data: bytes): | ||||||||||||||||||||||
| """Send an MQTT Client Proxy message to the radio. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -1315,8 +1369,14 @@ def _handleFromRadio(self, fromRadioBytes): | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| elif fromRadio.HasField("node_info"): | ||||||||||||||||||||||
| logger.debug(f"Received nodeinfo: {asDict['nodeInfo']}") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| node = self._getOrCreateByNum(asDict["nodeInfo"]["num"]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Track if this is a response to our user info request | ||||||||||||||||||||||
| node_num = asDict["nodeInfo"]["num"] | ||||||||||||||||||||||
| if "user" in asDict["nodeInfo"]: | ||||||||||||||||||||||
| node_id = asDict["nodeInfo"]["user"].get("id", "") | ||||||||||||||||||||||
| logger.debug(f"Received node info from node {node_id} (num: {node_num})") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| node = self._getOrCreateByNum(node_num) | ||||||||||||||||||||||
|
Comment on lines
+1372
to
+1379
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't belong here. The
Suggested change
|
||||||||||||||||||||||
| node.update(asDict["nodeInfo"]) | ||||||||||||||||||||||
| try: | ||||||||||||||||||||||
| newpos = self._fixupPosition(node["position"]) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """Test request_user_info functionality.""" | ||
|
|
||
| import logging | ||
| import pytest | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| from meshtastic.mesh_interface import MeshInterface | ||
| from meshtastic.protobuf import mesh_pb2, portnums_pb2 | ||
| from meshtastic import BROADCAST_ADDR | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| @pytest.mark.usefixtures("reset_mt_config") | ||
| def test_request_user_info_missing_node_info(): | ||
| """Test request_user_info when local node info is not available""" | ||
| iface = MeshInterface(noProto=True) | ||
| with pytest.raises(MeshInterface.MeshInterfaceError) as exc_info: | ||
| iface.request_user_info(destinationId=1) | ||
| assert "Could not get local node user info" in str(exc_info.value) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| @pytest.mark.usefixtures("reset_mt_config") | ||
| def test_request_user_info_valid(caplog): | ||
| """Test request_user_info with valid node info""" | ||
| with caplog.at_level(logging.DEBUG): | ||
| iface = MeshInterface(noProto=True) | ||
|
|
||
| # Mock getMyNodeInfo to return valid user data | ||
| mock_user = { | ||
| "user": { | ||
| "id": "!12345678", | ||
| "long_name": "Test Node", | ||
| "short_name": "TN", | ||
| "hw_model": 1, | ||
| "is_licensed": False, | ||
| "role": 0, | ||
| "public_key": b"testkey" | ||
| } | ||
| } | ||
| iface.getMyNodeInfo = MagicMock(return_value=mock_user) | ||
|
|
||
| # Call request_user_info | ||
| result = iface.request_user_info(destinationId=1) | ||
|
|
||
| # Verify a mesh packet was created with correct fields | ||
| assert isinstance(result, mesh_pb2.MeshPacket) | ||
| assert result.decoded.portnum == portnums_pb2.PortNum.NODEINFO_APP_VALUE | ||
| assert result.want_response == True | ||
| assert result.to == 1 | ||
|
|
||
| # Verify the serialized user info was sent as payload | ||
| decoded_user = mesh_pb2.User() | ||
| decoded_user.ParseFromString(result.decoded.payload) | ||
| assert decoded_user.id == "!12345678" | ||
| assert decoded_user.long_name == "Test Node" | ||
| assert decoded_user.short_name == "TN" | ||
| assert decoded_user.hw_model == 1 | ||
| assert decoded_user.is_licensed == False | ||
| assert decoded_user.role == 0 | ||
| assert decoded_user.public_key == b"testkey" | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| @pytest.mark.usefixtures("reset_mt_config") | ||
| def test_request_user_info_response_handling(caplog): | ||
| """Test handling of responses to user info requests""" | ||
| with caplog.at_level(logging.DEBUG): | ||
| iface = MeshInterface(noProto=True) | ||
| iface.nodes = {} # Initialize nodes dict | ||
|
|
||
| # Mock user info in response packet | ||
| user_info = mesh_pb2.User() | ||
| user_info.id = "!abcdef12" | ||
| user_info.long_name = "Remote Node" | ||
| user_info.short_name = "RN" | ||
|
|
||
| # Create response packet | ||
| packet = mesh_pb2.MeshPacket() | ||
| packet.from_ = 123 # Note: Using from_ to avoid Python keyword | ||
| packet.decoded.portnum = portnums_pb2.PortNum.NODEINFO_APP_VALUE | ||
| packet.decoded.payload = user_info.SerializeToString() | ||
|
|
||
| # Process the received packet | ||
| iface._handlePacketFromRadio(packet) | ||
|
|
||
| # Verify node info was stored correctly | ||
| assert "!abcdef12" in iface.nodes | ||
| stored_node = iface.nodes["!abcdef12"] | ||
| assert stored_node["user"]["id"] == "!abcdef12" | ||
| assert stored_node["user"]["longName"] == "Remote Node" | ||
| assert stored_node["user"]["shortName"] == "RN" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's have this as
--request-usertoo, to match the protobuf name.