def setURL(self, url): """Set mesh network URL""" if self.radioConfig is None: our_exit("Warning: No RadioConfig has been read") # URLs are of the form https://www.meshtastic.org/d/#{base64_channel_set} # Split on '/#' to find the base64 encoded channel settings splitURL = url.split("/#") b64 = splitURL[-1] # We normally strip padding to make for a shorter URL, but the python parser doesn't like # that. So add back any missing padding # per https://stackoverflow.com/a/9807138 missing_padding = len(b64) % 4 if missing_padding: b64 += '=' * (4 - missing_padding) decodedURL = base64.urlsafe_b64decode(b64) channelSet = apponly_pb2.ChannelSet() channelSet.ParseFromString(decodedURL) 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
def set_canned_message(self, message): """Set the canned message. Split into parts of 200 chars each.""" if len(message) > 800: our_exit( "Warning: The canned message must be less than 800 characters." ) # split into chunks chunks = [] chunks_size = 200 for i in range(0, len(message), chunks_size): chunks.append(message[i:i + chunks_size]) # for each chunk, send a message to set the values #for i in range(0, len(chunks)): for i, chunk in enumerate(chunks): p = admin_pb2.AdminMessage() # TODO: should be a way to improve this if i == 0: p.set_canned_message_plugin_part1 = chunk elif i == 1: p.set_canned_message_plugin_part2 = chunk elif i == 2: p.set_canned_message_plugin_part3 = chunk elif i == 3: p.set_canned_message_plugin_part4 = chunk logging.debug(f"Setting canned message '{chunk}' part {i+1}") self._sendAdmin(p)
def deleteChannel(self, channelIndex): """Delete the specifed channelIndex and shift other channels up""" ch = self.channels[channelIndex] if ch.role not in (channel_pb2.Channel.Role.SECONDARY, channel_pb2.Channel.Role.DISABLED): our_exit("Warning: Only SECONDARY channels can be deleted") # we are careful here because if we move the "admin" channel the channelIndex we need to use # for sending admin channels will also change adminIndex = self.iface.localNode._getAdminChannelIndex() self.channels.pop(channelIndex) self._fixupChannels() # expand back to 8 channels index = channelIndex while index < self.iface.myInfo.max_channels: self.writeChannel(index, adminIndex=adminIndex) index += 1 # if we are updating the local node, we might end up # *moving* the admin channel index as we are writing if (self.iface.localNode == self) and index >= adminIndex: # We've now passed the old location for admin index # (and written it), so we can start finding it by name again adminIndex = 0
def test_our_exit_non_zero_return_value(capsys): """Test our_exit with a non-zero return value""" with pytest.raises(SystemExit) as pytest_wrapped_e: our_exit("Error: Some message", 1) out, err = capsys.readouterr() assert re.search(r'Error: Some message', out, re.MULTILINE) assert err == '' assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 1
def writeConfig(self): """Write the current (edited) radioConfig to the device""" if self.radioConfig is None: our_exit("Error: No RadioConfig has been read") p = admin_pb2.AdminMessage() p.set_radio.CopyFrom(self.radioConfig) self._sendAdmin(p) logging.debug("Wrote config")
def getNode(self, nodeId): """Return a node object which contains device settings and channel info""" if nodeId in (LOCAL_ADDR, BROADCAST_ADDR): return self.localNode else: n = meshtastic.node.Node(self, nodeId) logging.debug("About to requestConfig") n.requestConfig() if not n.waitForConfig(): our_exit("Error: Timed out waiting for node config") return n
def _sendHardware(self, nodeid, r, wantResponse=False, onResponse=None): if not nodeid: our_exit( r"Warning: Must use a destination node ID for this operation (use --dest \!xxxxxxxxx)" ) return self.iface.sendData(r, nodeid, portnums_pb2.REMOTE_HARDWARE_APP, wantAck=True, channelIndex=self.channelIndex, wantResponse=wantResponse, onResponse=onResponse)
def __init__(self, iface): """ Constructor iface is the already open MeshInterface instance """ self.iface = iface ch = iface.localNode.getChannelByName("gpio") if not ch: our_exit( "Warning: No channel named 'gpio' was found.\n"\ "On the sending and receive nodes create a channel named 'gpio'.\n"\ "For example, run '--ch-add gpio' on one device, then '--seturl' on\n"\ "the other devices using the url from the device where the channel was added.") self.channelIndex = ch.index pub.subscribe(onGPIOreceive, "meshtastic.receive.remotehw")
def __init__(self, address, noProto=False, debugOut=None): if platform.system() != 'Linux': our_exit( "Linux is the only platform with experimental BLE support.", 1) self.address = address if not noProto: self.adapter = pygatt.GATTToolBackend() # BGAPIBackend() self.adapter.start() logging.debug(f"Connecting to {self.address}") self.device = self.adapter.connect(address) else: self.adapter = None self.device = None logging.debug("Connected to device") # fromradio = self.device.char_read(FROMRADIO_UUID) MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto) self._readFromRadio() # read the initial responses def handle_data(handle, data): # pylint: disable=W0613 self._handleFromRadio(data) if self.device: self.device.subscribe(FROMNUM_UUID, callback=handle_data)
def _sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR, wantAck=False, hopLimit=None): """Send a MeshPacket to the specified node (or if unspecified, broadcast). You probably don't want this - use sendData instead. Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. """ if hopLimit is None: hopLimit = self.defaultHopLimit # We allow users to talk to the local node before we've completed the full connection flow... if (self.myInfo is not None and destinationId != self.myInfo.my_node_num): self._waitConnected() toRadio = mesh_pb2.ToRadio() nodeNum = 0 if destinationId is None: our_exit("Warning: destinationId must not be None") elif isinstance(destinationId, int): nodeNum = destinationId elif destinationId == BROADCAST_ADDR: nodeNum = BROADCAST_NUM elif destinationId == LOCAL_ADDR: if self.myInfo: nodeNum = self.myInfo.my_node_num 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) else: if self.nodes: node = self.nodes.get(destinationId) if not node: our_exit( f"Warning: NodeId {destinationId} not found in DB") nodeNum = node['num'] else: logging.warning("Warning: There were no self.nodes.") meshPacket.to = nodeNum meshPacket.want_ack = wantAck meshPacket.hop_limit = hopLimit # if the user hasn't set an ID for this packet (likely and recommended), # we should pick a new unique ID so the message can be tracked. if meshPacket.id == 0: meshPacket.id = self._generatePacketId() toRadio.packet.CopyFrom(meshPacket) if self.noProto: logging.warning( f"Not sending packet because protocol use is disabled by noProto" ) else: logging.debug(f"Sending packet: {stripnl(meshPacket)}") self._sendToRadio(toRadio) return meshPacket
def sendData(self, data, destinationId=BROADCAST_ADDR, portNum=portnums_pb2.PortNum.PRIVATE_APP, wantAck=False, wantResponse=False, hopLimit=None, onResponse=None, channelIndex=0): """Send a data packet to some other node Keyword Arguments: data -- the data to send, either as an array of bytes or as a protobuf (which will be automatically serialized to bytes) 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 onResponse -- A closure of the form funct(packet), that will be called when a response packet arrives (or the transaction is NAKed due to non receipt) channelIndex - channel number to use Returns the sent packet. The id field will be populated in this packet and can be used to track future message acks/naks. """ if hopLimit is None: hopLimit = self.defaultHopLimit if getattr(data, "SerializeToString", None): logging.debug(f"Serializing protobuf as data: {stripnl(data)}") data = data.SerializeToString() logging.debug(f"len(data): {len(data)}") logging.debug( f"mesh_pb2.Constants.DATA_PAYLOAD_LEN: {mesh_pb2.Constants.DATA_PAYLOAD_LEN}" ) if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN: raise Exception("Data payload too big") if portNum == portnums_pb2.PortNum.UNKNOWN_APP: # we are now more strict wrt port numbers our_exit("Warning: A non-zero port number must be specified") meshPacket = mesh_pb2.MeshPacket() meshPacket.channel = channelIndex meshPacket.decoded.payload = data meshPacket.decoded.portnum = portNum meshPacket.decoded.want_response = wantResponse meshPacket.id = self._generatePacketId() if onResponse is not None: self._addResponseHandler(meshPacket.id, onResponse) p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit) return p
def test_our_exit_non_zero_return_value(): """Test our_exit with a non-zero return value""" with pytest.raises(SystemExit) as pytest_wrapped_e: our_exit("Error: Some message", 1) assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 1
def test_our_exit_zero_return_value(): """Test our_exit with a zero return value""" with pytest.raises(SystemExit) as pytest_wrapped_e: our_exit("Warning: Some message", 0) assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 0