mirror of
https://github.com/meshtastic/python.git
synced 2025-12-26 09:27:52 -05:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a3a840269 | ||
|
|
fe69f05e75 | ||
|
|
5c662822b9 | ||
|
|
c049d3424a | ||
|
|
471535853b | ||
|
|
676148cc14 | ||
|
|
a915b05240 | ||
|
|
a1668e8c66 | ||
|
|
e7664cb40b | ||
|
|
83c18f4008 | ||
|
|
8b6321ce7f | ||
|
|
9fac981ba6 | ||
|
|
ccc71930f7 | ||
|
|
9380f048fa | ||
|
|
0a655ac8df | ||
|
|
0b6676c5b3 | ||
|
|
e5ecba7ec0 | ||
|
|
a1809f5b84 | ||
|
|
9c66447913 | ||
|
|
65960fb982 | ||
|
|
9d0bc09e0f | ||
|
|
475ddcc8dd | ||
|
|
105276f98e | ||
|
|
4ee647403b | ||
|
|
10f48f130f | ||
|
|
bd697864e4 | ||
|
|
ab876c9efd | ||
|
|
aba303c677 | ||
|
|
43d59ca8d8 | ||
|
|
177705aeff | ||
|
|
b92fff0da6 | ||
|
|
6a6b72a2ae | ||
|
|
614a90c0eb | ||
|
|
9adbed4be6 | ||
|
|
809f005f61 | ||
|
|
d366e74e86 | ||
|
|
3f307880f9 | ||
|
|
50523ec1b1 | ||
|
|
684b2885aa | ||
|
|
f5eb8738fb | ||
|
|
14941c742a | ||
|
|
217add3b00 | ||
|
|
4bac85b6a9 | ||
|
|
36bed11959 | ||
|
|
6f9bcfaaff | ||
|
|
f17b66c872 | ||
|
|
040cb9bf34 | ||
|
|
3269b3018f | ||
|
|
aab10b0912 | ||
|
|
e2e9b7d55e | ||
|
|
cecc5c3b25 | ||
|
|
54bb846d00 | ||
|
|
1a5f525632 | ||
|
|
8ba3d26d63 | ||
|
|
b341b6cfdb | ||
|
|
38e7972191 | ||
|
|
5be70328fe | ||
|
|
dfe798dbdf | ||
|
|
d98b23dba0 | ||
|
|
4fbe5c7863 | ||
|
|
69bb8bcca2 | ||
|
|
bb2ea17371 | ||
|
|
b51af8a070 | ||
|
|
aede26694c | ||
|
|
8e1010e9f2 | ||
|
|
ce2d9f5728 | ||
|
|
9879e9f2df | ||
|
|
e79faf93d0 | ||
|
|
181c04716a | ||
|
|
c7d981ec35 | ||
|
|
75fe7622a4 | ||
|
|
5dc800f9a3 | ||
|
|
c7d3f9f787 | ||
|
|
c70d36d2cd | ||
|
|
f239c23d9a | ||
|
|
ac5d729cdf | ||
|
|
047f43534f | ||
|
|
c26e030f1f |
@@ -1,2 +1,6 @@
|
||||
[run]
|
||||
omit = meshtastic/*_pb2.py,meshtastic/tests/*.py,meshtastic/test.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
if __name__ == .__main__.:
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -10,12 +10,17 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Python 3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Uninstall meshtastic
|
||||
run: |
|
||||
pip3 uninstall meshtastic
|
||||
@@ -32,14 +37,31 @@ jobs:
|
||||
run: pylint meshtastic
|
||||
- name: Run tests with pytest
|
||||
run: pytest --cov=meshtastic
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
pytest --cov=meshtastic --cov-report=xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
yml: ./codecov.yml
|
||||
fail_ci_if_error: true
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Python 3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install meshtastic from local
|
||||
run: |
|
||||
pip3 install .
|
||||
|
||||
35
.github/workflows/publish_to_pypi.yml
vendored
Normal file
35
.github/workflows/publish_to_pypi.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Publish PyPI
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install pypa/build
|
||||
run: >-
|
||||
python -m
|
||||
pip install
|
||||
build
|
||||
--user
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: >-
|
||||
python -m
|
||||
build
|
||||
--sdist
|
||||
--wheel
|
||||
--outdir dist/
|
||||
.
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
username: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
24
.github/workflows/update_protobufs.yml
vendored
Normal file
24
.github/workflows/update_protobufs.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: "Update protobufs"
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
update-protobufs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Update Submodule
|
||||
run: |
|
||||
git pull --recurse-submodules
|
||||
git submodule update --remote --recursive
|
||||
- name: Commit update
|
||||
run: |
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'bot@noreply.github.com'
|
||||
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||
git add proto
|
||||
git commit -m "Update protobuf submodule" && git push || echo "No changes to commit"
|
||||
@@ -23,7 +23,7 @@ ignore-patterns=mqtt_pb2.py,channel_pb2.py,environmental_measurement_pb2.py,admi
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
#
|
||||
disable=invalid-name,fixme,logging-fstring-interpolation,too-many-statements,too-many-branches,too-many-locals,no-member,f-string-without-interpolation,protected-access,no-self-use,pointless-string-statement,too-few-public-methods,consider-using-f-string,broad-except,no-else-return,unused-argument,global-statement,global-variable-not-assigned,too-many-boolean-expressions,no-else-raise,bare-except
|
||||
disable=invalid-name,fixme,logging-fstring-interpolation,too-many-statements,too-many-branches,too-many-locals,no-member,f-string-without-interpolation,protected-access,no-self-use,pointless-string-statement,too-few-public-methods,consider-using-f-string,broad-except,no-else-return,unused-argument,global-statement,global-variable-not-assigned,too-many-boolean-expressions,no-else-raise,bare-except,c-extension-no-member
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
2
Makefile
2
Makefile
@@ -16,7 +16,7 @@ lint:
|
||||
|
||||
# show the slowest unit tests
|
||||
slow:
|
||||
pytest --durations=0
|
||||
pytest --durations=5
|
||||
|
||||
# run the coverage report and open results in a browser
|
||||
cov:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[](https://open.vscode.dev/meshtastic/Meshtastic-python)
|
||||

|
||||
[](https://codecov.io/gh/meshtastic/Meshtastic-python)
|
||||
|
||||
A python client for using [Meshtastic](https://www.meshtastic.org) devices. This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. It also provides access to any of the operations/data available in the device user interface or the Android application. Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in.
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
rm dist/*
|
||||
set -e
|
||||
|
||||
bin/regen-docs.sh
|
||||
pandoc --from=markdown --to=rst --output=README README.md
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m twine upload dist/*
|
||||
python3 -m twine upload dist/*
|
||||
|
||||
@@ -77,9 +77,11 @@ from pubsub import pub
|
||||
from dotmap import DotMap
|
||||
from tabulate import tabulate
|
||||
from google.protobuf.json_format import MessageToJson
|
||||
from .util import fixme, catchAndIgnore, stripnl, DeferredExecution, Timeout
|
||||
from .node import Node
|
||||
from . import mesh_pb2, portnums_pb2, apponly_pb2, admin_pb2, environmental_measurement_pb2, remote_hardware_pb2, channel_pb2, radioconfig_pb2, util
|
||||
from meshtastic.util import fixme, catchAndIgnore, stripnl, DeferredExecution, Timeout
|
||||
from meshtastic.node import Node
|
||||
from meshtastic import (mesh_pb2, portnums_pb2, apponly_pb2, admin_pb2,
|
||||
environmental_measurement_pb2, remote_hardware_pb2,
|
||||
channel_pb2, radioconfig_pb2, util)
|
||||
|
||||
# Note: To follow PEP224, comments should be after the module variable.
|
||||
|
||||
|
||||
@@ -13,14 +13,12 @@ import pyqrcode
|
||||
import pkg_resources
|
||||
import meshtastic.util
|
||||
import meshtastic.test
|
||||
from . import remote_hardware
|
||||
from . import portnums_pb2, channel_pb2, radioconfig_pb2
|
||||
from .globals import Globals
|
||||
from meshtastic import remote_hardware
|
||||
from meshtastic.ble_interface import BLEInterface
|
||||
from meshtastic import portnums_pb2, channel_pb2, radioconfig_pb2
|
||||
from meshtastic.globals import Globals
|
||||
|
||||
|
||||
have_tunnel = platform.system() == 'Linux'
|
||||
"""We only import the tunnel code if we are on a platform that can run it. """
|
||||
|
||||
def onReceive(packet, interface):
|
||||
"""Callback invoked when a packet arrives"""
|
||||
our_globals = Globals.getInstance()
|
||||
@@ -45,7 +43,7 @@ def onReceive(packet, interface):
|
||||
interface.sendText(reply)
|
||||
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
print(f'Warning: There is no field {ex} in the packet.')
|
||||
|
||||
|
||||
def onConnection(interface, topic=pub.AUTO_TOPIC):
|
||||
@@ -137,7 +135,9 @@ def onConnected(interface):
|
||||
our_globals = Globals.getInstance()
|
||||
args = our_globals.get_args()
|
||||
|
||||
print("Connected to radio")
|
||||
# do not print this line if we are exporting the config
|
||||
if not args.export_config:
|
||||
print("Connected to radio")
|
||||
|
||||
def getNode():
|
||||
"""This operation could be expensive, so we try to cache the results"""
|
||||
@@ -495,12 +495,16 @@ def onConnected(interface):
|
||||
qr = pyqrcode.create(url)
|
||||
print(qr.terminal())
|
||||
|
||||
have_tunnel = platform.system() == 'Linux'
|
||||
if have_tunnel and args.tunnel:
|
||||
# pylint: disable=C0415
|
||||
from . import tunnel
|
||||
# Even if others said we could close, stay open if the user asked for a tunnel
|
||||
closeNow = False
|
||||
tunnel.Tunnel(interface, subnet=args.tunnel_net)
|
||||
if interface.noProto:
|
||||
logging.warning(f"Not starting Tunnel - disabled by noProto")
|
||||
else:
|
||||
tunnel.Tunnel(interface, subnet=args.tunnel_net)
|
||||
|
||||
# if the user didn't ask for serial debugging output, we might want to exit after we've done our operation
|
||||
if (not args.seriallog) and closeNow:
|
||||
@@ -567,6 +571,7 @@ def export_config(interface):
|
||||
|
||||
def common():
|
||||
"""Shared code for all of our command line wrappers"""
|
||||
logfile = None
|
||||
our_globals = Globals.getInstance()
|
||||
args = our_globals.get_args()
|
||||
parser = our_globals.get_parser()
|
||||
@@ -621,23 +626,24 @@ def common():
|
||||
logging.info(f"Logging serial output to {args.seriallog}")
|
||||
# Note: using "line buffering"
|
||||
# pylint: disable=R1732
|
||||
logfile = open(args.seriallog, 'w+',
|
||||
buffering=1, encoding='utf8')
|
||||
logfile = open(args.seriallog, 'w+', buffering=1, encoding='utf8')
|
||||
our_globals.set_logfile(logfile)
|
||||
|
||||
subscribe()
|
||||
if args.ble:
|
||||
client = meshtastic.ble_interface.BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto)
|
||||
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto)
|
||||
elif args.host:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
args.host, debugOut=logfile, noProto=args.noproto)
|
||||
client = meshtastic.tcp_interface.TCPInterface(args.host, debugOut=logfile, noProto=args.noproto)
|
||||
else:
|
||||
client = meshtastic.serial_interface.SerialInterface(
|
||||
args.port, debugOut=logfile, noProto=args.noproto)
|
||||
client = meshtastic.serial_interface.SerialInterface(args.port, debugOut=logfile, noProto=args.noproto)
|
||||
|
||||
# We assume client is fully connected now
|
||||
onConnected(client)
|
||||
#if logfile:
|
||||
#logfile.close()
|
||||
|
||||
if args.noproto or (have_tunnel and args.tunnel): # loop until someone presses ctrlc
|
||||
have_tunnel = platform.system() == 'Linux'
|
||||
if args.noproto or args.reply or (have_tunnel and args.tunnel): # loop until someone presses ctrlc
|
||||
while True:
|
||||
time.sleep(1000)
|
||||
|
||||
@@ -802,11 +808,13 @@ def initParser():
|
||||
parser.add_argument('--unset-router', dest='deprecated',
|
||||
action='store_false', help='Deprecated, use "--set is_router false" instead')
|
||||
|
||||
have_tunnel = platform.system() == 'Linux'
|
||||
if have_tunnel:
|
||||
parser.add_argument('--tunnel',
|
||||
action='store_true', help="Create a TUN tunnel device for forwarding IP packets over the mesh")
|
||||
parser.add_argument(
|
||||
"--subnet", dest='tunnel_net', help="Sets the local-end subnet address for the TUN IP bridge", default=None)
|
||||
parser.add_argument('--tunnel', action='store_true',
|
||||
help="Create a TUN tunnel device for forwarding IP packets over the mesh")
|
||||
parser.add_argument("--subnet", dest='tunnel_net',
|
||||
help="Sets the local-end subnet address for the TUN IP bridge. (ex: 10.115' which is the default)",
|
||||
default=None)
|
||||
|
||||
parser.set_defaults(deprecated=None)
|
||||
|
||||
@@ -828,6 +836,10 @@ def main():
|
||||
our_globals.set_parser(parser)
|
||||
initParser()
|
||||
common()
|
||||
logfile = our_globals.get_logfile()
|
||||
if logfile:
|
||||
logfile.close()
|
||||
|
||||
|
||||
|
||||
def tunnelMain():
|
||||
|
||||
@@ -12,8 +12,8 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from . import channel_pb2 as channel__pb2
|
||||
from . import mesh_pb2 as mesh__pb2
|
||||
from . import radioconfig_pb2 as radioconfig__pb2
|
||||
from . import mesh_pb2 as mesh__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
@@ -21,9 +21,9 @@ DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
package='',
|
||||
syntax='proto3',
|
||||
serialized_options=b'\n\023com.geeksville.meshB\013AdminProtosH\003Z!github.com/meshtastic/gomeshproto',
|
||||
serialized_pb=b'\n\x0b\x61\x64min.proto\x1a\rchannel.proto\x1a\nmesh.proto\x1a\x11radioconfig.proto\"\xfb\x02\n\x0c\x41\x64minMessage\x12!\n\tset_radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1a\n\tset_owner\x18\x02 \x01(\x0b\x32\x05.UserH\x00\x12\x1f\n\x0bset_channel\x18\x03 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_radio_request\x18\x04 \x01(\x08H\x00\x12*\n\x12get_radio_response\x18\x05 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1d\n\x13get_channel_request\x18\x06 \x01(\rH\x00\x12(\n\x14get_channel_response\x18\x07 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1d\n\x13\x63onfirm_set_channel\x18 \x01(\x08H\x00\x12\x1b\n\x11\x63onfirm_set_radio\x18! \x01(\x08H\x00\x12\x18\n\x0e\x65xit_simulator\x18\" \x01(\x08H\x00\x12\x18\n\x0ereboot_seconds\x18# \x01(\x05H\x00\x42\t\n\x07variantBG\n\x13\x63om.geeksville.meshB\x0b\x41\x64minProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3'
|
||||
serialized_pb=b'\n\x0b\x61\x64min.proto\x1a\rchannel.proto\x1a\x11radioconfig.proto\x1a\nmesh.proto\"\xbd\x03\n\x0c\x41\x64minMessage\x12!\n\tset_radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1a\n\tset_owner\x18\x02 \x01(\x0b\x32\x05.UserH\x00\x12\x1f\n\x0bset_channel\x18\x03 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_radio_request\x18\x04 \x01(\x08H\x00\x12*\n\x12get_radio_response\x18\x05 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1d\n\x13get_channel_request\x18\x06 \x01(\rH\x00\x12(\n\x14get_channel_response\x18\x07 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_owner_request\x18\x08 \x01(\x08H\x00\x12#\n\x12get_owner_response\x18\t \x01(\x0b\x32\x05.UserH\x00\x12\x1d\n\x13\x63onfirm_set_channel\x18 \x01(\x08H\x00\x12\x1b\n\x11\x63onfirm_set_radio\x18! \x01(\x08H\x00\x12\x18\n\x0e\x65xit_simulator\x18\" \x01(\x08H\x00\x12\x18\n\x0ereboot_seconds\x18# \x01(\x05H\x00\x42\t\n\x07variantBG\n\x13\x63om.geeksville.meshB\x0b\x41\x64minProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3'
|
||||
,
|
||||
dependencies=[channel__pb2.DESCRIPTOR,mesh__pb2.DESCRIPTOR,radioconfig__pb2.DESCRIPTOR,])
|
||||
dependencies=[channel__pb2.DESCRIPTOR,radioconfig__pb2.DESCRIPTOR,mesh__pb2.DESCRIPTOR,])
|
||||
|
||||
|
||||
|
||||
@@ -85,28 +85,42 @@ _ADMINMESSAGE = _descriptor.Descriptor(
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='confirm_set_channel', full_name='AdminMessage.confirm_set_channel', index=7,
|
||||
name='get_owner_request', full_name='AdminMessage.get_owner_request', index=7,
|
||||
number=8, type=8, cpp_type=7, label=1,
|
||||
has_default_value=False, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='get_owner_response', full_name='AdminMessage.get_owner_response', index=8,
|
||||
number=9, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='confirm_set_channel', full_name='AdminMessage.confirm_set_channel', index=9,
|
||||
number=32, type=8, cpp_type=7, label=1,
|
||||
has_default_value=False, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='confirm_set_radio', full_name='AdminMessage.confirm_set_radio', index=8,
|
||||
name='confirm_set_radio', full_name='AdminMessage.confirm_set_radio', index=10,
|
||||
number=33, type=8, cpp_type=7, label=1,
|
||||
has_default_value=False, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exit_simulator', full_name='AdminMessage.exit_simulator', index=9,
|
||||
name='exit_simulator', full_name='AdminMessage.exit_simulator', index=11,
|
||||
number=34, type=8, cpp_type=7, label=1,
|
||||
has_default_value=False, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='reboot_seconds', full_name='AdminMessage.reboot_seconds', index=10,
|
||||
name='reboot_seconds', full_name='AdminMessage.reboot_seconds', index=12,
|
||||
number=35, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
@@ -128,7 +142,7 @@ _ADMINMESSAGE = _descriptor.Descriptor(
|
||||
index=0, containing_type=None, fields=[]),
|
||||
],
|
||||
serialized_start=62,
|
||||
serialized_end=441,
|
||||
serialized_end=507,
|
||||
)
|
||||
|
||||
_ADMINMESSAGE.fields_by_name['set_radio'].message_type = radioconfig__pb2._RADIOCONFIG
|
||||
@@ -136,6 +150,7 @@ _ADMINMESSAGE.fields_by_name['set_owner'].message_type = mesh__pb2._USER
|
||||
_ADMINMESSAGE.fields_by_name['set_channel'].message_type = channel__pb2._CHANNEL
|
||||
_ADMINMESSAGE.fields_by_name['get_radio_response'].message_type = radioconfig__pb2._RADIOCONFIG
|
||||
_ADMINMESSAGE.fields_by_name['get_channel_response'].message_type = channel__pb2._CHANNEL
|
||||
_ADMINMESSAGE.fields_by_name['get_owner_response'].message_type = mesh__pb2._USER
|
||||
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
|
||||
_ADMINMESSAGE.fields_by_name['set_radio'])
|
||||
_ADMINMESSAGE.fields_by_name['set_radio'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
|
||||
@@ -157,6 +172,12 @@ _ADMINMESSAGE.fields_by_name['get_channel_request'].containing_oneof = _ADMINMES
|
||||
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
|
||||
_ADMINMESSAGE.fields_by_name['get_channel_response'])
|
||||
_ADMINMESSAGE.fields_by_name['get_channel_response'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
|
||||
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
|
||||
_ADMINMESSAGE.fields_by_name['get_owner_request'])
|
||||
_ADMINMESSAGE.fields_by_name['get_owner_request'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
|
||||
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
|
||||
_ADMINMESSAGE.fields_by_name['get_owner_response'])
|
||||
_ADMINMESSAGE.fields_by_name['get_owner_response'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
|
||||
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
|
||||
_ADMINMESSAGE.fields_by_name['confirm_set_channel'])
|
||||
_ADMINMESSAGE.fields_by_name['confirm_set_channel'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import pygatt
|
||||
|
||||
|
||||
from .mesh_interface import MeshInterface
|
||||
from meshtastic.mesh_interface import MeshInterface
|
||||
|
||||
# Our standard BLE characteristics
|
||||
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
|
||||
|
||||
@@ -29,6 +29,8 @@ class Globals:
|
||||
self.parser = None
|
||||
self.target_node = None
|
||||
self.channel_index = None
|
||||
self.logfile = None
|
||||
self.tunnelInstance = None
|
||||
|
||||
def reset(self):
|
||||
"""Reset all of our globals. If you add a member, add it to this method, too."""
|
||||
@@ -36,7 +38,10 @@ class Globals:
|
||||
self.parser = None
|
||||
self.target_node = None
|
||||
self.channel_index = None
|
||||
self.logfile = None
|
||||
self.tunnelInstance = None
|
||||
|
||||
# setters
|
||||
def set_args(self, args):
|
||||
"""Set the args"""
|
||||
self.args = args
|
||||
@@ -53,6 +58,15 @@ class Globals:
|
||||
"""Set the channel_index"""
|
||||
self.channel_index = channel_index
|
||||
|
||||
def set_logfile(self, logfile):
|
||||
"""Set the logfile"""
|
||||
self.logfile = logfile
|
||||
|
||||
def set_tunnelInstance(self, tunnelInstance):
|
||||
"""Set the tunnelInstance"""
|
||||
self.tunnelInstance = tunnelInstance
|
||||
|
||||
# getters
|
||||
def get_args(self):
|
||||
"""Get args"""
|
||||
return self.args
|
||||
@@ -68,3 +82,11 @@ class Globals:
|
||||
def get_channel_index(self):
|
||||
"""Get channel_index"""
|
||||
return self.channel_index
|
||||
|
||||
def get_logfile(self):
|
||||
"""Get logfile"""
|
||||
return self.logfile
|
||||
|
||||
def get_tunnelInstance(self):
|
||||
"""Get tunnelInstance"""
|
||||
return self.tunnelInstance
|
||||
|
||||
@@ -17,9 +17,9 @@ from google.protobuf.json_format import MessageToJson
|
||||
|
||||
|
||||
import meshtastic.node
|
||||
from . import portnums_pb2, mesh_pb2
|
||||
from .util import stripnl, Timeout, our_exit
|
||||
from .__init__ import LOCAL_ADDR, BROADCAST_NUM, BROADCAST_ADDR, ResponseHandler, publishingThread, OUR_APP_VERSION, protocols
|
||||
from meshtastic import portnums_pb2, mesh_pb2
|
||||
from meshtastic.util import stripnl, Timeout, our_exit, remove_keys_from_dict, convert_mac_addr
|
||||
from meshtastic.__init__ import LOCAL_ADDR, BROADCAST_NUM, BROADCAST_ADDR, ResponseHandler, publishingThread, OUR_APP_VERSION, protocols
|
||||
|
||||
class MeshInterface:
|
||||
"""Interface class for meshtastic devices
|
||||
@@ -68,8 +68,7 @@ class MeshInterface:
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if exc_type is not None and exc_value is not None:
|
||||
logging.error(
|
||||
f'An exception of type {exc_type} with value {exc_value} has occurred')
|
||||
logging.error(f'An exception of type {exc_type} with value {exc_value} has occurred')
|
||||
if traceback is not None:
|
||||
logging.error(f'Traceback: {traceback}')
|
||||
self.close()
|
||||
@@ -84,7 +83,19 @@ class MeshInterface:
|
||||
nodes = ""
|
||||
if self.nodes:
|
||||
for n in self.nodes.values():
|
||||
nodes = nodes + f" {stripnl(n)}"
|
||||
# when the TBeam is first booted, it sometimes shows the raw data
|
||||
# so, we will just remove any raw keys
|
||||
keys_to_remove = ('raw', 'decoded', 'payload')
|
||||
n2 = remove_keys_from_dict(keys_to_remove, n)
|
||||
|
||||
# if we have 'macaddr', re-format it
|
||||
if 'macaddr' in n2['user']:
|
||||
val = n2['user']['macaddr']
|
||||
# decode the base64 value
|
||||
addr = convert_mac_addr(val)
|
||||
n2['user']['macaddr'] = addr
|
||||
|
||||
nodes = nodes + f" {stripnl(n2)}"
|
||||
infos = owner + myinfo + mesh + nodes
|
||||
print(infos)
|
||||
return infos
|
||||
@@ -382,11 +393,11 @@ class MeshInterface:
|
||||
return user.get('shortName', None)
|
||||
return None
|
||||
|
||||
def _waitConnected(self):
|
||||
def _waitConnected(self, timeout=15.0):
|
||||
"""Block until the initial node db download is complete, or timeout
|
||||
and raise an exception"""
|
||||
if not self.noProto:
|
||||
if not self.isConnected.wait(15.0): # timeout after x seconds
|
||||
if not self.isConnected.wait(timeout): # timeout after x seconds
|
||||
raise Exception("Timed out waiting for connection completion")
|
||||
|
||||
# If we failed while connecting, raise the connection to the client
|
||||
@@ -404,8 +415,7 @@ class MeshInterface:
|
||||
def _disconnected(self):
|
||||
"""Called by subclasses to tell clients this interface has disconnected"""
|
||||
self.isConnected.clear()
|
||||
publishingThread.queueWork(lambda: pub.sendMessage(
|
||||
"meshtastic.connection.lost", interface=self))
|
||||
publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.connection.lost", interface=self))
|
||||
|
||||
def _startHeartbeat(self):
|
||||
"""We need to send a heartbeat message to the device every X seconds"""
|
||||
@@ -431,8 +441,7 @@ class MeshInterface:
|
||||
if not self.isConnected.is_set():
|
||||
self.isConnected.set()
|
||||
self._startHeartbeat()
|
||||
publishingThread.queueWork(lambda: pub.sendMessage(
|
||||
"meshtastic.connection.established", interface=self))
|
||||
publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.connection.established", interface=self))
|
||||
|
||||
def _startConfig(self):
|
||||
"""Start device packets flowing"""
|
||||
@@ -504,7 +513,8 @@ class MeshInterface:
|
||||
elif fromRadio.HasField("node_info"):
|
||||
node = asDict["nodeInfo"]
|
||||
try:
|
||||
self._fixupPosition(node["position"])
|
||||
newpos = self._fixupPosition(node["position"])
|
||||
node["position"] = newpos
|
||||
except:
|
||||
logging.debug("Node without position")
|
||||
|
||||
@@ -536,12 +546,14 @@ class MeshInterface:
|
||||
"""Convert integer lat/lon into floats
|
||||
|
||||
Arguments:
|
||||
position {Position dictionary} -- object ot fix up
|
||||
position {Position dictionary} -- object to fix up
|
||||
Returns the position with the updated keys
|
||||
"""
|
||||
if "latitudeI" in position:
|
||||
position["latitude"] = position["latitudeI"] * 1e-7
|
||||
if "longitudeI" in position:
|
||||
position["longitude"] = position["longitudeI"] * 1e-7
|
||||
return position
|
||||
|
||||
def _nodeNumToId(self, num):
|
||||
"""Map a node node number to a node ID
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
||||
import logging
|
||||
import base64
|
||||
from google.protobuf.json_format import MessageToJson
|
||||
from . import portnums_pb2, apponly_pb2, admin_pb2, channel_pb2
|
||||
from .util import pskToString, stripnl, Timeout, our_exit, fromPSK
|
||||
from meshtastic import portnums_pb2, apponly_pb2, admin_pb2, channel_pb2
|
||||
from meshtastic.util import pskToString, stripnl, Timeout, our_exit, fromPSK
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,8 +2,8 @@
|
||||
"""
|
||||
import logging
|
||||
from pubsub import pub
|
||||
from . import portnums_pb2, remote_hardware_pb2
|
||||
from .util import our_exit
|
||||
from meshtastic import portnums_pb2, remote_hardware_pb2
|
||||
from meshtastic.util import our_exit
|
||||
|
||||
|
||||
def onGPIOreceive(packet, interface):
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
""" Serial interface class
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
import platform
|
||||
import os
|
||||
import stat
|
||||
import serial
|
||||
|
||||
import meshtastic.util
|
||||
from .stream_interface import StreamInterface
|
||||
from meshtastic.stream_interface import StreamInterface
|
||||
|
||||
if platform.system() != 'Windows':
|
||||
import termios
|
||||
|
||||
class SerialInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a serial link"""
|
||||
@@ -20,6 +22,7 @@ class SerialInterface(StreamInterface):
|
||||
devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
|
||||
debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})
|
||||
"""
|
||||
self.noProto = noProto
|
||||
|
||||
if devPath is None:
|
||||
ports = meshtastic.util.findPorts()
|
||||
@@ -35,43 +38,30 @@ class SerialInterface(StreamInterface):
|
||||
|
||||
logging.debug(f"Connecting to {devPath}")
|
||||
|
||||
# Note: we provide None for port here, because we will be opening it later
|
||||
self.stream = serial.Serial(
|
||||
None, 921600, exclusive=True, timeout=0.5, write_timeout=0)
|
||||
# first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR
|
||||
# see https://github.com/pyserial/pyserial/issues/124
|
||||
if not self.noProto:
|
||||
if platform.system() != 'Windows':
|
||||
with open(devPath, encoding='utf8') as f:
|
||||
attrs = termios.tcgetattr(f)
|
||||
attrs[2] = attrs[2] & ~termios.HUPCL
|
||||
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
|
||||
f.close()
|
||||
time.sleep(0.1)
|
||||
|
||||
# rts=False Needed to prevent TBEAMs resetting on OSX, because rts is connected to reset
|
||||
self.stream.port = devPath
|
||||
self.stream = serial.Serial(devPath, 921600, exclusive=True, timeout=0.5, write_timeout=0)
|
||||
if not self.noProto:
|
||||
self.stream.flush()
|
||||
time.sleep(0.1)
|
||||
|
||||
# HACK: If the platform driving the serial port is unable to leave the RTS pin in high-impedance
|
||||
# mode, set RTS to false so that the device platform won't be reset spuriously.
|
||||
# Linux does this properly, so don't apply this hack on Linux (because it makes the reset button not work).
|
||||
if self._hostPlatformAlwaysDrivesUartRts():
|
||||
self.stream.rts = False
|
||||
self.stream.open()
|
||||
StreamInterface.__init__(self, debugOut=debugOut, noProto=noProto, connectNow=connectNow)
|
||||
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow)
|
||||
|
||||
"""true if platform driving the serial port is Windows Subsystem for Linux 1."""
|
||||
def _isWsl1(self):
|
||||
# WSL1 identifies itself as Linux, but has a special char device at /dev/lxss for use with session control,
|
||||
# e.g. /init. We should treat WSL1 as Windows for the RTS-driving hack because the underlying platfrom
|
||||
# serial driver for the CP21xx still exhibits the buggy behavior.
|
||||
# WSL2 is not covered here, as it does not (as of 2021-May-25) support the appropriate functionality to
|
||||
# share or pass-through serial ports.
|
||||
try:
|
||||
# Claims to be Linux, but has /dev/lxss; must be WSL 1
|
||||
return platform.system() == 'Linux' and stat.S_ISCHR(os.stat('/dev/lxss').st_mode)
|
||||
except:
|
||||
# Couldn't stat /dev/lxss special device; not WSL1
|
||||
return False
|
||||
|
||||
def _hostPlatformAlwaysDrivesUartRts(self):
|
||||
# OS-X/Windows seems to have a bug in its CP21xx serial drivers. It ignores that we asked for no RTSCTS
|
||||
# control and will always drive RTS either high or low (rather than letting the CP102 leave
|
||||
# it as an open-collector floating pin).
|
||||
# TODO: When WSL2 supports USB passthrough, this will get messier. If/when WSL2 gets virtual serial
|
||||
# ports that "share" the Windows serial port (and thus the Windows drivers), this code will need to be
|
||||
# updated to reflect that as well -- or if T-Beams get made with an alternate USB to UART bridge that has
|
||||
# a less buggy driver.
|
||||
return platform.system() != 'Linux' or self._isWsl1()
|
||||
def close(self):
|
||||
"""Close a connection to the device"""
|
||||
if not self.noProto:
|
||||
self.stream.flush()
|
||||
time.sleep(0.1)
|
||||
self.stream.flush()
|
||||
time.sleep(0.1)
|
||||
logging.debug("Closing Serial stream")
|
||||
StreamInterface.close(self)
|
||||
|
||||
@@ -18,7 +18,7 @@ DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
package='',
|
||||
syntax='proto3',
|
||||
serialized_options=b'\n\023com.geeksville.meshB\025StoreAndForwardProtosH\003Z!github.com/meshtastic/gomeshproto',
|
||||
serialized_pb=b'\n\x12storeforward.proto\"\xe7\x04\n\x0fStoreAndForward\x12,\n\x02rr\x18\x01 \x01(\x0e\x32 .StoreAndForward.RequestResponse\x12*\n\x05stats\x18\x02 \x01(\x0b\x32\x1b.StoreAndForward.Statistics\x12)\n\x07history\x18\x03 \x01(\x0b\x32\x18.StoreAndForward.History\x1a\xc6\x01\n\nStatistics\x12\x15\n\rMessagesTotal\x18\x01 \x01(\r\x12\x15\n\rMessagesSaved\x18\x02 \x01(\r\x12\x13\n\x0bMessagesMax\x18\x03 \x01(\r\x12\x0e\n\x06UpTime\x18\x04 \x01(\r\x12\x10\n\x08Requests\x18\x05 \x01(\r\x12\x17\n\x0fRequestsHistory\x18\x06 \x01(\r\x12\x11\n\tHeartbeat\x18\x07 \x01(\x08\x12\x11\n\tReturnMax\x18\x08 \x01(\r\x12\x14\n\x0cReturnWindow\x18\t \x01(\r\x1a\x32\n\x07History\x12\x17\n\x0fHistoryMessages\x18\x01 \x01(\r\x12\x0e\n\x06Window\x18\x02 \x01(\r\"\xd1\x01\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x10\n\x0c\x43LIENT_ERROR\x10\x65\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x66\x12\x10\n\x0c\x43LIENT_STATS\x10g\x12\x0f\n\x0b\x43LIENT_PING\x10h\x12\x0f\n\x0b\x43LIENT_PONG\x10iBQ\n\x13\x63om.geeksville.meshB\x15StoreAndForwardProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3'
|
||||
serialized_pb=b'\n\x12storeforward.proto\"\x8a\x06\n\x0fStoreAndForward\x12,\n\x02rr\x18\x01 \x01(\x0e\x32 .StoreAndForward.RequestResponse\x12*\n\x05stats\x18\x02 \x01(\x0b\x32\x1b.StoreAndForward.Statistics\x12)\n\x07history\x18\x03 \x01(\x0b\x32\x18.StoreAndForward.History\x12-\n\theartbeat\x18\x04 \x01(\x0b\x32\x1a.StoreAndForward.Heartbeat\x1a\xcd\x01\n\nStatistics\x12\x16\n\x0emessages_total\x18\x01 \x01(\r\x12\x16\n\x0emessages_saved\x18\x02 \x01(\r\x12\x14\n\x0cmessages_max\x18\x03 \x01(\r\x12\x0f\n\x07up_time\x18\x04 \x01(\r\x12\x10\n\x08requests\x18\x05 \x01(\r\x12\x18\n\x10requests_history\x18\x06 \x01(\r\x12\x11\n\theartbeat\x18\x07 \x01(\x08\x12\x12\n\nreturn_max\x18\x08 \x01(\r\x12\x15\n\rreturn_window\x18\t \x01(\r\x1aI\n\x07History\x12\x18\n\x10history_messages\x18\x01 \x01(\r\x12\x0e\n\x06window\x18\x02 \x01(\r\x12\x14\n\x0clast_request\x18\x03 \x01(\r\x1a.\n\tHeartbeat\x12\x0e\n\x06period\x18\x01 \x01(\r\x12\x11\n\tsecondary\x18\x02 \x01(\r\"\xf7\x01\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x12\n\x0eROUTER_HISTORY\x10\x06\x12\x10\n\x0c\x43LIENT_ERROR\x10\x65\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x66\x12\x10\n\x0c\x43LIENT_STATS\x10g\x12\x0f\n\x0b\x43LIENT_PING\x10h\x12\x0f\n\x0b\x43LIENT_PONG\x10i\x12\x10\n\x0c\x43LIENT_ABORT\x10jBQ\n\x13\x63om.geeksville.meshB\x15StoreAndForwardProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3'
|
||||
)
|
||||
|
||||
|
||||
@@ -54,30 +54,38 @@ _STOREANDFORWARD_REQUESTRESPONSE = _descriptor.EnumDescriptor(
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CLIENT_ERROR', index=6, number=101,
|
||||
name='ROUTER_HISTORY', index=6, number=6,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CLIENT_HISTORY', index=7, number=102,
|
||||
name='CLIENT_ERROR', index=7, number=101,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CLIENT_STATS', index=8, number=103,
|
||||
name='CLIENT_HISTORY', index=8, number=102,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CLIENT_PING', index=9, number=104,
|
||||
name='CLIENT_STATS', index=9, number=103,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CLIENT_PONG', index=10, number=105,
|
||||
name='CLIENT_PING', index=10, number=104,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CLIENT_PONG', index=11, number=105,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CLIENT_ABORT', index=12, number=106,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=429,
|
||||
serialized_end=638,
|
||||
serialized_start=554,
|
||||
serialized_end=801,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_STOREANDFORWARD_REQUESTRESPONSE)
|
||||
|
||||
@@ -90,63 +98,63 @@ _STOREANDFORWARD_STATISTICS = _descriptor.Descriptor(
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='MessagesTotal', full_name='StoreAndForward.Statistics.MessagesTotal', index=0,
|
||||
name='messages_total', full_name='StoreAndForward.Statistics.messages_total', index=0,
|
||||
number=1, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='MessagesSaved', full_name='StoreAndForward.Statistics.MessagesSaved', index=1,
|
||||
name='messages_saved', full_name='StoreAndForward.Statistics.messages_saved', index=1,
|
||||
number=2, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='MessagesMax', full_name='StoreAndForward.Statistics.MessagesMax', index=2,
|
||||
name='messages_max', full_name='StoreAndForward.Statistics.messages_max', index=2,
|
||||
number=3, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='UpTime', full_name='StoreAndForward.Statistics.UpTime', index=3,
|
||||
name='up_time', full_name='StoreAndForward.Statistics.up_time', index=3,
|
||||
number=4, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='Requests', full_name='StoreAndForward.Statistics.Requests', index=4,
|
||||
name='requests', full_name='StoreAndForward.Statistics.requests', index=4,
|
||||
number=5, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='RequestsHistory', full_name='StoreAndForward.Statistics.RequestsHistory', index=5,
|
||||
name='requests_history', full_name='StoreAndForward.Statistics.requests_history', index=5,
|
||||
number=6, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='Heartbeat', full_name='StoreAndForward.Statistics.Heartbeat', index=6,
|
||||
name='heartbeat', full_name='StoreAndForward.Statistics.heartbeat', index=6,
|
||||
number=7, type=8, cpp_type=7, label=1,
|
||||
has_default_value=False, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='ReturnMax', full_name='StoreAndForward.Statistics.ReturnMax', index=7,
|
||||
name='return_max', full_name='StoreAndForward.Statistics.return_max', index=7,
|
||||
number=8, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='ReturnWindow', full_name='StoreAndForward.Statistics.ReturnWindow', index=8,
|
||||
name='return_window', full_name='StoreAndForward.Statistics.return_window', index=8,
|
||||
number=9, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
@@ -164,8 +172,8 @@ _STOREANDFORWARD_STATISTICS = _descriptor.Descriptor(
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=176,
|
||||
serialized_end=374,
|
||||
serialized_start=223,
|
||||
serialized_end=428,
|
||||
)
|
||||
|
||||
_STOREANDFORWARD_HISTORY = _descriptor.Descriptor(
|
||||
@@ -176,14 +184,58 @@ _STOREANDFORWARD_HISTORY = _descriptor.Descriptor(
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='HistoryMessages', full_name='StoreAndForward.History.HistoryMessages', index=0,
|
||||
name='history_messages', full_name='StoreAndForward.History.history_messages', index=0,
|
||||
number=1, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='Window', full_name='StoreAndForward.History.Window', index=1,
|
||||
name='window', full_name='StoreAndForward.History.window', index=1,
|
||||
number=2, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='last_request', full_name='StoreAndForward.History.last_request', index=2,
|
||||
number=3, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=430,
|
||||
serialized_end=503,
|
||||
)
|
||||
|
||||
_STOREANDFORWARD_HEARTBEAT = _descriptor.Descriptor(
|
||||
name='Heartbeat',
|
||||
full_name='StoreAndForward.Heartbeat',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='period', full_name='StoreAndForward.Heartbeat.period', index=0,
|
||||
number=1, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='secondary', full_name='StoreAndForward.Heartbeat.secondary', index=1,
|
||||
number=2, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
@@ -201,8 +253,8 @@ _STOREANDFORWARD_HISTORY = _descriptor.Descriptor(
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=376,
|
||||
serialized_end=426,
|
||||
serialized_start=505,
|
||||
serialized_end=551,
|
||||
)
|
||||
|
||||
_STOREANDFORWARD = _descriptor.Descriptor(
|
||||
@@ -233,10 +285,17 @@ _STOREANDFORWARD = _descriptor.Descriptor(
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='heartbeat', full_name='StoreAndForward.heartbeat', index=3,
|
||||
number=4, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[_STOREANDFORWARD_STATISTICS, _STOREANDFORWARD_HISTORY, ],
|
||||
nested_types=[_STOREANDFORWARD_STATISTICS, _STOREANDFORWARD_HISTORY, _STOREANDFORWARD_HEARTBEAT, ],
|
||||
enum_types=[
|
||||
_STOREANDFORWARD_REQUESTRESPONSE,
|
||||
],
|
||||
@@ -247,14 +306,16 @@ _STOREANDFORWARD = _descriptor.Descriptor(
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=23,
|
||||
serialized_end=638,
|
||||
serialized_end=801,
|
||||
)
|
||||
|
||||
_STOREANDFORWARD_STATISTICS.containing_type = _STOREANDFORWARD
|
||||
_STOREANDFORWARD_HISTORY.containing_type = _STOREANDFORWARD
|
||||
_STOREANDFORWARD_HEARTBEAT.containing_type = _STOREANDFORWARD
|
||||
_STOREANDFORWARD.fields_by_name['rr'].enum_type = _STOREANDFORWARD_REQUESTRESPONSE
|
||||
_STOREANDFORWARD.fields_by_name['stats'].message_type = _STOREANDFORWARD_STATISTICS
|
||||
_STOREANDFORWARD.fields_by_name['history'].message_type = _STOREANDFORWARD_HISTORY
|
||||
_STOREANDFORWARD.fields_by_name['heartbeat'].message_type = _STOREANDFORWARD_HEARTBEAT
|
||||
_STOREANDFORWARD_REQUESTRESPONSE.containing_type = _STOREANDFORWARD
|
||||
DESCRIPTOR.message_types_by_name['StoreAndForward'] = _STOREANDFORWARD
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
@@ -274,6 +335,13 @@ StoreAndForward = _reflection.GeneratedProtocolMessageType('StoreAndForward', (_
|
||||
# @@protoc_insertion_point(class_scope:StoreAndForward.History)
|
||||
})
|
||||
,
|
||||
|
||||
'Heartbeat' : _reflection.GeneratedProtocolMessageType('Heartbeat', (_message.Message,), {
|
||||
'DESCRIPTOR' : _STOREANDFORWARD_HEARTBEAT,
|
||||
'__module__' : 'storeforward_pb2'
|
||||
# @@protoc_insertion_point(class_scope:StoreAndForward.Heartbeat)
|
||||
})
|
||||
,
|
||||
'DESCRIPTOR' : _STOREANDFORWARD,
|
||||
'__module__' : 'storeforward_pb2'
|
||||
# @@protoc_insertion_point(class_scope:StoreAndForward)
|
||||
@@ -281,6 +349,7 @@ StoreAndForward = _reflection.GeneratedProtocolMessageType('StoreAndForward', (_
|
||||
_sym_db.RegisterMessage(StoreAndForward)
|
||||
_sym_db.RegisterMessage(StoreAndForward.Statistics)
|
||||
_sym_db.RegisterMessage(StoreAndForward.History)
|
||||
_sym_db.RegisterMessage(StoreAndForward.Heartbeat)
|
||||
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
|
||||
@@ -7,8 +7,8 @@ import traceback
|
||||
import serial
|
||||
|
||||
|
||||
from .mesh_interface import MeshInterface
|
||||
from .util import stripnl
|
||||
from meshtastic.mesh_interface import MeshInterface
|
||||
from meshtastic.util import stripnl
|
||||
|
||||
|
||||
START1 = 0x94
|
||||
@@ -62,7 +62,8 @@ class StreamInterface(MeshInterface):
|
||||
# because we want to ensure it is looking for START1)
|
||||
p = bytearray([START2] * 32)
|
||||
self._writeBytes(p)
|
||||
time.sleep(0.1) # wait 100ms to give device time to start running
|
||||
if not self.noProto:
|
||||
time.sleep(0.1) # wait 100ms to give device time to start running
|
||||
|
||||
self._rxThread.start()
|
||||
|
||||
@@ -88,6 +89,9 @@ class StreamInterface(MeshInterface):
|
||||
if self.stream: # ignore writes when stream is closed
|
||||
self.stream.write(b)
|
||||
self.stream.flush()
|
||||
# we sleep here to give the TBeam a chance to work
|
||||
if not self.noProto:
|
||||
time.sleep(0.1)
|
||||
|
||||
def _readBytes(self, length):
|
||||
"""Read an array of bytes from our stream"""
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import socket
|
||||
from typing import AnyStr
|
||||
|
||||
from .stream_interface import StreamInterface
|
||||
from meshtastic.stream_interface import StreamInterface
|
||||
|
||||
class TCPInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a TCP link"""
|
||||
@@ -17,8 +17,6 @@ class TCPInterface(StreamInterface):
|
||||
hostname {string} -- Hostname/IP address of the device to connect to
|
||||
"""
|
||||
|
||||
# Instead of wrapping as a stream, we use the native socket API
|
||||
# self.stream = sock.makefile('rw')
|
||||
self.stream = None
|
||||
|
||||
self.hostname = hostname
|
||||
|
||||
@@ -8,9 +8,9 @@ import traceback
|
||||
from dotmap import DotMap
|
||||
from pubsub import pub
|
||||
import meshtastic.util
|
||||
from .__init__ import BROADCAST_NUM
|
||||
from .serial_interface import SerialInterface
|
||||
from .tcp_interface import TCPInterface
|
||||
from meshtastic.__init__ import BROADCAST_NUM
|
||||
from meshtastic.serial_interface import SerialInterface
|
||||
from meshtastic.tcp_interface import TCPInterface
|
||||
|
||||
|
||||
"""The interfaces we are using for our tests"""
|
||||
|
||||
@@ -6,13 +6,21 @@ import pytest
|
||||
|
||||
|
||||
@pytest.mark.int
|
||||
def test_int_no_args():
|
||||
"""Test without any args"""
|
||||
def test_int_meshtastic_no_args():
|
||||
"""Test meshtastic without any args"""
|
||||
return_value, out = subprocess.getstatusoutput('meshtastic')
|
||||
assert re.match(r'usage: meshtastic', out)
|
||||
assert return_value == 1
|
||||
|
||||
|
||||
@pytest.mark.int
|
||||
def test_int_mesh_tunnel_no_args():
|
||||
"""Test mesh-tunnel without any args"""
|
||||
return_value, out = subprocess.getstatusoutput('mesh-tunnel')
|
||||
assert re.match(r'usage: mesh-tunnel', out)
|
||||
assert return_value == 1
|
||||
|
||||
|
||||
@pytest.mark.int
|
||||
def test_int_version():
|
||||
"""Test '--version'."""
|
||||
|
||||
@@ -5,16 +5,17 @@ import sys
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import platform
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
|
||||
from meshtastic.__main__ import initParser, main, Globals, onReceive, onConnection, export_config, getPref, setPref
|
||||
from meshtastic.__main__ import initParser, main, Globals, onReceive, onConnection, export_config, getPref, setPref, onNode, tunnelMain
|
||||
#from ..radioconfig_pb2 import UserPreferences
|
||||
import meshtastic.radioconfig_pb2
|
||||
from ..serial_interface import SerialInterface
|
||||
from ..tcp_interface import TCPInterface
|
||||
from ..ble_interface import BLEInterface
|
||||
#from ..ble_interface import BLEInterface
|
||||
from ..node import Node
|
||||
from ..channel_pb2 import Channel
|
||||
from ..remote_hardware import onGPIOreceive
|
||||
@@ -62,7 +63,7 @@ def test_main_main_version(capsys, reset_globals):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_main_no_args(reset_globals):
|
||||
def test_main_main_no_args(reset_globals, capsys):
|
||||
"""Test with no args"""
|
||||
sys.argv = ['']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
@@ -71,6 +72,8 @@ def test_main_main_no_args(reset_globals):
|
||||
main()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
_, err = capsys.readouterr()
|
||||
assert re.search(r'usage:', err, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -111,7 +114,7 @@ def test_main_ch_index_no_devices(patched_find_ports, capsys, reset_globals):
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('meshtastic.util.findPorts', return_value=[])
|
||||
def test_main_test_no_ports(patched_find_ports, reset_globals):
|
||||
def test_main_test_no_ports(patched_find_ports, reset_globals, capsys):
|
||||
"""Test --test with no hardware"""
|
||||
sys.argv = ['', '--test']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
@@ -122,11 +125,14 @@ def test_main_test_no_ports(patched_find_ports, reset_globals):
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
patched_find_ports.assert_called()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Warning: Must have at least two devices connected to USB', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1'])
|
||||
def test_main_test_one_port(patched_find_ports, reset_globals):
|
||||
def test_main_test_one_port(patched_find_ports, reset_globals, capsys):
|
||||
"""Test --test with one fake port"""
|
||||
sys.argv = ['', '--test']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
@@ -137,12 +143,14 @@ def test_main_test_one_port(patched_find_ports, reset_globals):
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
patched_find_ports.assert_called()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Warning: Must have at least two devices connected to USB', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('meshtastic.test.testAll', return_value=True)
|
||||
@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1', '/dev/ttyFake2'])
|
||||
def test_main_test_two_ports_success(patched_find_ports, patched_test_all, reset_globals):
|
||||
def test_main_test_two_ports_success(patched_test_all, reset_globals, capsys):
|
||||
"""Test --test two fake ports and testAll() is a simulated success"""
|
||||
sys.argv = ['', '--test']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
@@ -151,14 +159,15 @@ def test_main_test_two_ports_success(patched_find_ports, patched_test_all, reset
|
||||
main()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 0
|
||||
# TODO: why does this fail? patched_find_ports.assert_called()
|
||||
patched_test_all.assert_called()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Test was a success.', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('meshtastic.test.testAll', return_value=False)
|
||||
@patch('meshtastic.util.findPorts', return_value=['/dev/ttyFake1', '/dev/ttyFake2'])
|
||||
def test_main_test_two_ports_fails(patched_find_ports, patched_test_all, reset_globals):
|
||||
def test_main_test_two_ports_fails(patched_test_all, reset_globals, capsys):
|
||||
"""Test --test two fake ports and testAll() is a simulated failure"""
|
||||
sys.argv = ['', '--test']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
@@ -167,8 +176,10 @@ def test_main_test_two_ports_fails(patched_find_ports, patched_test_all, reset_g
|
||||
main()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
# TODO: why does this fail? patched_find_ports.assert_called()
|
||||
patched_test_all.assert_called()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Test was not successful.', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -209,23 +220,24 @@ def test_main_info_with_tcp_interface(capsys, reset_globals):
|
||||
mo.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_info_with_ble_interface(capsys, reset_globals):
|
||||
"""Test --info"""
|
||||
sys.argv = ['', '--info', '--ble', 'foo']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
|
||||
iface = MagicMock(autospec=BLEInterface)
|
||||
def mock_showInfo():
|
||||
print('inside mocked showInfo')
|
||||
iface.showInfo.side_effect = mock_showInfo
|
||||
with patch('meshtastic.ble_interface.BLEInterface', return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
mo.assert_called()
|
||||
# TODO: comment out ble (for now)
|
||||
#@pytest.mark.unit
|
||||
#def test_main_info_with_ble_interface(capsys, reset_globals):
|
||||
# """Test --info"""
|
||||
# sys.argv = ['', '--info', '--ble', 'foo']
|
||||
# Globals.getInstance().set_args(sys.argv)
|
||||
#
|
||||
# iface = MagicMock(autospec=BLEInterface)
|
||||
# def mock_showInfo():
|
||||
# print('inside mocked showInfo')
|
||||
# iface.showInfo.side_effect = mock_showInfo
|
||||
# with patch('meshtastic.ble_interface.BLEInterface', return_value=iface) as mo:
|
||||
# main()
|
||||
# out, err = capsys.readouterr()
|
||||
# assert re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
# assert re.search(r'inside mocked showInfo', out, re.MULTILINE)
|
||||
# assert err == ''
|
||||
# mo.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -1232,18 +1244,23 @@ def test_main_setchan(capsys, reset_globals):
|
||||
main()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
_, err = capsys.readouterr()
|
||||
assert re.search(r'usage:', err, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_onReceive_empty(caplog, reset_globals):
|
||||
def test_main_onReceive_empty(caplog, reset_globals, capsys):
|
||||
"""Test onReceive"""
|
||||
sys.argv = ['']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
args = MagicMock()
|
||||
Globals.getInstance().set_args(args)
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
packet = {'decoded': 'foo'}
|
||||
packet = {}
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
onReceive(packet, iface)
|
||||
assert re.search(r'in onReceive', caplog.text, re.MULTILINE)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Warning: There is no field 'to' in the packet.", out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
# TODO: use this captured position app message (might want/need in the future)
|
||||
@@ -1258,7 +1275,7 @@ def test_main_onReceive_empty(caplog, reset_globals):
|
||||
# }
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_onReceive_with_sendtext(caplog, reset_globals):
|
||||
def test_main_onReceive_with_sendtext(caplog, capsys, reset_globals):
|
||||
"""Test onReceive with sendtext
|
||||
The entire point of this test is to make sure the interface.close() call
|
||||
is made in onReceive().
|
||||
@@ -1287,41 +1304,31 @@ def test_main_onReceive_with_sendtext(caplog, reset_globals):
|
||||
onReceive(packet, iface)
|
||||
assert re.search(r'in onReceive', caplog.text, re.MULTILINE)
|
||||
mo.assert_called()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Sending text message hello to', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_onReceive_with_reply(caplog, capsys, reset_globals):
|
||||
"""Test onReceive with a reply
|
||||
To capture: on one device run '--sendtext aaa --reply' and on another
|
||||
device run '--sendtext bbb --reply', then back to the first device and
|
||||
run '--sendtext aaa2 --reply'. You should now see a "Sending reply" message.
|
||||
def test_main_onReceive_with_text(caplog, capsys, reset_globals):
|
||||
"""Test onReceive with text
|
||||
"""
|
||||
sys.argv = ['', '--sendtext', 'hello', '--reply']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
args = MagicMock()
|
||||
args.sendtext.return_value = 'foo'
|
||||
Globals.getInstance().set_args(args)
|
||||
|
||||
# Note: 'TEXT_MESSAGE_APP' value is 1
|
||||
|
||||
send_packet = {
|
||||
# Note: Some of this is faked below.
|
||||
packet = {
|
||||
'to': 4294967295,
|
||||
'decoded': {
|
||||
'portnum': 1,
|
||||
'payload': "hello"
|
||||
'payload': "hello",
|
||||
'text': "faked"
|
||||
},
|
||||
'id': 334776977,
|
||||
'hop_limit': 3,
|
||||
'want_ack': True
|
||||
}
|
||||
|
||||
reply_packet = {
|
||||
'from': 682968668,
|
||||
'to': 4294967295,
|
||||
'decoded': {
|
||||
'portnum': 'TEXT_MESSAGE_APP',
|
||||
'payload': b'bbb',
|
||||
'text': 'bbb'
|
||||
},
|
||||
'id': 1709936182,
|
||||
'rxTime': 1640381999,
|
||||
'want_ack': True,
|
||||
'rxSnr': 6.0,
|
||||
'hopLimit': 3,
|
||||
'raw': 'faked',
|
||||
@@ -1332,16 +1339,13 @@ def test_main_onReceive_with_reply(caplog, capsys, reset_globals):
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
iface.myInfo.my_node_num = 4294967295
|
||||
|
||||
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
|
||||
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
main()
|
||||
onReceive(send_packet, iface)
|
||||
onReceive(reply_packet, iface)
|
||||
onReceive(packet, iface)
|
||||
assert re.search(r'in onReceive', caplog.text, re.MULTILINE)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'got msg ', out, re.MULTILINE)
|
||||
assert re.search(r'Sending reply', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
mo.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -1379,6 +1383,10 @@ fixed_position: true
|
||||
position_flags: 35"""
|
||||
export_config(mo)
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
# ensure we do not output this line
|
||||
assert not re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
|
||||
assert re.search(r'owner: foo', out, re.MULTILINE)
|
||||
assert re.search(r'channel_url: bar', out, re.MULTILINE)
|
||||
assert re.search(r'location:', out, re.MULTILINE)
|
||||
@@ -1404,7 +1412,7 @@ def test_main_export_config_called_from_main(capsys, reset_globals):
|
||||
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
assert not re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
assert re.search(r'# start of Meshtastic configure yaml', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
mo.assert_called()
|
||||
@@ -1522,6 +1530,36 @@ def test_main_getPref_valid_field(capsys, reset_globals):
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_getPref_valid_field_string(capsys, reset_globals):
|
||||
"""Test getPref() with a valid field and value as a string"""
|
||||
prefs = MagicMock()
|
||||
prefs.DESCRIPTOR.fields_by_name.get.return_value = 'wifi_ssid'
|
||||
prefs.wifi_ssid = 'foo'
|
||||
prefs.ls_secs = 300
|
||||
prefs.fixed_position = False
|
||||
|
||||
getPref(prefs, 'wifi_ssid')
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'wifi_ssid: foo', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_getPref_valid_field_bool(capsys, reset_globals):
|
||||
"""Test getPref() with a valid field and value as a bool"""
|
||||
prefs = MagicMock()
|
||||
prefs.DESCRIPTOR.fields_by_name.get.return_value = 'fixed_position'
|
||||
prefs.wifi_ssid = 'foo'
|
||||
prefs.ls_secs = 300
|
||||
prefs.fixed_position = False
|
||||
|
||||
getPref(prefs, 'fixed_position')
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'fixed_position: False', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_getPref_invalid_field(capsys, reset_globals):
|
||||
"""Test getPref() with an invalid field"""
|
||||
@@ -1554,7 +1592,7 @@ def test_main_getPref_invalid_field(capsys, reset_globals):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_setPref_valid_field(capsys, reset_globals):
|
||||
def test_main_setPref_valid_field_int(capsys, reset_globals):
|
||||
"""Test setPref() with a valid field"""
|
||||
|
||||
class Field:
|
||||
@@ -1604,3 +1642,152 @@ def test_main_setPref_invalid_field(capsys, reset_globals):
|
||||
# ensure they are sorted
|
||||
assert re.search(r'fixed_position\s+is_router\s+ls_secs', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_ch_set_psk_no_ch_index(capsys, reset_globals):
|
||||
"""Test --ch-set psk """
|
||||
sys.argv = ['', '--ch-set', 'psk', 'foo', '--host', 'meshtastic.local']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
|
||||
iface = MagicMock(autospec=TCPInterface)
|
||||
with patch('meshtastic.tcp_interface.TCPInterface', return_value=iface) as mo:
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
assert re.search(r"Warning: Need to specify '--ch-index'", out, re.MULTILINE)
|
||||
assert err == ''
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
mo.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_ch_set_psk_with_ch_index(capsys, reset_globals):
|
||||
"""Test --ch-set psk """
|
||||
sys.argv = ['', '--ch-set', 'psk', 'foo', '--host', 'meshtastic.local', '--ch-index', '0']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
|
||||
iface = MagicMock(autospec=TCPInterface)
|
||||
with patch('meshtastic.tcp_interface.TCPInterface', return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
assert re.search(r"Writing modified channels to device", out, re.MULTILINE)
|
||||
assert err == ''
|
||||
mo.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_main_ch_set_name_with_ch_index(capsys, reset_globals):
|
||||
"""Test --ch-set setting other than psk"""
|
||||
sys.argv = ['', '--ch-set', 'name', 'foo', '--host', 'meshtastic.local', '--ch-index', '0']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
|
||||
iface = MagicMock(autospec=TCPInterface)
|
||||
with patch('meshtastic.tcp_interface.TCPInterface', return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
assert re.search(r'Set name to foo', out, re.MULTILINE)
|
||||
assert re.search(r"Writing modified channels to device", out, re.MULTILINE)
|
||||
assert err == ''
|
||||
mo.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_onNode(capsys, reset_globals):
|
||||
"""Test onNode"""
|
||||
onNode('foo')
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Node changed', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_tunnel_no_args(capsys, reset_globals):
|
||||
"""Test tunnel no arguments"""
|
||||
sys.argv = ['']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
tunnelMain()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
_, err = capsys.readouterr()
|
||||
assert re.search(r'usage: ', err, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('meshtastic.util.findPorts', return_value=[])
|
||||
@patch('platform.system')
|
||||
def test_tunnel_tunnel_arg_with_no_devices(mock_platform_system, patched_find_ports, caplog, capsys, reset_globals):
|
||||
"""Test tunnel with tunnel arg (act like we are on a linux system)"""
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
sys.argv = ['', '--tunnel']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
print(f'platform.system():{platform.system()}')
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
tunnelMain()
|
||||
mock_platform_system.assert_called()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Warning: No Meshtastic devices detected', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('meshtastic.util.findPorts', return_value=[])
|
||||
@patch('platform.system')
|
||||
def test_tunnel_subnet_arg_with_no_devices(mock_platform_system, patched_find_ports, caplog, capsys, reset_globals):
|
||||
"""Test tunnel with subnet arg (act like we are on a linux system)"""
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
sys.argv = ['', '--subnet', 'foo']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
print(f'platform.system():{platform.system()}')
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
tunnelMain()
|
||||
mock_platform_system.assert_called()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Warning: No Meshtastic devices detected', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_tunnel_tunnel_arg(mock_platform_system, caplog, reset_globals, iface_with_nodes, capsys):
|
||||
"""Test tunnel with tunnel arg (act like we are on a linux system)"""
|
||||
# Override the time.sleep so there is no loop
|
||||
def my_sleep(amount):
|
||||
sys.exit(3)
|
||||
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
sys.argv = ['', '--tunnel']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface):
|
||||
with patch('time.sleep', side_effect=my_sleep):
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
tunnelMain()
|
||||
mock_platform_system.assert_called()
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 3
|
||||
assert re.search(r'Not starting Tunnel', caplog.text, re.MULTILINE)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Connected to radio', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
@@ -10,6 +10,8 @@ from ..mesh_interface import MeshInterface
|
||||
from ..node import Node
|
||||
from .. import mesh_pb2
|
||||
from ..__init__ import LOCAL_ADDR, BROADCAST_ADDR
|
||||
from ..radioconfig_pb2 import RadioConfig
|
||||
from ..util import Timeout
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -57,10 +59,8 @@ def test_MeshInterface(capsys, reset_globals):
|
||||
def test_getMyUser(reset_globals, iface_with_nodes):
|
||||
"""Test getMyUser()"""
|
||||
iface = iface_with_nodes
|
||||
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
myuser = iface.getMyUser()
|
||||
print(f'myuser:{myuser}')
|
||||
assert myuser is not None
|
||||
assert myuser["id"] == '!9388f81c'
|
||||
|
||||
@@ -166,6 +166,22 @@ def test_sendPosition(reset_globals, caplog):
|
||||
assert re.search(r'p.time:', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_close_with_heartbeatTimer(reset_globals, caplog):
|
||||
"""Test close() with heartbeatTimer"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
anode = Node('foo', 'bar')
|
||||
radioConfig = RadioConfig()
|
||||
radioConfig.preferences.phone_timeout_secs = 10
|
||||
anode.radioConfig = radioConfig
|
||||
iface.localNode = anode
|
||||
assert iface.heartbeatTimer is None
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
iface._startHeartbeat()
|
||||
assert iface.heartbeatTimer is not None
|
||||
iface.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_handleFromRadio_empty_payload(reset_globals, caplog):
|
||||
"""Test _handleFromRadio"""
|
||||
@@ -460,3 +476,155 @@ def test_generatePacketId(capsys, reset_globals):
|
||||
assert re.search(r'Not connected yet, can not generate packet', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_fixupPosition_empty_pos(capsys, reset_globals):
|
||||
"""Test _fixupPosition()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
pos = {}
|
||||
newpos = iface._fixupPosition(pos)
|
||||
assert newpos == pos
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_fixupPosition_no_changes_needed(capsys, reset_globals):
|
||||
"""Test _fixupPosition()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
pos = {"latitude": 101, "longitude": 102}
|
||||
newpos = iface._fixupPosition(pos)
|
||||
assert newpos == pos
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_fixupPosition(capsys, reset_globals):
|
||||
"""Test _fixupPosition()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
pos = {"latitudeI": 1010000000, "longitudeI": 1020000000}
|
||||
newpos = iface._fixupPosition(pos)
|
||||
assert newpos == {"latitude": 101.0,
|
||||
"latitudeI": 1010000000,
|
||||
"longitude": 102.0,
|
||||
"longitudeI": 1020000000}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_nodeNumToId(capsys, reset_globals, iface_with_nodes):
|
||||
"""Test _nodeNumToId()"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
someid = iface._nodeNumToId(2475227164)
|
||||
assert someid == '!9388f81c'
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_nodeNumToId_not_found(capsys, reset_globals, iface_with_nodes):
|
||||
"""Test _nodeNumToId()"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
someid = iface._nodeNumToId(123)
|
||||
assert someid is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_nodeNumToId_to_all(capsys, reset_globals, iface_with_nodes):
|
||||
"""Test _nodeNumToId()"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
someid = iface._nodeNumToId(0xffffffff)
|
||||
assert someid == '^all'
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_getOrCreateByNum_minimal(capsys, reset_globals, iface_with_nodes):
|
||||
"""Test _getOrCreateByNum()"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
tmp = iface._getOrCreateByNum(123)
|
||||
assert tmp == {'num': 123}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_getOrCreateByNum_not_found(capsys, reset_globals, iface_with_nodes):
|
||||
"""Test _getOrCreateByNum()"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
iface._getOrCreateByNum(0xffffffff)
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_getOrCreateByNum(capsys, reset_globals, iface_with_nodes):
|
||||
"""Test _getOrCreateByNum()"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
tmp = iface._getOrCreateByNum(2475227164)
|
||||
assert tmp['num'] == 2475227164
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_enter():
|
||||
"""Test __enter__()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
assert iface == iface.__enter__()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_exit_with_exception(caplog):
|
||||
"""Test __exit__()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
with caplog.at_level(logging.ERROR):
|
||||
iface.__exit__('foo', 'bar', 'baz')
|
||||
assert re.search(r'An exception of type foo with value bar has occurred', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'Traceback: baz', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_showNodes_exclude_self(capsys, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test that we hit that continue statement"""
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
iface = iface_with_nodes
|
||||
iface.localNode.nodeNum = 2475227164
|
||||
iface.showNodes()
|
||||
iface.showNodes(includeSelf=False)
|
||||
capsys.readouterr()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_waitForConfig(caplog, capsys):
|
||||
"""Test waitForConfig()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
# override how long to wait
|
||||
iface._timeout = Timeout(0.01)
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
iface.waitForConfig()
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Exception: Timed out waiting for interface config', err, re.MULTILINE)
|
||||
assert out == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_waitConnected_raises_an_exception(caplog, capsys):
|
||||
"""Test waitConnected()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
iface.failure = "warn about something"
|
||||
iface._waitConnected(0.01)
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'warn about something', err, re.MULTILINE)
|
||||
assert out == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_waitConnected_isConnected_timeout(caplog, capsys):
|
||||
"""Test waitConnected()"""
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
iface = MeshInterface()
|
||||
iface._waitConnected(0.01)
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'warn about something', err, re.MULTILINE)
|
||||
assert out == ''
|
||||
|
||||
@@ -11,6 +11,7 @@ from ..serial_interface import SerialInterface
|
||||
from ..admin_pb2 import AdminMessage
|
||||
from ..channel_pb2 import Channel
|
||||
from ..radioconfig_pb2 import RadioConfig
|
||||
from ..util import Timeout
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -29,7 +30,7 @@ def test_node(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_node_reqquestConfig():
|
||||
def test_node_requestConfig(capsys):
|
||||
"""Test run requestConfig"""
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
amesg = MagicMock(autospec=AdminMessage)
|
||||
@@ -37,6 +38,9 @@ def test_node_reqquestConfig():
|
||||
with patch('meshtastic.admin_pb2.AdminMessage', return_value=amesg):
|
||||
anode = Node(mo, 'bar')
|
||||
anode.requestConfig()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Requesting preferences from remote node', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -87,6 +91,16 @@ def test_setOwner_no_short_name_and_long_name_has_words(caplog):
|
||||
assert re.search(r'p.set_owner.team:0', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setOwner_long_name_no_short(caplog):
|
||||
"""Test setOwner"""
|
||||
anode = Node('foo', 'bar', noProto=True)
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
anode.setOwner(long_name ='Aabo', is_licensed=True)
|
||||
assert re.search(r'p.set_owner.long_name:Aabo:', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'p.set_owner.short_name:Aab:', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_exitSimulator(caplog):
|
||||
"""Test exitSimulator"""
|
||||
@@ -106,13 +120,16 @@ def test_reboot(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setURL_empty_url():
|
||||
def test_setURL_empty_url(capsys):
|
||||
"""Test reboot"""
|
||||
anode = Node('foo', 'bar', noProto=True)
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
anode.setURL('')
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Warning: No RadioConfig has been read', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -133,7 +150,7 @@ def test_setURL_valid_URL(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setURL_valid_URL_but_no_settings(caplog):
|
||||
def test_setURL_valid_URL_but_no_settings(caplog, capsys):
|
||||
"""Test setURL"""
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
url = "https://www.meshtastic.org/d/#"
|
||||
@@ -143,6 +160,9 @@ def test_setURL_valid_URL_but_no_settings(caplog):
|
||||
anode.setURL(url)
|
||||
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 err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -623,7 +643,7 @@ def test_writeConfig(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_requestChannel_not_localNode(caplog):
|
||||
def test_requestChannel_not_localNode(caplog, capsys):
|
||||
"""Test _requestChannel()"""
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
|
||||
@@ -633,6 +653,9 @@ def test_requestChannel_not_localNode(caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
anode._requestChannel(0)
|
||||
assert re.search(r'Requesting channel 0 info from remote node', caplog.text, re.MULTILINE)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Requesting channel 0 info', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -857,3 +880,14 @@ def test_onResponseRequestSetting_with_error(capsys):
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Error on response', out)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_waitForConfig():
|
||||
"""Test waitForConfig()"""
|
||||
anode = Node('foo', 'bar')
|
||||
radioConfig = RadioConfig()
|
||||
anode.radioConfig = radioConfig
|
||||
anode._timeout = Timeout(0.01)
|
||||
result = anode.waitForConfig()
|
||||
assert not result
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_watchGPIOs(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_sendHardware_no_nodeid():
|
||||
def test_sendHardware_no_nodeid(capsys):
|
||||
"""Test sending no nodeid to _sendHardware()"""
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
|
||||
@@ -87,3 +87,6 @@ def test_sendHardware_no_nodeid():
|
||||
rhw = RemoteHardwareClient(mo)
|
||||
rhw._sendHardware(None, None)
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Warning: Must use a destination node ID', out)
|
||||
assert err == ''
|
||||
|
||||
@@ -11,7 +11,7 @@ from ..serial_interface import SerialInterface
|
||||
@pytest.mark.unit
|
||||
@patch('serial.Serial')
|
||||
@patch('meshtastic.util.findPorts', return_value=['/dev/ttyUSBfake'])
|
||||
def test_SerialInterface_single_port(mocked_findPorts, mocked_serial):
|
||||
def test_SerialInterface_single_port(mocked_findPorts, mocked_serial, capsys):
|
||||
"""Test that we can instantiate a SerialInterface with a single port"""
|
||||
iface = SerialInterface(noProto=True)
|
||||
iface.showInfo()
|
||||
@@ -19,6 +19,12 @@ def test_SerialInterface_single_port(mocked_findPorts, mocked_serial):
|
||||
iface.close()
|
||||
mocked_findPorts.assert_called()
|
||||
mocked_serial.assert_called()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Nodes in mesh', out, re.MULTILINE)
|
||||
assert re.search(r'Preferences', out, re.MULTILINE)
|
||||
assert re.search(r'Channels', out, re.MULTILINE)
|
||||
assert re.search(r'Primary channel', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import platform
|
||||
import os
|
||||
|
||||
# Do not like using hard coded sleeps, but it probably makes
|
||||
@@ -140,8 +141,9 @@ def test_smoke1_nodes():
|
||||
"""Test --nodes"""
|
||||
return_value, out = subprocess.getstatusoutput('meshtastic --nodes')
|
||||
assert re.match(r'Connected to radio', out)
|
||||
assert re.search(r'^│ N │ User', out, re.MULTILINE)
|
||||
assert re.search(r'^│ 1 │', out, re.MULTILINE)
|
||||
if platform.system() != 'Windows':
|
||||
assert re.search(r' User ', out, re.MULTILINE)
|
||||
assert re.search(r' 1 ', out, re.MULTILINE)
|
||||
assert return_value == 0
|
||||
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ def test_StreamInterface_with_noProto(caplog, reset_globals):
|
||||
assert data == test_data
|
||||
|
||||
|
||||
# Note: This takes a bit, so moving from unit to slow
|
||||
# Tip: If you want to see the print output, run with '-s' flag:
|
||||
# pytest -s meshtastic/tests/test_stream_interface.py::test_sendToRadioImpl
|
||||
## Note: This takes a bit, so moving from unit to slow
|
||||
## Tip: If you want to see the print output, run with '-s' flag:
|
||||
## pytest -s meshtastic/tests/test_stream_interface.py::test_sendToRadioImpl
|
||||
@pytest.mark.unitslow
|
||||
def test_sendToRadioImpl(caplog, reset_globals):
|
||||
"""Test _sendToRadioImpl()"""
|
||||
@@ -78,4 +78,3 @@ def test_sendToRadioImpl(caplog, reset_globals):
|
||||
assert re.search(r'Sending: ', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'reading character', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'In reader loop', caplog.text, re.MULTILINE)
|
||||
print(caplog.text)
|
||||
|
||||
@@ -13,6 +13,7 @@ def test_TCPInterface(capsys):
|
||||
"""Test that we can instantiate a TCPInterface"""
|
||||
with patch('socket.socket') as mock_socket:
|
||||
iface = TCPInterface(hostname='localhost', noProto=True)
|
||||
iface.myConnect()
|
||||
iface.showInfo()
|
||||
iface.localNode.showInfo()
|
||||
out, err = capsys.readouterr()
|
||||
@@ -24,3 +25,11 @@ def test_TCPInterface(capsys):
|
||||
assert err == ''
|
||||
assert mock_socket.called
|
||||
iface.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_without_connecting(capsys):
|
||||
"""Test that we can instantiate a TCPInterface with connectNow as false"""
|
||||
with patch('socket.socket'):
|
||||
iface = TCPInterface(hostname='localhost', noProto=True, connectNow=False)
|
||||
assert iface.socket is None
|
||||
|
||||
266
meshtastic/tests/test_tunnel.py
Normal file
266
meshtastic/tests/test_tunnel.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""Meshtastic unit tests for tunnel.py"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
|
||||
from ..tcp_interface import TCPInterface
|
||||
from ..tunnel import Tunnel, onTunnelReceive
|
||||
from ..globals import Globals
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_Tunnel_on_non_linux_system(mock_platform_system, reset_globals):
|
||||
"""Test that we cannot instantiate a Tunnel on a non Linux system"""
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'notLinux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with patch('socket.socket') as mock_socket:
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
iface = TCPInterface(hostname='localhost', noProto=True)
|
||||
Tunnel(iface)
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
assert mock_socket.called
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_Tunnel_without_interface(mock_platform_system, reset_globals):
|
||||
"""Test that we can not instantiate a Tunnel without a valid interface"""
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
Tunnel(None)
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
@patch('platform.system')
|
||||
def test_Tunnel_with_interface(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test that we can not instantiate a Tunnel without a valid interface"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.WARNING):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
assert tun == Globals.getInstance().get_tunnelInstance()
|
||||
iface.close()
|
||||
assert re.search(r'Not creating a TapDevice()', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'Not starting TUN reader', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'Not sending packet', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
@patch('platform.system')
|
||||
def test_onTunnelReceive_from_ourselves(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test onTunnelReceive"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
sys.argv = ['']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
packet = {'decoded': { 'payload': 'foo'}, 'from': 2475227164}
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
Globals.getInstance().set_tunnelInstance(tun)
|
||||
onTunnelReceive(packet, iface)
|
||||
assert re.search(r'in onTunnelReceive', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'Ignoring message we sent', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_onTunnelReceive_from_someone_else(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test onTunnelReceive"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
sys.argv = ['']
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
packet = {'decoded': { 'payload': 'foo'}, 'from': 123}
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
Globals.getInstance().set_tunnelInstance(tun)
|
||||
onTunnelReceive(packet, iface)
|
||||
assert re.search(r'in onTunnelReceive', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_shouldFilterPacket_random(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _shouldFilterPacket()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
# random packet
|
||||
packet = b'1234567890123456789012345678901234567890'
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
ignore = tun._shouldFilterPacket(packet)
|
||||
assert not ignore
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_shouldFilterPacket_in_blacklist(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _shouldFilterPacket()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
# faked IGMP
|
||||
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
ignore = tun._shouldFilterPacket(packet)
|
||||
assert ignore
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_shouldFilterPacket_icmp(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _shouldFilterPacket()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
# faked ICMP
|
||||
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
ignore = tun._shouldFilterPacket(packet)
|
||||
assert re.search(r'forwarding ICMP message', caplog.text, re.MULTILINE)
|
||||
assert not ignore
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_shouldFilterPacket_udp(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _shouldFilterPacket()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
# faked UDP
|
||||
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
ignore = tun._shouldFilterPacket(packet)
|
||||
assert re.search(r'forwarding udp', caplog.text, re.MULTILINE)
|
||||
assert not ignore
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_shouldFilterPacket_udp_blacklisted(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _shouldFilterPacket()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
# faked UDP
|
||||
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x6c\x07\x6c\x00\x00\x00'
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
# Note: custom logging level
|
||||
LOG_TRACE = 5
|
||||
with caplog.at_level(LOG_TRACE):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
ignore = tun._shouldFilterPacket(packet)
|
||||
assert re.search(r'ignoring blacklisted UDP', caplog.text, re.MULTILINE)
|
||||
assert ignore
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_shouldFilterPacket_tcp(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _shouldFilterPacket()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
# faked TCP
|
||||
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
ignore = tun._shouldFilterPacket(packet)
|
||||
assert re.search(r'forwarding tcp', caplog.text, re.MULTILINE)
|
||||
assert not ignore
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_shouldFilterPacket_tcp_blacklisted(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _shouldFilterPacket()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
# faked TCP
|
||||
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x0c\x17\x0c\x00\x00\x00'
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
# Note: custom logging level
|
||||
LOG_TRACE = 5
|
||||
with caplog.at_level(LOG_TRACE):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
ignore = tun._shouldFilterPacket(packet)
|
||||
assert re.search(r'ignoring blacklisted TCP', caplog.text, re.MULTILINE)
|
||||
assert ignore
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_ipToNodeId_none(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _ipToNodeId()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
nodeid = tun._ipToNodeId('something not useful')
|
||||
assert nodeid is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('platform.system')
|
||||
def test_ipToNodeId_all(mock_platform_system, caplog, reset_globals, iface_with_nodes):
|
||||
"""Test _ipToNodeId()"""
|
||||
iface = iface_with_nodes
|
||||
iface.noProto = True
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = 'Linux'
|
||||
mock_platform_system.side_effect = a_mock
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch('socket.socket'):
|
||||
tun = Tunnel(iface)
|
||||
nodeid = tun._ipToNodeId(b'\x00\x00\xff\xff')
|
||||
assert nodeid == '^all'
|
||||
@@ -3,11 +3,14 @@
|
||||
import re
|
||||
import logging
|
||||
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from meshtastic.util import (fixme, stripnl, pskToString, our_exit,
|
||||
support_info, genPSK256, fromStr, fromPSK,
|
||||
quoteBooleans, catchAndIgnore)
|
||||
quoteBooleans, catchAndIgnore,
|
||||
remove_keys_from_dict, Timeout, hexstr,
|
||||
ipstr, readnet_u16, findPorts, convert_mac_addr)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -38,7 +41,7 @@ def test_fromStr():
|
||||
assert fromStr('abc') == 'abc'
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.unitslow
|
||||
def test_quoteBooleans():
|
||||
"""Test quoteBooleans"""
|
||||
assert quoteBooleans('') == ''
|
||||
@@ -91,7 +94,7 @@ def test_pskToString_one_byte_non_zero_value():
|
||||
assert pskToString(bytes([0x01])) == 'default'
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.unitslow
|
||||
def test_pskToString_many_bytes():
|
||||
"""Test pskToString many bytes"""
|
||||
assert pskToString(bytes([0x02, 0x01])) == 'secret'
|
||||
@@ -103,20 +106,26 @@ def test_pskToString_simple():
|
||||
assert pskToString(bytes([0x03])) == 'simple2'
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_our_exit_zero_return_value():
|
||||
@pytest.mark.unitslow
|
||||
def test_our_exit_zero_return_value(capsys):
|
||||
"""Test our_exit with a zero return value"""
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
our_exit("Warning: Some message", 0)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Warning: Some message', out, re.MULTILINE)
|
||||
assert err == ''
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_our_exit_non_zero_return_value():
|
||||
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
|
||||
|
||||
@@ -149,3 +158,94 @@ def test_catchAndIgnore(caplog):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
catchAndIgnore("something", some_closure)
|
||||
assert re.search(r'Exception thrown in something', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
def test_remove_keys_from_dict_empty_keys_empty_dict():
|
||||
"""Test when keys and dict both are empty"""
|
||||
assert not remove_keys_from_dict((), {})
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_remove_keys_from_dict_empty_dict():
|
||||
"""Test when dict is empty"""
|
||||
assert not remove_keys_from_dict(('a'), {})
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_remove_keys_from_dict_empty_keys():
|
||||
"""Test when keys is empty"""
|
||||
assert remove_keys_from_dict((), {'a':1}) == {'a':1}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_remove_keys_from_dict():
|
||||
"""Test remove_keys_from_dict()"""
|
||||
assert remove_keys_from_dict(('b'), {'a':1, 'b':2}) == {'a':1}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_remove_keys_from_dict_multiple_keys():
|
||||
"""Test remove_keys_from_dict()"""
|
||||
keys = ('a', 'b')
|
||||
adict = {'a': 1, 'b': 2, 'c': 3}
|
||||
assert remove_keys_from_dict(keys, adict) == {'c':3}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_remove_keys_from_dict_nested():
|
||||
"""Test remove_keys_from_dict()"""
|
||||
keys = ('b')
|
||||
adict = {'a': {'b': 1}, 'b': 2, 'c': 3}
|
||||
exp = {'a': {}, 'c': 3}
|
||||
assert remove_keys_from_dict(keys, adict) == exp
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
def test_Timeout_not_found():
|
||||
"""Test Timeout()"""
|
||||
to = Timeout(0.2)
|
||||
attrs = ('foo')
|
||||
to.waitForSet('bar', attrs)
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
def test_Timeout_found():
|
||||
"""Test Timeout()"""
|
||||
to = Timeout(0.2)
|
||||
attrs = ()
|
||||
to.waitForSet('bar', attrs)
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
def test_hexstr():
|
||||
"""Test hexstr()"""
|
||||
assert hexstr(b'123') == '31:32:33'
|
||||
assert hexstr(b'') == ''
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ipstr():
|
||||
"""Test ipstr()"""
|
||||
assert ipstr(b'1234') == '49.50.51.52'
|
||||
assert ipstr(b'') == ''
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
def test_readnet_u16():
|
||||
"""Test readnet_u16()"""
|
||||
assert readnet_u16(b'123456', 2) == 13108
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch('serial.tools.list_ports.comports', return_value=[])
|
||||
def test_findPorts_when_none_found(patch_comports):
|
||||
"""Test findPorts()"""
|
||||
assert not findPorts()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_convert_mac_addr():
|
||||
"""Test convert_mac_addr()"""
|
||||
assert convert_mac_addr('/c0gFyhb') == 'fd:cd:20:17:28:5b'
|
||||
assert convert_mac_addr('') == ''
|
||||
|
||||
@@ -17,61 +17,28 @@
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import platform
|
||||
from pubsub import pub
|
||||
|
||||
from pytap2 import TapDevice
|
||||
|
||||
from . import portnums_pb2
|
||||
|
||||
# A new non standard log level that is lower level than DEBUG
|
||||
LOG_TRACE = 5
|
||||
|
||||
# fixme - find a way to move onTunnelReceive inside of the class
|
||||
tunnelInstance = None
|
||||
|
||||
"""A list of chatty UDP services we should never accidentally
|
||||
forward to our slow network"""
|
||||
udpBlacklist = {
|
||||
1900, # SSDP
|
||||
5353, # multicast DNS
|
||||
}
|
||||
|
||||
"""A list of TCP services to block"""
|
||||
tcpBlacklist = {}
|
||||
|
||||
"""A list of protocols we ignore"""
|
||||
protocolBlacklist = {
|
||||
0x02, # IGMP
|
||||
0x80, # Service-Specific Connection-Oriented Protocol in a Multilink and Connectionless Environment
|
||||
}
|
||||
|
||||
|
||||
def hexstr(barray):
|
||||
"""Print a string of hex digits"""
|
||||
return ":".join('{:02x}'.format(x) for x in barray)
|
||||
|
||||
|
||||
def ipstr(barray):
|
||||
"""Print a string of ip digits"""
|
||||
return ".".join('{}'.format(x) for x in barray)
|
||||
|
||||
|
||||
def readnet_u16(p, offset):
|
||||
"""Read big endian u16 (network byte order)"""
|
||||
return p[offset] * 256 + p[offset + 1]
|
||||
from meshtastic import portnums_pb2
|
||||
from meshtastic.util import ipstr, readnet_u16
|
||||
from meshtastic.globals import Globals
|
||||
|
||||
|
||||
def onTunnelReceive(packet, interface):
|
||||
"""Callback for received tunneled messages from mesh
|
||||
|
||||
FIXME figure out how to do closures with methods in python"""
|
||||
"""Callback for received tunneled messages from mesh."""
|
||||
logging.debug(f'in onTunnelReceive()')
|
||||
our_globals = Globals.getInstance()
|
||||
tunnelInstance = our_globals.get_tunnelInstance()
|
||||
tunnelInstance.onReceive(packet)
|
||||
|
||||
|
||||
class Tunnel:
|
||||
"""A TUN based IP tunnel over meshtastic"""
|
||||
|
||||
def __init__(self, iface, subnet=None, netmask="255.255.0.0"):
|
||||
def __init__(self, iface, subnet='10.115', netmask="255.255.0.0"):
|
||||
"""
|
||||
Constructor
|
||||
|
||||
@@ -79,35 +46,69 @@ class Tunnel:
|
||||
subnet is used to construct our network number (normally 10.115.x.x)
|
||||
"""
|
||||
|
||||
if subnet is None:
|
||||
subnet = "10.115"
|
||||
if not iface:
|
||||
raise Exception("Tunnel() must have a interface")
|
||||
|
||||
self.iface = iface
|
||||
self.subnetPrefix = subnet
|
||||
|
||||
global tunnelInstance
|
||||
tunnelInstance = self
|
||||
if platform.system() != 'Linux':
|
||||
raise Exception("Tunnel() can only be run instantiated on a Linux system")
|
||||
|
||||
our_globals = Globals.getInstance()
|
||||
our_globals.set_tunnelInstance(self)
|
||||
|
||||
"""A list of chatty UDP services we should never accidentally
|
||||
forward to our slow network"""
|
||||
self.udpBlacklist = {
|
||||
1900, # SSDP
|
||||
5353, # multicast DNS
|
||||
}
|
||||
|
||||
"""A list of TCP services to block"""
|
||||
self.tcpBlacklist = {
|
||||
5900, # VNC (Note: Only adding for testing purposes.)
|
||||
}
|
||||
|
||||
"""A list of protocols we ignore"""
|
||||
self.protocolBlacklist = {
|
||||
0x02, # IGMP
|
||||
0x80, # Service-Specific Connection-Oriented Protocol in a Multilink and Connectionless Environment
|
||||
}
|
||||
|
||||
# A new non standard log level that is lower level than DEBUG
|
||||
self.LOG_TRACE = 5
|
||||
|
||||
# TODO: check if root?
|
||||
logging.info("Starting IP to mesh tunnel (you must be root for this *pre-alpha* "\
|
||||
"feature to work). Mesh members:")
|
||||
|
||||
pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP")
|
||||
myAddr = self._nodeNumToIp(self.iface.myInfo.my_node_num)
|
||||
|
||||
for node in self.iface.nodes.values():
|
||||
nodeId = node["user"]["id"]
|
||||
ip = self._nodeNumToIp(node["num"])
|
||||
logging.info(f"Node { nodeId } has IP address { ip }")
|
||||
if self.iface.nodes:
|
||||
for node in self.iface.nodes.values():
|
||||
nodeId = node["user"]["id"]
|
||||
ip = self._nodeNumToIp(node["num"])
|
||||
logging.info(f"Node { nodeId } has IP address { ip }")
|
||||
|
||||
logging.debug("creating TUN device with MTU=200")
|
||||
# FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data
|
||||
self.tun = TapDevice(name="mesh")
|
||||
self.tun.up()
|
||||
self.tun.ifconfig(address=myAddr, netmask=netmask, mtu=200)
|
||||
logging.debug(f"starting TUN reader, our IP address is {myAddr}")
|
||||
self._rxThread = threading.Thread(
|
||||
target=self.__tunReader, args=(), daemon=True)
|
||||
self._rxThread.start()
|
||||
self.tun = None
|
||||
if self.iface.noProto:
|
||||
logging.warning(f"Not creating a TapDevice() because it is disabled by noProto")
|
||||
else:
|
||||
self.tun = TapDevice(name="mesh")
|
||||
self.tun.up()
|
||||
self.tun.ifconfig(address=myAddr, netmask=netmask, mtu=200)
|
||||
|
||||
self._rxThread = None
|
||||
if self.iface.noProto:
|
||||
logging.warning(f"Not starting TUN reader because it is disabled by noProto")
|
||||
else:
|
||||
logging.debug(f"starting TUN reader, our IP address is {myAddr}")
|
||||
self._rxThread = threading.Thread(target=self.__tunReader, args=(), daemon=True)
|
||||
self._rxThread.start()
|
||||
|
||||
def onReceive(self, packet):
|
||||
"""onReceive"""
|
||||
@@ -115,12 +116,12 @@ class Tunnel:
|
||||
if packet["from"] == self.iface.myInfo.my_node_num:
|
||||
logging.debug("Ignoring message we sent")
|
||||
else:
|
||||
logging.debug(
|
||||
f"Received mesh tunnel message type={type(p)} len={len(p)}")
|
||||
logging.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
|
||||
# we don't really need to check for filtering here (sender should have checked),
|
||||
# but this provides useful debug printing on types of packets received
|
||||
if not self._shouldFilterPacket(p):
|
||||
self.tun.write(p)
|
||||
if not self.iface.noProto:
|
||||
if not self._shouldFilterPacket(p):
|
||||
self.tun.write(p)
|
||||
|
||||
def _shouldFilterPacket(self, p):
|
||||
"""Given a packet, decode it and return true if it should be ignored"""
|
||||
@@ -129,10 +130,9 @@ class Tunnel:
|
||||
destAddr = p[16:20]
|
||||
subheader = 20
|
||||
ignore = False # Assume we will be forwarding the packet
|
||||
if protocol in protocolBlacklist:
|
||||
if protocol in self.protocolBlacklist:
|
||||
ignore = True
|
||||
logging.log(
|
||||
LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
|
||||
logging.log(self.LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
|
||||
elif protocol == 0x01: # ICMP
|
||||
icmpType = p[20]
|
||||
icmpCode = p[21]
|
||||
@@ -145,19 +145,17 @@ class Tunnel:
|
||||
elif protocol == 0x11: # UDP
|
||||
srcport = readnet_u16(p, subheader)
|
||||
destport = readnet_u16(p, subheader + 2)
|
||||
if destport in udpBlacklist:
|
||||
if destport in self.udpBlacklist:
|
||||
ignore = True
|
||||
logging.log(
|
||||
LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
|
||||
logging.log(self.LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
|
||||
else:
|
||||
logging.debug(
|
||||
f"forwarding udp srcport={srcport}, destport={destport}")
|
||||
logging.debug(f"forwarding udp srcport={srcport}, destport={destport}")
|
||||
elif protocol == 0x06: # TCP
|
||||
srcport = readnet_u16(p, subheader)
|
||||
destport = readnet_u16(p, subheader + 2)
|
||||
if destport in tcpBlacklist:
|
||||
if destport in self.tcpBlacklist:
|
||||
ignore = True
|
||||
logging.log(LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
|
||||
logging.log(self.LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
|
||||
else:
|
||||
logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
|
||||
else:
|
||||
|
||||
@@ -4,6 +4,7 @@ import traceback
|
||||
from queue import Queue
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import platform
|
||||
import logging
|
||||
@@ -169,8 +170,7 @@ class DeferredExecution():
|
||||
o = self.queue.get()
|
||||
o()
|
||||
except:
|
||||
logging.error(
|
||||
f"Unexpected error in deferred execution {sys.exc_info()[0]}")
|
||||
logging.error(f"Unexpected error in deferred execution {sys.exc_info()[0]}")
|
||||
print(traceback.format_exc())
|
||||
|
||||
|
||||
@@ -201,3 +201,42 @@ def support_info():
|
||||
platform.python_implementation(), platform.python_compiler()))
|
||||
print('')
|
||||
print('Please add the output from the command: meshtastic --info')
|
||||
|
||||
|
||||
def remove_keys_from_dict(keys, adict):
|
||||
"""Return a dictionary without some keys in it.
|
||||
Will removed nested keys.
|
||||
"""
|
||||
for key in keys:
|
||||
try:
|
||||
del adict[key]
|
||||
except:
|
||||
pass
|
||||
for val in adict.values():
|
||||
if isinstance(val, dict):
|
||||
remove_keys_from_dict(keys, val)
|
||||
return adict
|
||||
|
||||
|
||||
def hexstr(barray):
|
||||
"""Print a string of hex digits"""
|
||||
return ":".join('{:02x}'.format(x) for x in barray)
|
||||
|
||||
|
||||
def ipstr(barray):
|
||||
"""Print a string of ip digits"""
|
||||
return ".".join('{}'.format(x) for x in barray)
|
||||
|
||||
|
||||
def readnet_u16(p, offset):
|
||||
"""Read big endian u16 (network byte order)"""
|
||||
return p[offset] * 256 + p[offset + 1]
|
||||
|
||||
|
||||
def convert_mac_addr(val):
|
||||
"""Convert the base 64 encoded value to a mac address
|
||||
val - base64 encoded value (ex: '/c0gFyhb'))
|
||||
returns: a string formatted like a mac address (ex: 'fd:cd:20:17:28:5b')
|
||||
"""
|
||||
val_as_bytes = base64.b64decode(val)
|
||||
return hexstr(val_as_bytes)
|
||||
|
||||
2
proto
2
proto
Submodule proto updated: 1d3b4806ab...7b80bde421
5
setup.py
5
setup.py
@@ -12,7 +12,7 @@ with open("README.md", "r") as fh:
|
||||
# This call to setup() does all the work
|
||||
setup(
|
||||
name="meshtastic",
|
||||
version="1.2.46",
|
||||
version="1.2.52",
|
||||
description="Python API & client shell for talking to Meshtastic devices",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
@@ -23,7 +23,10 @@ setup(
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
packages=["meshtastic"],
|
||||
include_package_data=True,
|
||||
|
||||
5
vercel.json
Normal file
5
vercel.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"github": {
|
||||
"silent": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user