Skip to content

Commit

Permalink
Merge pull request #175 from mkinney/continue_working_on_unit_tests
Browse files Browse the repository at this point in the history
add unit tests for toRadio, sendData() and sendPosition(); found and …
  • Loading branch information
mkinney committed Dec 23, 2021
2 parents 71c8ca5 + 0f0a978 commit d21eaf9
Show file tree
Hide file tree
Showing 7 changed files with 1,051 additions and 50 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ install:
lint:
pylint meshtastic

# run the coverage report and open results in a browser
cov:
pytest --cov-report html --cov=meshtastic
# on mac, this will open the coverage report in a browser
Expand All @@ -19,4 +20,5 @@ cov:
examples: FORCE
pytest -mexamples

# Makefile hack to get the examples to always run
FORCE: ;
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ An example using Python 3 code to send a message to the mesh:

```
import meshtastic
interface = meshtastic.SerialInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
interface = meshtastic.serial_interface.SerialInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
interface.sendText("hello mesh") # or sendData to send binary data, see documentations for other options.
interface.close()
```
Expand Down Expand Up @@ -103,7 +103,7 @@ You can even set the channel preshared key to a particular AES128 or AES256 sequ
meshtastic --ch-set psk 0x1a1a1a1a2b2b2b2b1a1a1a1a2b2b2b2b1a1a1a1a2b2b2b2b1a1a1a1a2b2b2b2b --info
```

Use "--ch-set psk none" to turn off encryption.
Use "--ch-set psk none" to turn off encryption.

Use "--ch-set psk random" will assign a new (high quality) random AES256 key to the primary channel (similar to what the Android app does when making new channels).

Expand Down
20 changes: 15 additions & 5 deletions meshtastic/mesh_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def getTimeAgo(ts):

rows = []
if self.nodes:
logging.debug(f'self.nodes:{self.nodes}')
for node in self.nodes.values():
if not includeSelf and node['num'] == self.localNode.nodeNum:
continue
Expand Down Expand Up @@ -227,8 +228,9 @@ def sendData(self, data, destinationId=BROADCAST_ADDR,
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:
Exception("Data payload too big")
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")
Expand Down Expand Up @@ -261,12 +263,15 @@ def sendPosition(self, latitude=0.0, longitude=0.0, altitude=0, timeSec=0,
p = mesh_pb2.Position()
if latitude != 0.0:
p.latitude_i = int(latitude / 1e-7)
logging.debug(f'p.latitude_i:{p.latitude_i}')

if longitude != 0.0:
p.longitude_i = int(longitude / 1e-7)
logging.debug(f'p.longitude_i:{p.longitude_i}')

if altitude != 0:
p.altitude = int(altitude)
logging.debug(f'p.altitude:{p.altitude}')

if timeSec == 0:
timeSec = time.time() # returns unix timestamp in seconds
Expand Down Expand Up @@ -307,7 +312,10 @@ def _sendPacket(self, meshPacket,
elif destinationId == BROADCAST_ADDR:
nodeNum = BROADCAST_NUM
elif destinationId == LOCAL_ADDR:
nodeNum = self.myInfo.my_node_num
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)
Expand All @@ -330,7 +338,7 @@ def _sendPacket(self, meshPacket,
meshPacket.id = self._generatePacketId()

toRadio.packet.CopyFrom(meshPacket)
#logging.debug(f"Sending packet: {stripnl(meshPacket)}")
logging.debug(f"Sending packet: {stripnl(meshPacket)}")
self._sendToRadio(toRadio)
return meshPacket

Expand All @@ -344,6 +352,7 @@ def getMyNodeInfo(self):
"""Get info about my node."""
if self.myInfo is None:
return None
logging.debug(f'self.nodesByNum:{self.nodesByNum}')
return self.nodesByNum.get(self.myInfo.my_node_num)

def getMyUser(self):
Expand All @@ -370,8 +379,9 @@ def getShortName(self):
def _waitConnected(self):
"""Block until the initial node db download is complete, or timeout
and raise an exception"""
if not self.isConnected.wait(15.0): # timeout after x seconds
raise Exception("Timed out waiting for connection completion")
if not self.noProto:
if not self.isConnected.wait(15.0): # timeout after x seconds
raise Exception("Timed out waiting for connection completion")

# If we failed while connecting, raise the connection to the client
if self.failure:
Expand Down
94 changes: 52 additions & 42 deletions meshtastic/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from .util import pskToString, stripnl, Timeout, our_exit, fromPSK




class Node:
"""A model of a (local or remote) node in the mesh
Expand All @@ -28,7 +30,9 @@ def showChannels(self):
"""Show human readable description of our channels."""
print("Channels:")
if self.channels:
logging.debug(f'self.channels:{self.channels}')
for c in self.channels:
#print('c.settings.psk:', c.settings.psk)
cStr = stripnl(MessageToJson(c.settings))
# only show if there is no psk (meaning disabled channel)
if c.settings.psk:
Expand Down Expand Up @@ -108,7 +112,7 @@ def deleteChannel(self, channelIndex):
# *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 writen it), so we can start finding it by name again
# (and written it), so we can start finding it by name again
adminIndex = 0

def getChannelByName(self, name):
Expand Down Expand Up @@ -220,25 +224,29 @@ def setURL(self, url):
self.writeChannel(ch.index)
i = i + 1


def onResponseRequestSettings(self, p):
"""Handle the response packet for requesting settings _requestSettings()"""
logging.debug(f'onResponseRequestSetting() p:{p}')
errorFound = False
if 'routing' in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE":
errorFound = True
print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
if errorFound is False:
self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
logging.debug(f'self.radioConfig:{self.radioConfig}')
logging.debug("Received radio config, now fetching channels...")
self._timeout.reset() # We made foreward progress
self._requestChannel(0) # now start fetching channels


def _requestSettings(self):
"""Done with initial config messages, now send regular
MeshPackets to ask for settings."""
p = admin_pb2.AdminMessage()
p.get_radio_request = True

def onResponse(p):
"""A closure to handle the response packet"""
errorFound = False
if 'routing' in p["decoded"]:
if p["decoded"]["routing"]["errorReason"] != "NONE":
errorFound = True
print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
if errorFound is False:
self.radioConfig = p["decoded"]["admin"]["raw"].get_radio_response
logging.debug("Received radio config, now fetching channels...")
self._timeout.reset() # We made foreward progress
self._requestChannel(0) # now start fetching channels

# Show progress message for super slow operations
if self != self.iface.localNode:
print("Requesting preferences from remote node.")
Expand All @@ -249,7 +257,7 @@ def onResponse(p):
print(" 4. All devices have been rebooted after all of the above. (optional, but recommended)")
print("Note: This could take a while (it requests remote channel configs, then writes config)")

return self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
return self._sendAdmin(p, wantResponse=True, onResponse=self.onResponseRequestSettings)

def exitSimulator(self):
"""Tell a simulator node to exit (this message
Expand Down Expand Up @@ -290,6 +298,34 @@ def _fillChannels(self):
self.channels.append(ch)
index += 1


def onResponseRequestChannel(self, p):
"""Handle the response packet for requesting a channel _requestChannel()"""
logging.debug(f'onResponseRequestChannel() p:{p}')
c = p["decoded"]["admin"]["raw"].get_channel_response
self.partialChannels.append(c)
self._timeout.reset() # We made foreward progress
logging.debug(f"Received channel {stripnl(c)}")
index = c.index

# for stress testing, we can always download all channels
fastChannelDownload = True

# Once we see a response that has NO settings, assume
# we are at the end of channels and stop fetching
quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload

if quitEarly or index >= self.iface.myInfo.max_channels - 1:
logging.debug("Finished downloading channels")

self.channels = self.partialChannels
self._fixupChannels()

# FIXME, the following should only be called after we have settings and channels
self.iface._connected() # Tell everyone else we are ready to go
else:
self._requestChannel(index + 1)

def _requestChannel(self, channelNum: int):
"""Done with initial config messages, now send regular
MeshPackets to ask for settings"""
Expand All @@ -303,33 +339,7 @@ def _requestChannel(self, channelNum: int):
else:
logging.debug(f"Requesting channel {channelNum}")

def onResponse(p):
"""A closure to handle the response packet for requesting a channel"""
c = p["decoded"]["admin"]["raw"].get_channel_response
self.partialChannels.append(c)
self._timeout.reset() # We made foreward progress
logging.debug(f"Received channel {stripnl(c)}")
index = c.index

# for stress testing, we can always download all channels
fastChannelDownload = True

# Once we see a response that has NO settings, assume
# we are at the end of channels and stop fetching
quitEarly = (c.role == channel_pb2.Channel.Role.DISABLED) and fastChannelDownload

if quitEarly or index >= self.iface.myInfo.max_channels - 1:
logging.debug("Finished downloading channels")

self.channels = self.partialChannels
self._fixupChannels()

# FIXME, the following should only be called after we have settings and channels
self.iface._connected() # Tell everyone else we are ready to go
else:
self._requestChannel(index + 1)

return self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
return self._sendAdmin(p, wantResponse=True, onResponse=self.onResponseRequestChannel)


# pylint: disable=R1710
Expand Down
45 changes: 45 additions & 0 deletions meshtastic/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import argparse

from unittest.mock import MagicMock
import pytest

from meshtastic.__main__ import Globals
from ..mesh_interface import MeshInterface

@pytest.fixture
def reset_globals():
Expand All @@ -13,3 +15,46 @@ def reset_globals():
parser = argparse.ArgumentParser()
Globals.getInstance().reset()
Globals.getInstance().set_parser(parser)


@pytest.fixture
def iface_with_nodes():
"""Fixture to setup some nodes."""
nodesById = {
'!9388f81c': {
'num': 2475227164,
'user': {
'id': '!9388f81c',
'longName': 'Unknown f81c',
'shortName': '?1C',
'macaddr': 'RBeTiPgc',
'hwModel': 'TBEAM'
},
'position': {},
'lastHeard': 1640204888
}
}

nodesByNum = {
2475227164: {
'num': 2475227164,
'user': {
'id': '!9388f81c',
'longName': 'Unknown f81c',
'shortName': '?1C',
'macaddr': 'RBeTiPgc',
'hwModel': 'TBEAM'
},
'position': {
'time': 1640206266
},
'lastHeard': 1640206266
}
}
iface = MeshInterface(noProto=True)
iface.nodes = nodesById
iface.nodesByNum = nodesByNum
myInfo = MagicMock()
iface.myInfo = myInfo
iface.myInfo.my_node_num = 2475227164
return iface

0 comments on commit d21eaf9

Please sign in to comment.