mirror of
https://github.com/meshtastic/python.git
synced 2025-12-25 17:07:53 -05:00
Compare commits
120 Commits
2.3.4.post
...
2.3.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96afa703ba | ||
|
|
1b14b1ef20 | ||
|
|
68836b1af1 | ||
|
|
195f0c9d90 | ||
|
|
1ff7334385 | ||
|
|
b15e27c7b6 | ||
|
|
267923fdc5 | ||
|
|
9ab1b32bdb | ||
|
|
3a4795d3b8 | ||
|
|
c2a2d5a77c | ||
|
|
b30cde979c | ||
|
|
8456f36c6b | ||
|
|
e6a88e055f | ||
|
|
7bea6f6120 | ||
|
|
725de4c2f9 | ||
|
|
4203553a44 | ||
|
|
9e319f3c52 | ||
|
|
cd5913ae6d | ||
|
|
ccfb04720f | ||
|
|
c34d08b0e5 | ||
|
|
b5d1b7612f | ||
|
|
b58094b9ce | ||
|
|
23f41bff0a | ||
|
|
a1021c4f78 | ||
|
|
b06329f47e | ||
|
|
53b0e35b0c | ||
|
|
9ac5aeeaf0 | ||
|
|
5c703aff1d | ||
|
|
5441266565 | ||
|
|
890557fa5d | ||
|
|
e27d210a71 | ||
|
|
16c08b8b47 | ||
|
|
ebd3c7f5e8 | ||
|
|
da0312a5b0 | ||
|
|
919ae8c40f | ||
|
|
dd4fccbc77 | ||
|
|
32682b5230 | ||
|
|
9dab76bb64 | ||
|
|
e6d61c6603 | ||
|
|
ee857c5128 | ||
|
|
87a4bb0888 | ||
|
|
d72cc0e201 | ||
|
|
b350b9eab9 | ||
|
|
dc112f2f3a | ||
|
|
14ae4eeac1 | ||
|
|
bbc526d0a8 | ||
|
|
abe98f5079 | ||
|
|
e8dfee8454 | ||
|
|
1746ad15d7 | ||
|
|
4d67e7fc76 | ||
|
|
3b112d2f49 | ||
|
|
93e9c1c66c | ||
|
|
8e641b3186 | ||
|
|
ed545cd9b4 | ||
|
|
bcd60c9ef7 | ||
|
|
c3d044e3f2 | ||
|
|
8d538e8f24 | ||
|
|
fa1a3d7901 | ||
|
|
3adeb925dd | ||
|
|
9de04a12e1 | ||
|
|
e01a1bb6e0 | ||
|
|
1ebea53703 | ||
|
|
ba2d6c9d93 | ||
|
|
fef0e1b77f | ||
|
|
dc1dec17b5 | ||
|
|
02ed17837b | ||
|
|
8cf996a242 | ||
|
|
02189aae07 | ||
|
|
894581ce00 | ||
|
|
fb8db01427 | ||
|
|
fa5ede93ed | ||
|
|
022a8a1017 | ||
|
|
21f6e25ab0 | ||
|
|
243e297505 | ||
|
|
9fc86f9450 | ||
|
|
e5999f5be6 | ||
|
|
3886bc16d7 | ||
|
|
7fe98bc266 | ||
|
|
2f9307fd99 | ||
|
|
3d6fa369e8 | ||
|
|
6812f508bc | ||
|
|
158cac6b0b | ||
|
|
4d10b6e1bd | ||
|
|
bb6e3b7a49 | ||
|
|
8b29dc683a | ||
|
|
d4156d78c6 | ||
|
|
c52977781e | ||
|
|
5ebc8e6f95 | ||
|
|
56d1dcafe5 | ||
|
|
393c765557 | ||
|
|
243b310eb2 | ||
|
|
cd1ecab4e0 | ||
|
|
c710953b85 | ||
|
|
70c5a30b77 | ||
|
|
9f0ba7aeae | ||
|
|
4226201423 | ||
|
|
bdf3a24be1 | ||
|
|
e8ba5581f6 | ||
|
|
948846e0f1 | ||
|
|
6c4dbb6fe6 | ||
|
|
afbabf9538 | ||
|
|
d8107122a2 | ||
|
|
03c1f08e45 | ||
|
|
760fcfcea7 | ||
|
|
a4830f5f62 | ||
|
|
2b1f337a41 | ||
|
|
ddad5f08b3 | ||
|
|
6e7933a3ce | ||
|
|
f449ff9506 | ||
|
|
a07e853f69 | ||
|
|
0d57449030 | ||
|
|
067cddd354 | ||
|
|
4af1b322da | ||
|
|
c580df15e1 | ||
|
|
b280d0ba23 | ||
|
|
439b1ade2e | ||
|
|
9f2b54eb98 | ||
|
|
278ca74a70 | ||
|
|
1c93b7bd52 | ||
|
|
590b60fefb |
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -18,30 +18,29 @@ jobs:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python 3
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
- name: Uninstall meshtastic
|
||||
run: |
|
||||
pip3 uninstall meshtastic
|
||||
pip3 uninstall -y meshtastic
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install -r requirements.txt
|
||||
pip3 install poetry
|
||||
- name: Install meshtastic from local
|
||||
run: |
|
||||
pip3 install .
|
||||
which meshtastic
|
||||
meshtastic --version
|
||||
poetry install
|
||||
poetry run meshtastic --version
|
||||
- name: Run pylint
|
||||
run: pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"
|
||||
run: poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"
|
||||
- name: Check types with mypy
|
||||
run: mypy meshtastic/
|
||||
run: poetry run mypy meshtastic/
|
||||
- name: Run tests with pytest
|
||||
run: pytest --cov=meshtastic
|
||||
run: poetry run pytest --cov=meshtastic
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
pytest --cov=meshtastic --cov-report=xml
|
||||
poetry run pytest --cov=meshtastic --cov-report=xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
@@ -62,11 +61,12 @@ jobs:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python 3
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v5
|
||||
- name: Install meshtastic from local
|
||||
run: |
|
||||
pip3 install .
|
||||
which meshtastic
|
||||
meshtastic --version
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install poetry
|
||||
poetry install
|
||||
poetry run meshtastic --version
|
||||
|
||||
87
.github/workflows/release.yml
vendored
87
.github/workflows/release.yml
vendored
@@ -12,26 +12,36 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
#- name: Bump version
|
||||
# run: >-
|
||||
# bin/bump_version.py
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
#- name: Commit updated version.py
|
||||
# id: commit_updated
|
||||
# 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 setup.py
|
||||
# git commit -m "bump version" && git push || echo "No changes to commit"
|
||||
# git log -n 1 --pretty=format:"%H" | tail -n 1 | awk '{print "::set-output name=sha::"$0}'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install poetry
|
||||
|
||||
- name: Bump version
|
||||
run: >-
|
||||
poetry version patch
|
||||
|
||||
- name: Commit updated version.
|
||||
id: commit_updated
|
||||
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 pyproject.toml
|
||||
git commit -m "bump version" && git push || echo "No changes to commit"
|
||||
git log -n 1 --pretty=format:"%H" | tail -n 1 | awk '{print "::set-output name=sha::"$0}'
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: >-
|
||||
bin/show_version.py
|
||||
poetry version --short | sed 's/^/::set-output name=version::/'
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/create-release@v1
|
||||
@@ -47,26 +57,9 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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/
|
||||
.
|
||||
poetry build
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
@@ -79,14 +72,14 @@ jobs:
|
||||
# needs: release_create
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v3
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# ref: ${{ needs.release_create.outputs.new_sha }}
|
||||
|
||||
# - name: Set up Python 3.9
|
||||
# uses: actions/setup-python@v2
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: 3.9
|
||||
# python-version: "3.9"
|
||||
|
||||
# - name: Setup code signing
|
||||
# env:
|
||||
@@ -125,21 +118,19 @@ jobs:
|
||||
needs: release_create
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release_create.outputs.new_sha }}
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pip install pyinstaller
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
pyinstaller -F -n meshtastic --collect-all meshtastic meshtastic/__main__.py
|
||||
pip install poetry
|
||||
bin/build-bin.sh
|
||||
|
||||
- name: Add ubuntu to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -166,21 +157,19 @@ jobs:
|
||||
needs: release_create
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release_create.outputs.new_sha }}
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pip install pyinstaller
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
pyinstaller -F -n meshtastic --collect-all meshtastic meshtastic/__main__.py
|
||||
pip install poetry
|
||||
bin/build-bin.sh
|
||||
|
||||
- name: Add windows to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
|
||||
13
.github/workflows/update_protobufs.yml
vendored
13
.github/workflows/update_protobufs.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
@@ -18,9 +18,14 @@ jobs:
|
||||
|
||||
- name: Download nanopb
|
||||
run: |
|
||||
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.6-linux-x86.tar.gz
|
||||
tar xvzf nanopb-0.4.6-linux-x86.tar.gz
|
||||
mv nanopb-0.4.6-linux-x86 nanopb-0.4.6
|
||||
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
|
||||
tar xvzf nanopb-0.4.8-linux-x86.tar.gz
|
||||
mv nanopb-0.4.8-linux-x86 nanopb-0.4.8
|
||||
|
||||
- name: Install poetry (needed by regen-protobufs.sh)
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install poetry
|
||||
|
||||
- name: Re-generate protocol buffers
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ venv/
|
||||
__pycache__
|
||||
examples/__pycache__
|
||||
meshtastic.spec
|
||||
.hypothesis/
|
||||
coverage.xml
|
||||
@@ -28,8 +28,7 @@ If you're interested in contributing but don't have specific things you'd like t
|
||||
This should always be considered a list in progress and flux -- inclusion doesn't guarantee implementation, and exclusion doesn't mean something's not wanted. GitHub issues are a great place to discuss ideas.
|
||||
|
||||
* Types
|
||||
* type annotations throughout the codebase
|
||||
* mypy running in CI to type-check new code
|
||||
* type annotations throughout the codebase, and upgrading mypy running in CI to `--strict`
|
||||
* async-friendliness
|
||||
* CLI completeness & consistency
|
||||
* the CLI should support all features of the firmware
|
||||
@@ -40,9 +39,9 @@ This should always be considered a list in progress and flux -- inclusion doesn'
|
||||
* pubsub events should be documented clearly
|
||||
* helpers for third-party code
|
||||
* it should be easy to write a script that supports similar options to the CLI so many tools support the same ways of connecting to nodes
|
||||
* interactive client
|
||||
* data storage & processing
|
||||
* there should be a standardized way of recording packets for later use, debugging, etc.
|
||||
* a persistence layer could also keep track of nodes beyond nodedb, as the apps do
|
||||
* a sqlite database schema and tools for writing to it may be a good starting point
|
||||
* enable maps, charts, visualizations
|
||||
|
||||
|
||||
9
bin/build-bin.sh
Executable file
9
bin/build-bin.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo Building ubuntu binary
|
||||
poetry install
|
||||
source $(poetry env info --path)/bin/activate
|
||||
pyinstaller -F -n meshtastic --collect-all meshtastic meshtastic/__main__.py
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Bump the version number"""
|
||||
import re
|
||||
|
||||
version_filename = "setup.py"
|
||||
|
||||
lines = None
|
||||
|
||||
with open(version_filename, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
with open(version_filename, "w", encoding="utf-8") as f:
|
||||
for line in lines:
|
||||
if line.lstrip().startswith("version="):
|
||||
# get rid of quotes around the version
|
||||
line = line.replace('"', "")
|
||||
# get rid of trailing comma
|
||||
line = line.replace(",", "")
|
||||
# split on '='
|
||||
words = line.split("=")
|
||||
# split the version into parts (by period)
|
||||
v = words[1].split(".")
|
||||
build_num = re.findall(r"\d+", v[2])[0]
|
||||
new_build_num = str(int(build_num) + 1)
|
||||
ver = f"{v[0]}.{v[1]}.{v[2].replace(build_num, new_build_num)}".replace(
|
||||
"\n", ""
|
||||
)
|
||||
f.write(f' version="{ver}",\n')
|
||||
else:
|
||||
f.write(line)
|
||||
@@ -3,20 +3,24 @@ set -e
|
||||
# You may consider running: "pytest -m smoke1" instead of this test.
|
||||
|
||||
echo "Running (crude) prerelease tests to verify sanity"
|
||||
|
||||
# Use the python environment created by poetry
|
||||
source $(poetry env info --path)/bin/activate
|
||||
|
||||
echo running hello
|
||||
python3 tests/hello_world.py
|
||||
# bin/run.sh --help
|
||||
# meshtastic --help
|
||||
echo toggling router
|
||||
bin/run.sh --set is_router true
|
||||
bin/run.sh --set is_router false
|
||||
meshtastic --set is_router true
|
||||
meshtastic --set is_router false
|
||||
# TODO: This does not seem to work.
|
||||
echo setting channel
|
||||
bin/run.sh --seturl "https://www.meshtastic.org/c/#GAMiENTxuzogKQdZ8Lz_q89Oab8qB0RlZmF1bHQ="
|
||||
meshtastic --seturl "https://www.meshtastic.org/c/#GAMiENTxuzogKQdZ8Lz_q89Oab8qB0RlZmF1bHQ="
|
||||
echo setting owner
|
||||
bin/run.sh --set-owner "Test Build"
|
||||
meshtastic --set-owner "Test Build"
|
||||
echo setting position
|
||||
bin/run.sh --setlat 32.7767 --setlon -96.7970 --setalt 1337
|
||||
meshtastic --setlat 32.7767 --setlon -96.7970 --setalt 1337
|
||||
echo dumping info
|
||||
bin/run.sh --info
|
||||
meshtastic run meshtastic --info
|
||||
echo sending closing message
|
||||
bin/run.sh --sendtext "Sanity complete"
|
||||
meshtastic --sendtext "Sanity complete"
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
#gsed -i 's/import "\//import ".\//g' ./protobufs/meshtastic/*
|
||||
#gsed -i 's/package meshtastic;//g' ./protobufs/meshtastic/*
|
||||
|
||||
./nanopb-0.4.7/generator-bin/protoc -I=protobufs --python_out ./ --mypy_out ./ ./protobufs/meshtastic/*.proto
|
||||
./nanopb-0.4.7/generator-bin/protoc -I=protobufs --python_out ./meshtastic/ --mypy_out ./meshtastic/ ./protobufs/nanopb.proto
|
||||
# protoc looks for mypy plugin in the python path
|
||||
source $(poetry env info --path)/bin/activate
|
||||
|
||||
./nanopb-0.4.8/generator-bin/protoc -I=protobufs --python_out ./ --mypy_out ./ ./protobufs/meshtastic/*.proto
|
||||
./nanopb-0.4.8/generator-bin/protoc -I=protobufs --python_out ./meshtastic/ --mypy_out ./meshtastic/ ./protobufs/nanopb.proto
|
||||
|
||||
# workaround for import bug in protoc https://github.com/protocolbuffers/protobuf/issues/1491#issuecomment-690618628
|
||||
|
||||
|
||||
11
bin/run-ci-local.sh
Executable file
11
bin/run-ci-local.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script lets you run github ci actions locally
|
||||
# You need to have act installed. You can get it at https://nektosact.com/
|
||||
|
||||
# by default it simulates a push event
|
||||
# other useful options
|
||||
# -j build-and-publish-ubuntu
|
||||
|
||||
# also: we only run one of the 4 matrix tests, because otherwise it absolutely hammers the CPU (so many containers and threads)
|
||||
act -P ubuntu-latest=-self-hosted --matrix "python-version:3.8" "$@"
|
||||
@@ -1,2 +0,0 @@
|
||||
rm log_*
|
||||
python3 -m meshtastic "$@"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Show the version number"""
|
||||
|
||||
version_filename = "setup.py"
|
||||
|
||||
lines = None
|
||||
|
||||
with open(version_filename, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
for line in lines:
|
||||
if line.lstrip().startswith("version="):
|
||||
# get rid of quotes around the version
|
||||
line2 = line.replace('"', "")
|
||||
# get rid of the trailing comma
|
||||
line2 = line2.replace(",", "")
|
||||
# split on =
|
||||
words = line2.split("=")
|
||||
# Note: This format is for github actions
|
||||
print(f"::set-output name=version::{words[1].strip()}")
|
||||
@@ -3,8 +3,6 @@ set -e
|
||||
|
||||
bin/regen-docs.sh
|
||||
pandoc --from=markdown --to=rst --output=README README.md
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m twine check dist/*
|
||||
# test the upload
|
||||
python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
|
||||
|
||||
poetry publish -r test-pypi --build
|
||||
echo "view the upload at https://test.pypi.org/ it it looks good upload for real"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
rm dist/*
|
||||
set -e
|
||||
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m twine upload dist/*
|
||||
poetry build
|
||||
poetry run pytest
|
||||
poetry publish
|
||||
#python3 setup.py sdist bdist_wheel
|
||||
#python3 -m twine upload dist/*
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
"""
|
||||
# an API for Meshtastic devices
|
||||
# A library for the Meshtastic Client API
|
||||
|
||||
Primary class: SerialInterface
|
||||
Primary interfaces: SerialInterface, TCPInterface, BLEInterface
|
||||
Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)"
|
||||
Source code on [github](https://github.com/meshtastic/python)
|
||||
|
||||
properties of SerialInterface:
|
||||
notable properties of interface classes:
|
||||
|
||||
- localConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to
|
||||
the device.
|
||||
- nodes - The database of received nodes. Includes always up-to-date location and username information for each
|
||||
node in the mesh. This is a read-only datastructure.
|
||||
- nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId
|
||||
- myInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
|
||||
- myInfo & metadata - Contain read-only information about the local radio device (software version, hardware version, etc)
|
||||
- localNode - Pointer to a node object for the local node
|
||||
|
||||
notable properties of nodes:
|
||||
- localConfig - Current radio settings, can be written to the radio with the `writeConfig` method.
|
||||
- moduleConfig - Current module settings, can be written to the radio with the `writeConfig` method.
|
||||
- channels - The node's channels, keyed by index.
|
||||
|
||||
# Published PubSub topics
|
||||
|
||||
@@ -72,7 +76,6 @@ from typing import *
|
||||
|
||||
import google.protobuf.json_format
|
||||
import serial # type: ignore[import-untyped]
|
||||
import timeago # type: ignore[import-untyped]
|
||||
from dotmap import DotMap # type: ignore[import-untyped]
|
||||
from google.protobuf.json_format import MessageToJson
|
||||
from pubsub import pub # type: ignore[import-untyped]
|
||||
@@ -113,6 +116,9 @@ OUR_APP_VERSION = 20300
|
||||
format is Mmmss (where M is 1+the numeric major number. i.e. 20120 means 1.1.20
|
||||
"""
|
||||
|
||||
NODELESS_WANT_CONFIG_ID = 69420
|
||||
"""A special thing to pass for want_config_id that instructs nodes to skip sending nodeinfos other than its own."""
|
||||
|
||||
publishingThread = DeferredExecution("publishing")
|
||||
|
||||
|
||||
@@ -121,6 +127,7 @@ class ResponseHandler(NamedTuple):
|
||||
|
||||
# requestId: int - used only as a key
|
||||
callback: Callable
|
||||
ackPermitted: bool = False
|
||||
# FIXME, add timestamp and age out old requests
|
||||
|
||||
|
||||
|
||||
@@ -16,16 +16,15 @@ from pubsub import pub # type: ignore[import-untyped]
|
||||
|
||||
import meshtastic.test
|
||||
import meshtastic.util
|
||||
from meshtastic import mt_config
|
||||
from meshtastic import channel_pb2, config_pb2, portnums_pb2, remote_hardware, BROADCAST_ADDR
|
||||
from meshtastic.version import get_active_version
|
||||
from meshtastic.ble_interface import BLEInterface
|
||||
from meshtastic.globals import Globals
|
||||
|
||||
from meshtastic.mesh_interface import MeshInterface
|
||||
|
||||
def onReceive(packet, interface):
|
||||
"""Callback invoked when a packet arrives"""
|
||||
our_globals = Globals.getInstance()
|
||||
args = our_globals.get_args()
|
||||
args = mt_config.args
|
||||
try:
|
||||
d = packet.get("decoded")
|
||||
logging.debug(f"in onReceive() d:{d}")
|
||||
@@ -58,6 +57,11 @@ def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613
|
||||
"""Callback invoked when we connect/disconnect from a radio"""
|
||||
print(f"Connection changed: {topic.getName()}")
|
||||
|
||||
def checkChannel(interface: MeshInterface, channelIndex: int) -> bool:
|
||||
"""Given an interface and channel index, return True if that channel is non-disabled on the local node"""
|
||||
ch = interface.localNode.getChannelByChannelIndex(channelIndex)
|
||||
logging.debug(f"ch:{ch}")
|
||||
return (ch and ch.role != channel_pb2.Channel.Role.DISABLED)
|
||||
|
||||
def getPref(node, comp_name):
|
||||
"""Get a channel or preferences value"""
|
||||
@@ -69,7 +73,7 @@ def getPref(node, comp_name):
|
||||
# Note: protobufs has the keys in snake_case, so snake internally
|
||||
snake_name = meshtastic.util.camel_to_snake(name[1])
|
||||
logging.debug(f"snake_name:{snake_name} camel_name:{camel_name}")
|
||||
logging.debug(f"use camel:{Globals.getInstance().get_camel_case()}")
|
||||
logging.debug(f"use camel:{mt_config.camel_case}")
|
||||
|
||||
# First validate the input
|
||||
localConfig = node.localConfig
|
||||
@@ -86,7 +90,7 @@ def getPref(node, comp_name):
|
||||
break
|
||||
|
||||
if not found:
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
print(
|
||||
f"{localConfig.__class__.__name__} and {moduleConfig.__class__.__name__} do not have an attribute {snake_name}."
|
||||
)
|
||||
@@ -105,7 +109,7 @@ def getPref(node, comp_name):
|
||||
config_values = getattr(config, config_type.name)
|
||||
if not wholeField:
|
||||
pref_value = getattr(config_values, pref.name)
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
print(f"{str(config_type.name)}.{camel_name}: {str(pref_value)}")
|
||||
logging.debug(
|
||||
f"{str(config_type.name)}.{camel_name}: {str(pref_value)}"
|
||||
@@ -127,25 +131,46 @@ def getPref(node, comp_name):
|
||||
|
||||
def splitCompoundName(comp_name):
|
||||
"""Split compound (dot separated) preference name into parts"""
|
||||
name = comp_name.split(".", 1)
|
||||
if len(name) != 2:
|
||||
name = comp_name.split(".")
|
||||
if len(name) < 2:
|
||||
name[0] = comp_name
|
||||
name.append(comp_name)
|
||||
return name
|
||||
|
||||
def traverseConfig(config_root, config, interface_config):
|
||||
"""Iterate through current config level preferences and either traverse deeper if preference is a dict or set preference"""
|
||||
snake_name = meshtastic.util.camel_to_snake(config_root)
|
||||
for pref in config:
|
||||
pref_name = f"{snake_name}.{pref}"
|
||||
if isinstance(config[pref], dict):
|
||||
traverseConfig(pref_name, config[pref], interface_config)
|
||||
else:
|
||||
setPref(
|
||||
interface_config,
|
||||
pref_name,
|
||||
str(config[pref])
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def setPref(config, comp_name, valStr) -> bool:
|
||||
"""Set a channel or preferences value"""
|
||||
|
||||
name = splitCompoundName(comp_name)
|
||||
|
||||
snake_name = meshtastic.util.camel_to_snake(name[1])
|
||||
camel_name = meshtastic.util.snake_to_camel(name[1])
|
||||
snake_name = meshtastic.util.camel_to_snake(name[-1])
|
||||
camel_name = meshtastic.util.snake_to_camel(name[-1])
|
||||
logging.debug(f"snake_name:{snake_name}")
|
||||
logging.debug(f"camel_name:{camel_name}")
|
||||
|
||||
objDesc = config.DESCRIPTOR
|
||||
config_part = config
|
||||
config_type = objDesc.fields_by_name.get(name[0])
|
||||
if config_type and config_type.message_type is not None:
|
||||
for name_part in name[1:-1]:
|
||||
part_snake_name = meshtastic.util.camel_to_snake((name_part))
|
||||
config_part = getattr(config, config_type.name)
|
||||
config_type = config_type.message_type.fields_by_name.get(part_snake_name)
|
||||
pref = None
|
||||
if config_type and config_type.message_type is not None:
|
||||
pref = config_type.message_type.fields_by_name.get(snake_name)
|
||||
@@ -171,7 +196,7 @@ def setPref(config, comp_name, valStr) -> bool:
|
||||
if e:
|
||||
val = e.number
|
||||
else:
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
print(
|
||||
f"{name[0]}.{camel_name} does not have an enum called {val}, so you can not set it."
|
||||
)
|
||||
@@ -192,25 +217,26 @@ def setPref(config, comp_name, valStr) -> bool:
|
||||
if snake_name != "ignore_incoming":
|
||||
try:
|
||||
if config_type.message_type is not None:
|
||||
config_values = getattr(config, config_type.name)
|
||||
config_values = getattr(config_part, config_type.name)
|
||||
setattr(config_values, pref.name, val)
|
||||
else:
|
||||
setattr(config, snake_name, val)
|
||||
setattr(config_part, snake_name, val)
|
||||
except TypeError:
|
||||
# The setter didn't like our arg type guess try again as a string
|
||||
config_values = getattr(config, config_type.name)
|
||||
config_values = getattr(config_part, config_type.name)
|
||||
setattr(config_values, pref.name, valStr)
|
||||
else:
|
||||
config_values = getattr(config, config_type.name)
|
||||
if val == 0:
|
||||
# clear values
|
||||
print("Clearing ignore_incoming list")
|
||||
del config_type.message_type.ignore_incoming[:]
|
||||
del config_values.ignore_incoming[:]
|
||||
else:
|
||||
print(f"Adding '{val}' to the ignore_incoming list")
|
||||
config_type.message_type.ignore_incoming.extend([val])
|
||||
config_values.ignore_incoming.extend([int(valStr)])
|
||||
|
||||
prefix = f"{name[0]}." if config_type.message_type is not None else ""
|
||||
if Globals.getInstance().get_camel_case():
|
||||
prefix = f"{'.'.join(name[0:-1])}." if config_type.message_type is not None else ""
|
||||
if mt_config.camel_case:
|
||||
print(f"Set {prefix}{camel_name} to {valStr}")
|
||||
else:
|
||||
print(f"Set {prefix}{snake_name} to {valStr}")
|
||||
@@ -225,41 +251,55 @@ def onConnected(interface):
|
||||
False # Should we wait for an acknowledgment if we send to a remote node?
|
||||
)
|
||||
try:
|
||||
our_globals = Globals.getInstance()
|
||||
args = our_globals.get_args()
|
||||
args = mt_config.args
|
||||
|
||||
# do not print this line if we are exporting the config
|
||||
if not args.export_config:
|
||||
print("Connected to radio")
|
||||
|
||||
if args.setlat or args.setlon or args.setalt:
|
||||
if args.remove_position:
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
print("Setting positions of remote nodes is not supported.")
|
||||
return
|
||||
closeNow = True
|
||||
print("Removing fixed position and disabling fixed position setting")
|
||||
interface.localNode.removeFixedPosition()
|
||||
elif args.setlat or args.setlon or args.setalt:
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
print("Setting latitude, longitude, and altitude of remote nodes is not supported.")
|
||||
return
|
||||
closeNow = True
|
||||
|
||||
alt = 0
|
||||
lat = 0.0
|
||||
lon = 0.0
|
||||
# TODO: use getNode(args.dest) to be able to set it for a remote node
|
||||
localConfig = interface.localNode.localConfig
|
||||
lat = 0
|
||||
lon = 0
|
||||
if args.setalt:
|
||||
alt = int(args.setalt)
|
||||
localConfig.position.fixed_position = True
|
||||
print(f"Fixing altitude at {alt} meters")
|
||||
if args.setlat:
|
||||
lat = float(args.setlat)
|
||||
localConfig.position.fixed_position = True
|
||||
try:
|
||||
lat = int(args.setlat)
|
||||
except ValueError:
|
||||
lat = float(args.setlat)
|
||||
print(f"Fixing latitude at {lat} degrees")
|
||||
if args.setlon:
|
||||
lon = float(args.setlon)
|
||||
localConfig.position.fixed_position = True
|
||||
try:
|
||||
lon = int(args.setlon)
|
||||
except ValueError:
|
||||
lon = float(args.setlon)
|
||||
print(f"Fixing longitude at {lon} degrees")
|
||||
|
||||
print("Setting device position")
|
||||
print("Setting device position and enabling fixed position setting")
|
||||
# can include lat/long/alt etc: latitude = 37.5, longitude = -122.1
|
||||
interface.sendPosition(lat, lon, alt)
|
||||
interface.localNode.writeConfig("position")
|
||||
interface.localNode.setFixedPosition(lat, lon, alt)
|
||||
elif not args.no_time:
|
||||
# We normally provide a current time to the mesh when we connect
|
||||
interface.sendPosition()
|
||||
if interface.localNode.nodeNum in interface.nodesByNum and "position" in interface.nodesByNum[interface.localNode.nodeNum]:
|
||||
# send the same position the node already knows, just to update time
|
||||
position = interface.nodesByNum[interface.localNode.nodeNum]["position"]
|
||||
interface.sendPosition(position.get("latitude", 0.0), position.get("longitude", 0.0), position.get("altitude", 0.0))
|
||||
else:
|
||||
interface.sendPosition()
|
||||
|
||||
if args.set_owner:
|
||||
closeNow = True
|
||||
@@ -343,6 +383,11 @@ def onConnected(interface):
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False).rebootOTA()
|
||||
|
||||
if args.enter_dfu:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False).enterDFUMode()
|
||||
|
||||
if args.shutdown:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
@@ -365,6 +410,11 @@ def onConnected(interface):
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False).factoryReset()
|
||||
|
||||
if args.remove_node:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False).removeNode(args.remove_node)
|
||||
|
||||
if args.reset_nodedb:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
@@ -372,12 +422,8 @@ def onConnected(interface):
|
||||
|
||||
if args.sendtext:
|
||||
closeNow = True
|
||||
channelIndex = 0
|
||||
if args.ch_index is not None:
|
||||
channelIndex = int(args.ch_index)
|
||||
ch = interface.localNode.getChannelByChannelIndex(channelIndex)
|
||||
logging.debug(f"ch:{ch}")
|
||||
if ch and ch.role != channel_pb2.Channel.Role.DISABLED:
|
||||
channelIndex = mt_config.channel_index or 0
|
||||
if checkChannel(interface, channelIndex):
|
||||
print(
|
||||
f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}"
|
||||
)
|
||||
@@ -397,15 +443,28 @@ def onConnected(interface):
|
||||
loraConfig = getattr(interface.localNode.localConfig, "lora")
|
||||
hopLimit = getattr(loraConfig, "hop_limit")
|
||||
dest = str(args.traceroute)
|
||||
print(f"Sending traceroute request to {dest} (this could take a while)")
|
||||
interface.sendTraceRoute(dest, hopLimit)
|
||||
channelIndex = mt_config.channel_index or 0
|
||||
if checkChannel(interface, channelIndex):
|
||||
print(f"Sending traceroute request to {dest} on channelIndex:{channelIndex} (this could take a while)")
|
||||
interface.sendTraceRoute(dest, hopLimit, channelIndex=channelIndex)
|
||||
|
||||
if args.request_telemetry:
|
||||
if args.dest == BROADCAST_ADDR:
|
||||
meshtastic.util.our_exit("Warning: Must use a destination node ID.")
|
||||
else:
|
||||
print(f"Sending telemetry request to {args.dest} (this could take a while)")
|
||||
interface.sendTelemetry(destinationId=args.dest, wantResponse=True)
|
||||
channelIndex = mt_config.channel_index or 0
|
||||
if checkChannel(interface, channelIndex):
|
||||
print(f"Sending telemetry request to {args.dest} on channelIndex:{channelIndex} (this could take a while)")
|
||||
interface.sendTelemetry(destinationId=args.dest, wantResponse=True, channelIndex=channelIndex)
|
||||
|
||||
if args.request_position:
|
||||
if args.dest == BROADCAST_ADDR:
|
||||
meshtastic.util.our_exit("Warning: Must use a destination node ID.")
|
||||
else:
|
||||
channelIndex = mt_config.channel_index or 0
|
||||
if checkChannel(interface, channelIndex):
|
||||
print(f"Sending position request to {args.dest} on channelIndex:{channelIndex} (this could take a while)")
|
||||
interface.sendPosition(destinationId=args.dest, wantResponse=True, channelIndex=channelIndex)
|
||||
|
||||
if args.gpio_wrb or args.gpio_rd or args.gpio_watch:
|
||||
if args.dest == BROADCAST_ADDR:
|
||||
@@ -472,7 +531,7 @@ def onConnected(interface):
|
||||
print("Writing modified preferences to device")
|
||||
node.writeConfig(field)
|
||||
else:
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
print(
|
||||
f"{node.localConfig.__class__.__name__} and {node.moduleConfig.__class__.__name__} do not have an attribute {pref[0]}."
|
||||
)
|
||||
@@ -529,15 +588,15 @@ def onConnected(interface):
|
||||
localConfig = interface.localNode.localConfig
|
||||
|
||||
if "alt" in configuration["location"]:
|
||||
alt = int(configuration["location"]["alt"])
|
||||
alt = int(configuration["location"]["alt"] or 0)
|
||||
localConfig.position.fixed_position = True
|
||||
print(f"Fixing altitude at {alt} meters")
|
||||
if "lat" in configuration["location"]:
|
||||
lat = float(configuration["location"]["lat"])
|
||||
lat = float(configuration["location"]["lat"] or 0)
|
||||
localConfig.position.fixed_position = True
|
||||
print(f"Fixing latitude at {lat} degrees")
|
||||
if "lon" in configuration["location"]:
|
||||
lon = float(configuration["location"]["lon"])
|
||||
lon = float(configuration["location"]["lon"] or 0)
|
||||
localConfig.position.fixed_position = True
|
||||
print(f"Fixing longitude at {lon} degrees")
|
||||
print("Setting device position")
|
||||
@@ -547,12 +606,7 @@ def onConnected(interface):
|
||||
if "config" in configuration:
|
||||
localConfig = interface.getNode(args.dest).localConfig
|
||||
for section in configuration["config"]:
|
||||
for pref in configuration["config"][section]:
|
||||
setPref(
|
||||
localConfig,
|
||||
f"{meshtastic.util.camel_to_snake(section)}.{pref}",
|
||||
str(configuration["config"][section][pref]),
|
||||
)
|
||||
traverseConfig(section, configuration["config"][section], localConfig)
|
||||
interface.getNode(args.dest).writeConfig(
|
||||
meshtastic.util.camel_to_snake(section)
|
||||
)
|
||||
@@ -560,12 +614,7 @@ def onConnected(interface):
|
||||
if "module_config" in configuration:
|
||||
moduleConfig = interface.getNode(args.dest).moduleConfig
|
||||
for section in configuration["module_config"]:
|
||||
for pref in configuration["module_config"][section]:
|
||||
setPref(
|
||||
moduleConfig,
|
||||
f"{meshtastic.util.camel_to_snake(section)}.{pref}",
|
||||
str(configuration["module_config"][section][pref]),
|
||||
)
|
||||
traverseConfig(section, configuration["module_config"][section], moduleConfig)
|
||||
interface.getNode(args.dest).writeConfig(
|
||||
meshtastic.util.camel_to_snake(section)
|
||||
)
|
||||
@@ -574,6 +623,9 @@ def onConnected(interface):
|
||||
print("Writing modified configuration to device")
|
||||
|
||||
if args.export_config:
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
print("Exporting configuration of remote nodes is not supported.")
|
||||
return
|
||||
# export the configuration (the opposite of '--configure')
|
||||
closeNow = True
|
||||
export_config(interface)
|
||||
@@ -585,7 +637,7 @@ def onConnected(interface):
|
||||
# handle changing channels
|
||||
|
||||
if args.ch_add:
|
||||
channelIndex = our_globals.get_channel_index()
|
||||
channelIndex = mt_config.channel_index
|
||||
if channelIndex is not None:
|
||||
# Since we set the channel index after adding a channel, don't allow --ch-index
|
||||
meshtastic.util.our_exit(
|
||||
@@ -616,12 +668,12 @@ def onConnected(interface):
|
||||
n.writeChannel(ch.index)
|
||||
if channelIndex is None:
|
||||
print(f"Setting newly-added channel's {ch.index} as '--ch-index' for further modifications")
|
||||
our_globals.set_channel_index(ch.index)
|
||||
mt_config.channel_index = ch.index
|
||||
|
||||
if args.ch_del:
|
||||
closeNow = True
|
||||
|
||||
channelIndex = our_globals.get_channel_index()
|
||||
channelIndex = mt_config.channel_index
|
||||
if channelIndex is None:
|
||||
meshtastic.util.our_exit(
|
||||
"Warning: Need to specify '--ch-index' for '--ch-del'.", 1
|
||||
@@ -637,7 +689,7 @@ def onConnected(interface):
|
||||
|
||||
def setSimpleConfig(modem_preset):
|
||||
"""Set one of the simple modem_config"""
|
||||
channelIndex = our_globals.get_channel_index()
|
||||
channelIndex = mt_config.channel_index
|
||||
if channelIndex is not None and channelIndex > 0:
|
||||
meshtastic.util.our_exit(
|
||||
"Warning: Cannot set modem preset for non-primary channel", 1
|
||||
@@ -672,12 +724,16 @@ def onConnected(interface):
|
||||
if args.ch_set or args.ch_enable or args.ch_disable:
|
||||
closeNow = True
|
||||
|
||||
channelIndex = our_globals.get_channel_index()
|
||||
channelIndex = mt_config.channel_index
|
||||
if channelIndex is None:
|
||||
meshtastic.util.our_exit("Warning: Need to specify '--ch-index'.", 1)
|
||||
ch = interface.getNode(args.dest).channels[channelIndex]
|
||||
|
||||
if args.ch_enable or args.ch_disable:
|
||||
print(
|
||||
"Warning: --ch-enable and --ch-disable can produce noncontiguous channels, "
|
||||
"which can cause errors in some clients. Whenever possible, use --ch-add and --ch-del instead."
|
||||
)
|
||||
if channelIndex == 0:
|
||||
meshtastic.util.our_exit(
|
||||
"Warning: Cannot enable/disable PRIMARY channel."
|
||||
@@ -776,10 +832,14 @@ def onConnected(interface):
|
||||
return
|
||||
interface.showNodes()
|
||||
|
||||
if args.qr:
|
||||
if args.qr or args.qr_all:
|
||||
closeNow = True
|
||||
url = interface.localNode.getURL(includeAll=False)
|
||||
print(f"Primary channel URL {url}")
|
||||
url = interface.getNode(args.dest, True).getURL(includeAll=args.qr_all)
|
||||
if args.qr_all:
|
||||
urldesc = "Complete URL (includes all channels)"
|
||||
else:
|
||||
urldesc = "Primary channel URL"
|
||||
print(f"{urldesc}: {url}")
|
||||
qr = pyqrcode.create(url)
|
||||
print(qr.terminal())
|
||||
|
||||
@@ -788,6 +848,9 @@ def onConnected(interface):
|
||||
|
||||
have_tunnel = platform.system() == "Linux"
|
||||
if have_tunnel and args.tunnel:
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
print("A tunnel can only be created using the local node.")
|
||||
return
|
||||
# pylint: disable=C0415
|
||||
from . import tunnel
|
||||
|
||||
@@ -807,6 +870,10 @@ def onConnected(interface):
|
||||
)
|
||||
interface.getNode(args.dest, False).iface.waitForAckNak()
|
||||
|
||||
if args.wait_to_disconnect:
|
||||
print(f"Waiting {args.wait_to_disconnect} seconds before disconnecting" )
|
||||
time.sleep(int(args.wait_to_disconnect))
|
||||
|
||||
# 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:
|
||||
interface.close() # after running command then exit
|
||||
@@ -827,7 +894,7 @@ def printConfig(config):
|
||||
names = []
|
||||
for field in config.message_type.fields:
|
||||
tmp_name = f"{config_section.name}.{field.name}"
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
tmp_name = meshtastic.util.snake_to_camel(tmp_name)
|
||||
names.append(tmp_name)
|
||||
for temp_name in sorted(names):
|
||||
@@ -872,23 +939,26 @@ def export_config(interface):
|
||||
if owner_short:
|
||||
configObj["owner_short"] = owner_short
|
||||
if channel_url:
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
configObj["channelUrl"] = channel_url
|
||||
else:
|
||||
configObj["channel_url"] = channel_url
|
||||
if lat or lon or alt:
|
||||
configObj["location"] = {"lat": lat, "lon": lon, "alt": alt}
|
||||
# lat and lon don't make much sense without the other (so fill with 0s), and alt isn't meaningful without both
|
||||
if lat or lon:
|
||||
configObj["location"] = {"lat": lat or float(0), "lon": lon or float(0)}
|
||||
if alt:
|
||||
configObj["location"]["alt"] = alt
|
||||
|
||||
config = MessageToDict(interface.localNode.localConfig)
|
||||
if config:
|
||||
# Convert inner keys to correct snake/camelCase
|
||||
prefs = {}
|
||||
for pref in config:
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
prefs[meshtastic.util.snake_to_camel(pref)] = config[pref]
|
||||
else:
|
||||
prefs[pref] = config[pref]
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
configObj["config"] = config
|
||||
else:
|
||||
configObj["config"] = config
|
||||
@@ -900,7 +970,7 @@ def export_config(interface):
|
||||
for pref in module_config:
|
||||
if len(module_config[pref]) > 0:
|
||||
prefs[pref] = module_config[pref]
|
||||
if Globals.getInstance().get_camel_case():
|
||||
if mt_config.camel_case:
|
||||
configObj["module_config"] = prefs
|
||||
else:
|
||||
configObj["module_config"] = prefs
|
||||
@@ -914,9 +984,8 @@ 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()
|
||||
args = mt_config.args
|
||||
parser = mt_config.parser
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if (args.debug or args.listen) else logging.INFO,
|
||||
format="%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s",
|
||||
@@ -932,7 +1001,7 @@ def common():
|
||||
|
||||
if args.ch_index is not None:
|
||||
channelIndex = int(args.ch_index)
|
||||
our_globals.set_channel_index(channelIndex)
|
||||
mt_config.channel_index = channelIndex
|
||||
|
||||
if not args.dest:
|
||||
args.dest = BROADCAST_ADDR
|
||||
@@ -967,7 +1036,7 @@ def common():
|
||||
# Note: using "line buffering"
|
||||
# pylint: disable=R1732
|
||||
logfile = open(args.seriallog, "w+", buffering=1, encoding="utf8")
|
||||
our_globals.set_logfile(logfile)
|
||||
mt_config.logfile = logfile
|
||||
|
||||
subscribe()
|
||||
if args.ble_scan:
|
||||
@@ -981,11 +1050,11 @@ def common():
|
||||
meshtastic.util.our_exit("BLE scan finished", 0)
|
||||
return
|
||||
elif args.ble:
|
||||
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto)
|
||||
client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes)
|
||||
elif args.host:
|
||||
try:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
args.host, debugOut=logfile, noProto=args.noproto
|
||||
args.host, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
|
||||
)
|
||||
except Exception as ex:
|
||||
meshtastic.util.our_exit(
|
||||
@@ -994,7 +1063,7 @@ def common():
|
||||
else:
|
||||
try:
|
||||
client = meshtastic.serial_interface.SerialInterface(
|
||||
args.port, debugOut=logfile, noProto=args.noproto
|
||||
args.port, debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
|
||||
)
|
||||
except PermissionError as ex:
|
||||
username = os.getlogin()
|
||||
@@ -1009,7 +1078,7 @@ def common():
|
||||
if client.devPath is None:
|
||||
try:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
"localhost", debugOut=logfile, noProto=args.noproto
|
||||
"localhost", debugOut=logfile, noProto=args.noproto, noNodes=args.no_nodes
|
||||
)
|
||||
except Exception as ex:
|
||||
meshtastic.util.our_exit(
|
||||
@@ -1058,9 +1127,8 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse
|
||||
|
||||
def initParser():
|
||||
"""Initialize the command line argument parsing."""
|
||||
our_globals = Globals.getInstance()
|
||||
parser = our_globals.get_parser()
|
||||
args = our_globals.get_args()
|
||||
parser = mt_config.parser
|
||||
args = mt_config.args
|
||||
|
||||
# The "Help" group includes the help option and other informational stuff about the CLI itself
|
||||
outerHelpGroup = parser.add_argument_group('Help')
|
||||
@@ -1128,7 +1196,16 @@ def initParser():
|
||||
|
||||
group.add_argument(
|
||||
"--qr",
|
||||
help="Display the QR code that corresponds to the current channel",
|
||||
help=(
|
||||
"Display a QR code for the node's primary channel (or all channels with --qr-all). "
|
||||
"Also shows the shareable channel URL."
|
||||
),
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--qr-all",
|
||||
help="Display a QR code and URL for all of the node's channels.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
@@ -1281,9 +1358,17 @@ def initParser():
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--request-telemetry",
|
||||
"--request-telemetry",
|
||||
help="Request telemetry from a node. "
|
||||
"You need pass the destination ID as argument with '--dest'. "
|
||||
"You need to pass the destination ID as argument with '--dest'. "
|
||||
"For repeaters, the nodeNum is required.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--request-position",
|
||||
help="Request the position from a node. "
|
||||
"You need to pass the destination ID as an argument with '--dest'. "
|
||||
"For repeaters, the nodeNum is required.",
|
||||
action="store_true",
|
||||
)
|
||||
@@ -1300,7 +1385,13 @@ def initParser():
|
||||
|
||||
group.add_argument(
|
||||
"--reboot-ota",
|
||||
help="Tell the destination node to reboot into factory firmware",
|
||||
help="Tell the destination node to reboot into factory firmware (ESP32)",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--enter-dfu",
|
||||
help="Tell the destination node to enter DFU mode (NRF52)",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
@@ -1332,9 +1423,13 @@ def initParser():
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--remove-node",
|
||||
help="Tell the destination node to remove a specific node from its DB, by node number or ID"
|
||||
)
|
||||
group.add_argument(
|
||||
"--reset-nodedb",
|
||||
help="Tell the destination node clear its list of nodes",
|
||||
help="Tell the destination node to clear its list of nodes",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
@@ -1358,12 +1453,32 @@ def initParser():
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument("--setalt", help="Set device altitude in meters (allows use without GPS)")
|
||||
|
||||
group.add_argument("--setlat", help="Set device latitude (allows use without GPS)")
|
||||
group.add_argument(
|
||||
"--no-nodes",
|
||||
help="Request that the node not send node info to the client. "
|
||||
"Will break things that depend on the nodedb, but will speed up startup. Requires 2.3.11+ firmware.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--setlon", help="Set device longitude (allows use without GPS)"
|
||||
"--setalt",
|
||||
help="Set device altitude in meters (allows use without GPS), and enable fixed position.",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--setlat",
|
||||
help="Set device latitude (allows use without GPS), and enable fixed position. Accepts a decimal value or an integer premultiplied by 1e7.",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--setlon",
|
||||
help="Set device longitude (allows use without GPS), and enable fixed position. Accepts a decimal value or an integer premultiplied by 1e7.",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--remove-position",
|
||||
help="Clear any existing fixed position and disable fixed position.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
@@ -1391,6 +1506,14 @@ def initParser():
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--wait-to-disconnect",
|
||||
help="How many seconds to wait before disconnecting from the device.",
|
||||
const="5",
|
||||
nargs="?",
|
||||
action="store",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--noproto",
|
||||
help="Don't start the API, just function as a dumb serial terminal.",
|
||||
@@ -1422,34 +1545,32 @@ def initParser():
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
our_globals.set_args(args)
|
||||
our_globals.set_parser(parser)
|
||||
mt_config.args = args
|
||||
mt_config.parser = parser
|
||||
|
||||
|
||||
def main():
|
||||
"""Perform command line meshtastic operations"""
|
||||
our_globals = Globals.getInstance()
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
epilog="If no connection arguments are specified, we search for a compatible serial device, "
|
||||
"and if none is found, then attempt a TCP connection to localhost.")
|
||||
our_globals.set_parser(parser)
|
||||
mt_config.parser = parser
|
||||
initParser()
|
||||
common()
|
||||
logfile = our_globals.get_logfile()
|
||||
logfile = mt_config.logfile
|
||||
if logfile:
|
||||
logfile.close()
|
||||
|
||||
|
||||
def tunnelMain():
|
||||
"""Run a meshtastic IP tunnel"""
|
||||
our_globals = Globals.getInstance()
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
our_globals.set_parser(parser)
|
||||
mt_config.parser = parser
|
||||
initParser()
|
||||
args = our_globals.get_args()
|
||||
args = mt_config.args
|
||||
args.tunnel = True
|
||||
our_globals.set_args(args)
|
||||
mt_config.args = args
|
||||
common()
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15meshtastic/atak.proto\x12\nmeshtastic\"\xe6\x01\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12$\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x13.meshtastic.Contact\x12 \n\x05group\x18\x03 \x01(\x0b\x32\x11.meshtastic.Group\x12\"\n\x06status\x18\x04 \x01(\x0b\x32\x12.meshtastic.Status\x12\x1e\n\x03pli\x18\x05 \x01(\x0b\x32\x0f.meshtastic.PLIH\x00\x12#\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x13.meshtastic.GeoChatH\x00\x42\x11\n\x0fpayload_variant\"2\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x05\n\x03_to\"M\n\x05Group\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.meshtastic.MemberRole\x12\x1e\n\x04team\x18\x02 \x01(\x0e\x32\x10.meshtastic.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42_\n\x13\x63om.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15meshtastic/atak.proto\x12\nmeshtastic\"\xe6\x01\n\tTAKPacket\x12\x15\n\ris_compressed\x18\x01 \x01(\x08\x12$\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x13.meshtastic.Contact\x12 \n\x05group\x18\x03 \x01(\x0b\x32\x11.meshtastic.Group\x12\"\n\x06status\x18\x04 \x01(\x0b\x32\x12.meshtastic.Status\x12\x1e\n\x03pli\x18\x05 \x01(\x0b\x32\x0f.meshtastic.PLIH\x00\x12#\n\x04\x63hat\x18\x06 \x01(\x0b\x32\x13.meshtastic.GeoChatH\x00\x42\x11\n\x0fpayload_variant\"\\\n\x07GeoChat\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x02to\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0bto_callsign\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x05\n\x03_toB\x0e\n\x0c_to_callsign\"M\n\x05Group\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.meshtastic.MemberRole\x12\x1e\n\x04team\x18\x02 \x01(\x0e\x32\x10.meshtastic.Team\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\r\"4\n\x07\x43ontact\x12\x10\n\x08\x63\x61llsign\x18\x01 \x01(\t\x12\x17\n\x0f\x64\x65vice_callsign\x18\x02 \x01(\t\"_\n\x03PLI\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\r\n\x05speed\x18\x04 \x01(\r\x12\x0e\n\x06\x63ourse\x18\x05 \x01(\r*\xc0\x01\n\x04Team\x12\x14\n\x10Unspecifed_Color\x10\x00\x12\t\n\x05White\x10\x01\x12\n\n\x06Yellow\x10\x02\x12\n\n\x06Orange\x10\x03\x12\x0b\n\x07Magenta\x10\x04\x12\x07\n\x03Red\x10\x05\x12\n\n\x06Maroon\x10\x06\x12\n\n\x06Purple\x10\x07\x12\r\n\tDark_Blue\x10\x08\x12\x08\n\x04\x42lue\x10\t\x12\x08\n\x04\x43yan\x10\n\x12\x08\n\x04Teal\x10\x0b\x12\t\n\x05Green\x10\x0c\x12\x0e\n\nDark_Green\x10\r\x12\t\n\x05\x42rown\x10\x0e*\x7f\n\nMemberRole\x12\x0e\n\nUnspecifed\x10\x00\x12\x0e\n\nTeamMember\x10\x01\x12\x0c\n\x08TeamLead\x10\x02\x12\x06\n\x02HQ\x10\x03\x12\n\n\x06Sniper\x10\x04\x12\t\n\x05Medic\x10\x05\x12\x13\n\x0f\x46orwardObserver\x10\x06\x12\x07\n\x03RTO\x10\x07\x12\x06\n\x02K9\x10\x08\x42_\n\x13\x63om.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.atak_pb2', globals())
|
||||
@@ -21,20 +21,20 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_TEAM._serialized_start=580
|
||||
_TEAM._serialized_end=772
|
||||
_MEMBERROLE._serialized_start=774
|
||||
_MEMBERROLE._serialized_end=901
|
||||
_TEAM._serialized_start=622
|
||||
_TEAM._serialized_end=814
|
||||
_MEMBERROLE._serialized_start=816
|
||||
_MEMBERROLE._serialized_end=943
|
||||
_TAKPACKET._serialized_start=38
|
||||
_TAKPACKET._serialized_end=268
|
||||
_GEOCHAT._serialized_start=270
|
||||
_GEOCHAT._serialized_end=320
|
||||
_GROUP._serialized_start=322
|
||||
_GROUP._serialized_end=399
|
||||
_STATUS._serialized_start=401
|
||||
_STATUS._serialized_end=426
|
||||
_CONTACT._serialized_start=428
|
||||
_CONTACT._serialized_end=480
|
||||
_PLI._serialized_start=482
|
||||
_PLI._serialized_end=577
|
||||
_GEOCHAT._serialized_end=362
|
||||
_GROUP._serialized_start=364
|
||||
_GROUP._serialized_end=441
|
||||
_STATUS._serialized_start=443
|
||||
_STATUS._serialized_end=468
|
||||
_CONTACT._serialized_start=470
|
||||
_CONTACT._serialized_end=522
|
||||
_PLI._serialized_start=524
|
||||
_PLI._serialized_end=619
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -302,6 +302,7 @@ class GeoChat(google.protobuf.message.Message):
|
||||
|
||||
MESSAGE_FIELD_NUMBER: builtins.int
|
||||
TO_FIELD_NUMBER: builtins.int
|
||||
TO_CALLSIGN_FIELD_NUMBER: builtins.int
|
||||
message: builtins.str
|
||||
"""
|
||||
The text message
|
||||
@@ -310,15 +311,23 @@ class GeoChat(google.protobuf.message.Message):
|
||||
"""
|
||||
Uid recipient of the message
|
||||
"""
|
||||
to_callsign: builtins.str
|
||||
"""
|
||||
Callsign of the recipient for the message
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
message: builtins.str = ...,
|
||||
to: builtins.str | None = ...,
|
||||
to_callsign: builtins.str | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing_extensions.Literal["_to", b"_to", "to", b"to"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["_to", b"_to", "message", b"message", "to", b"to"]) -> None: ...
|
||||
def HasField(self, field_name: typing_extensions.Literal["_to", b"_to", "_to_callsign", b"_to_callsign", "to", b"to", "to_callsign", b"to_callsign"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["_to", b"_to", "_to_callsign", b"_to_callsign", "message", b"message", "to", b"to", "to_callsign", b"to_callsign"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing_extensions.Literal["_to", b"_to"]) -> typing_extensions.Literal["to"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing_extensions.Literal["_to_callsign", b"_to_callsign"]) -> typing_extensions.Literal["to_callsign"] | None: ...
|
||||
|
||||
global___GeoChat = GeoChat
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import time
|
||||
import struct
|
||||
import asyncio
|
||||
from threading import Thread, Event
|
||||
from typing import Optional
|
||||
|
||||
from bleak import BleakScanner, BleakClient
|
||||
|
||||
from meshtastic.mesh_interface import MeshInterface
|
||||
@@ -30,7 +32,7 @@ class BLEInterface(MeshInterface):
|
||||
MESH = False
|
||||
|
||||
|
||||
def __init__(self, address, noProto = False, debugOut = None):
|
||||
def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False):
|
||||
self.state = BLEInterface.BLEState()
|
||||
|
||||
if not address:
|
||||
@@ -58,7 +60,7 @@ class BLEInterface(MeshInterface):
|
||||
return
|
||||
|
||||
logging.debug("Mesh init starting")
|
||||
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto)
|
||||
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes)
|
||||
self._startConfig()
|
||||
if not self.noProto:
|
||||
self._waitConnected(timeout = 60.0)
|
||||
|
||||
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18meshtastic/channel.proto\x12\nmeshtastic\"\xb8\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x0b\n\x03psk\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12\x33\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32\x1a.meshtastic.ModuleSettings\",\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\"\xa1\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12-\n\x08settings\x18\x02 \x01(\x0b\x32\x1b.meshtastic.ChannelSettings\x12&\n\x04role\x18\x03 \x01(\x0e\x32\x18.meshtastic.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x62\n\x13\x63om.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18meshtastic/channel.proto\x12\nmeshtastic\"\xb8\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x0b\n\x03psk\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12\x33\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32\x1a.meshtastic.ModuleSettings\"E\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x17\n\x0fis_client_muted\x18\x02 \x01(\x08\"\xa1\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12-\n\x08settings\x18\x02 \x01(\x0b\x32\x1b.meshtastic.ChannelSettings\x12&\n\x04role\x18\x03 \x01(\x0e\x32\x18.meshtastic.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x62\n\x13\x63om.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.channel_pb2', globals())
|
||||
@@ -26,9 +26,9 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
_CHANNELSETTINGS._serialized_start=41
|
||||
_CHANNELSETTINGS._serialized_end=225
|
||||
_MODULESETTINGS._serialized_start=227
|
||||
_MODULESETTINGS._serialized_end=271
|
||||
_CHANNEL._serialized_start=274
|
||||
_CHANNEL._serialized_end=435
|
||||
_CHANNEL_ROLE._serialized_start=387
|
||||
_CHANNEL_ROLE._serialized_end=435
|
||||
_MODULESETTINGS._serialized_end=296
|
||||
_CHANNEL._serialized_start=299
|
||||
_CHANNEL._serialized_end=460
|
||||
_CHANNEL_ROLE._serialized_start=412
|
||||
_CHANNEL_ROLE._serialized_end=460
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -125,16 +125,23 @@ class ModuleSettings(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
POSITION_PRECISION_FIELD_NUMBER: builtins.int
|
||||
IS_CLIENT_MUTED_FIELD_NUMBER: builtins.int
|
||||
position_precision: builtins.int
|
||||
"""
|
||||
Bits of precision for the location sent in position packets.
|
||||
"""
|
||||
is_client_muted: builtins.bool
|
||||
"""
|
||||
Controls whether or not the phone / clients should mute the current channel
|
||||
Useful for noisy public channels you don't necessarily want to disable
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
position_precision: builtins.int = ...,
|
||||
is_client_muted: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["position_precision", b"position_precision"]) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["is_client_muted", b"is_client_muted", "position_precision", b"position_precision"]) -> None: ...
|
||||
|
||||
global___ModuleSettings = ModuleSettings
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -245,6 +245,7 @@ class Config(google.protobuf.message.Message):
|
||||
IS_MANAGED_FIELD_NUMBER: builtins.int
|
||||
DISABLE_TRIPLE_CLICK_FIELD_NUMBER: builtins.int
|
||||
TZDEF_FIELD_NUMBER: builtins.int
|
||||
LED_HEARTBEAT_DISABLED_FIELD_NUMBER: builtins.int
|
||||
role: global___Config.DeviceConfig.Role.ValueType
|
||||
"""
|
||||
Sets the role of node
|
||||
@@ -294,6 +295,10 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv.
|
||||
"""
|
||||
led_heartbeat_disabled: builtins.bool
|
||||
"""
|
||||
If true, disable the default blinking LED (LED_PIN) behavior on the device
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -308,8 +313,9 @@ class Config(google.protobuf.message.Message):
|
||||
is_managed: builtins.bool = ...,
|
||||
disable_triple_click: builtins.bool = ...,
|
||||
tzdef: builtins.str = ...,
|
||||
led_heartbeat_disabled: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "debug_log_enabled", b"debug_log_enabled", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "debug_log_enabled", b"debug_log_enabled", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "led_heartbeat_disabled", b"led_heartbeat_disabled", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ...
|
||||
|
||||
@typing_extensions.final
|
||||
class PositionConfig(google.protobuf.message.Message):
|
||||
@@ -575,27 +581,25 @@ class Config(google.protobuf.message.Message):
|
||||
DEVICE_BATTERY_INA_ADDRESS_FIELD_NUMBER: builtins.int
|
||||
is_power_saving: builtins.bool
|
||||
"""
|
||||
If set, we are powered from a low-current source (i.e. solar), so even if it looks like we have power flowing in
|
||||
we should try to minimize power consumption as much as possible.
|
||||
YOU DO NOT NEED TO SET THIS IF YOU'VE set is_router (it is implied in that case).
|
||||
Advanced Option
|
||||
Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio.
|
||||
Don't use this setting if you want to use your device with the phone apps or are using a device without a user button.
|
||||
Technical Details: Works for ESP32 devices and NRF52 devices in the Sensor or Tracker roles
|
||||
"""
|
||||
on_battery_shutdown_after_secs: builtins.int
|
||||
"""
|
||||
If non-zero, the device will fully power off this many seconds after external power is removed.
|
||||
Description: If non-zero, the device will fully power off this many seconds after external power is removed.
|
||||
"""
|
||||
adc_multiplier_override: builtins.float
|
||||
"""
|
||||
Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k)
|
||||
Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation.
|
||||
Should be set to floating point value between 2 and 4
|
||||
Fixes issues on Heltec v2
|
||||
https://meshtastic.org/docs/configuration/radio/power/#adc-multiplier-override
|
||||
Should be set to floating point value between 2 and 6
|
||||
"""
|
||||
wait_bluetooth_secs: builtins.int
|
||||
"""
|
||||
Wait Bluetooth Seconds
|
||||
The number of seconds for to wait before turning off BLE in No Bluetooth states
|
||||
0 for default of 1 minute
|
||||
Description: The number of seconds for to wait before turning off BLE in No Bluetooth states
|
||||
Technical Details: ESP32 Only 0 for default of 1 minute
|
||||
"""
|
||||
sds_secs: builtins.int
|
||||
"""
|
||||
@@ -606,16 +610,13 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
ls_secs: builtins.int
|
||||
"""
|
||||
Light Sleep Seconds
|
||||
In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on
|
||||
ESP32 Only
|
||||
0 for default of 300
|
||||
Description: In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on
|
||||
Technical Details: ESP32 Only 0 for default of 300
|
||||
"""
|
||||
min_wake_secs: builtins.int
|
||||
"""
|
||||
Minimum Wake Seconds
|
||||
While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value
|
||||
0 for default of 10 seconds
|
||||
Description: While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value
|
||||
Technical Details: ESP32 Only 0 for default of 10 seconds
|
||||
"""
|
||||
device_battery_ina_address: builtins.int
|
||||
"""
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
"""Globals singleton class.
|
||||
|
||||
Instead of using a global, stuff your variables in this "trash can".
|
||||
This is not much better than using python's globals, but it allows
|
||||
us to better test meshtastic. Plus, there are some weird python
|
||||
global issues/gotcha that we can hopefully avoid by using this
|
||||
class instead.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class Globals:
|
||||
"""Globals class is a Singleton."""
|
||||
|
||||
__instance = None
|
||||
|
||||
@staticmethod
|
||||
def getInstance():
|
||||
"""Get an instance of the Globals class."""
|
||||
if Globals.__instance is None:
|
||||
Globals()
|
||||
return Globals.__instance
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor for the Globals CLass"""
|
||||
if Globals.__instance is not None:
|
||||
raise Exception("This class is a singleton") # pylint: disable=W0719
|
||||
else:
|
||||
Globals.__instance = self
|
||||
self.args = None
|
||||
self.parser = None
|
||||
self.channel_index = None
|
||||
self.logfile = None
|
||||
self.tunnelInstance = None
|
||||
# TODO: to migrate to camel_case for v1.3 change this value to True
|
||||
self.camel_case = False
|
||||
|
||||
def reset(self):
|
||||
"""Reset all of our globals. If you add a member, add it to this method, too."""
|
||||
self.args = None
|
||||
self.parser = None
|
||||
self.channel_index = None
|
||||
self.logfile = None
|
||||
self.tunnelInstance = None
|
||||
# TODO: to migrate to camel_case for v1.3 change this value to True
|
||||
self.camel_case = False
|
||||
|
||||
# setters
|
||||
def set_args(self, args):
|
||||
"""Set the args"""
|
||||
self.args = args
|
||||
|
||||
def set_parser(self, parser):
|
||||
"""Set the parser"""
|
||||
self.parser = parser
|
||||
|
||||
def set_channel_index(self, channel_index):
|
||||
"""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
|
||||
|
||||
def set_camel_case(self):
|
||||
"""Force using camelCase for things like prefs/set/set"""
|
||||
self.camel_case = True
|
||||
|
||||
# getters
|
||||
def get_args(self):
|
||||
"""Get args"""
|
||||
return self.args
|
||||
|
||||
def get_parser(self):
|
||||
"""Get parser"""
|
||||
return self.parser
|
||||
|
||||
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
|
||||
|
||||
def get_camel_case(self):
|
||||
"""Get whether or not to use camelCase"""
|
||||
return self.camel_case
|
||||
@@ -9,9 +9,11 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
import google.protobuf.json_format
|
||||
import timeago # type: ignore[import-untyped]
|
||||
from pubsub import pub # type: ignore[import-untyped]
|
||||
from tabulate import tabulate
|
||||
|
||||
@@ -23,6 +25,7 @@ from meshtastic import (
|
||||
BROADCAST_ADDR,
|
||||
BROADCAST_NUM,
|
||||
LOCAL_ADDR,
|
||||
NODELESS_WANT_CONFIG_ID,
|
||||
ResponseHandler,
|
||||
protocols,
|
||||
publishingThread,
|
||||
@@ -38,7 +41,30 @@ from meshtastic.util import (
|
||||
)
|
||||
|
||||
|
||||
class MeshInterface:
|
||||
def _timeago(delta_secs: int) -> str:
|
||||
"""Convert a number of seconds in the past into a short, friendly string
|
||||
e.g. "now", "30 sec ago", "1 hour ago"
|
||||
Zero or negative intervals simply return "now"
|
||||
"""
|
||||
intervals = (
|
||||
("year", 60 * 60 * 24 * 365),
|
||||
("month", 60 * 60 * 24 * 30),
|
||||
("day", 60 * 60 * 24),
|
||||
("hour", 60 * 60),
|
||||
("min", 60),
|
||||
("sec", 1),
|
||||
)
|
||||
for name, interval_duration in intervals:
|
||||
if delta_secs < interval_duration:
|
||||
continue
|
||||
x = delta_secs // interval_duration
|
||||
plur = "s" if x > 1 else ""
|
||||
return f"{x} {name}{plur} ago"
|
||||
|
||||
return "now"
|
||||
|
||||
|
||||
class MeshInterface: # pylint: disable=R0902
|
||||
"""Interface class for meshtastic devices
|
||||
|
||||
Properties:
|
||||
@@ -54,35 +80,39 @@ class MeshInterface:
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
def __init__(self, debugOut=None, noProto=False):
|
||||
def __init__(self, debugOut=None, noProto: bool=False, noNodes: bool=False) -> None:
|
||||
"""Constructor
|
||||
|
||||
Keyword Arguments:
|
||||
noProto -- If True, don't try to run our protocol on the
|
||||
link - just be a dumb serial client.
|
||||
noNodes -- If True, instruct the node to not send its nodedb
|
||||
on startup, just other configuration information.
|
||||
"""
|
||||
self.debugOut = debugOut
|
||||
self.nodes = None # FIXME
|
||||
self.isConnected = threading.Event()
|
||||
self.noProto = noProto
|
||||
self.localNode = meshtastic.node.Node(self, -1) # We fixup nodenum later
|
||||
self.myInfo = None # We don't have device info yet
|
||||
self.metadata = None # We don't have device metadata yet
|
||||
self.responseHandlers = {} # A map from request ID to the handler
|
||||
self.nodes: Optional[Dict[str,Dict]] = None # FIXME
|
||||
self.isConnected: threading.Event = threading.Event()
|
||||
self.noProto: bool = noProto
|
||||
self.localNode: meshtastic.node.Node = meshtastic.node.Node(self, -1) # We fixup nodenum later
|
||||
self.myInfo: Optional[mesh_pb2.MyNodeInfo] = None # We don't have device info yet
|
||||
self.metadata: Optional[mesh_pb2.DeviceMetadata] = None # We don't have device metadata yet
|
||||
self.responseHandlers: Dict[int,ResponseHandler] = {} # A map from request ID to the handler
|
||||
self.failure = (
|
||||
None # If we've encountered a fatal exception it will be kept here
|
||||
)
|
||||
self._timeout = Timeout()
|
||||
self._acknowledgment = Acknowledgment()
|
||||
self.heartbeatTimer = None
|
||||
self._timeout: Timeout = Timeout()
|
||||
self._acknowledgment: Acknowledgment = Acknowledgment()
|
||||
self.heartbeatTimer: Optional[threading.Timer] = None
|
||||
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
|
||||
self.currentPacketId = random.randint(0, 0xFFFFFFFF)
|
||||
self.nodesByNum = None
|
||||
self.configId = None
|
||||
self.gotResponse = False # used in gpio read
|
||||
self.mask = None # used in gpio read and gpio watch
|
||||
self.queueStatus = None
|
||||
self.queue = collections.OrderedDict()
|
||||
self.currentPacketId: int = random.randint(0, 0xFFFFFFFF)
|
||||
self.nodesByNum: Optional[Dict[int, Dict]] = None
|
||||
self.noNodes: bool = noNodes
|
||||
self.configId: Optional[int] = NODELESS_WANT_CONFIG_ID if noNodes else None
|
||||
self.gotResponse: bool = False # used in gpio read
|
||||
self.mask: Optional[int] = None # used in gpio read and gpio watch
|
||||
self.queueStatus: Optional[mesh_pb2.QueueStatus] = None
|
||||
self.queue: collections.OrderedDict = collections.OrderedDict()
|
||||
self._localChannels = None
|
||||
|
||||
def close(self):
|
||||
"""Shutdown this interface"""
|
||||
@@ -103,7 +133,7 @@ class MeshInterface:
|
||||
logging.error(f"Traceback: {traceback}")
|
||||
self.close()
|
||||
|
||||
def showInfo(self, file=sys.stdout): # pylint: disable=W0613
|
||||
def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613
|
||||
"""Show human readable summary about this object"""
|
||||
owner = f"Owner: {self.getLongName()} ({self.getShortName()})"
|
||||
myinfo = ""
|
||||
@@ -131,39 +161,42 @@ class MeshInterface:
|
||||
# use id as dictionary key for correct json format in list of nodes
|
||||
nodeid = n2["user"]["id"]
|
||||
nodes[nodeid] = n2
|
||||
infos = owner + myinfo + metadata + mesh + json.dumps(nodes)
|
||||
infos = owner + myinfo + metadata + mesh + json.dumps(nodes, indent=2)
|
||||
print(infos)
|
||||
return infos
|
||||
|
||||
def showNodes(self, includeSelf=True, file=sys.stdout): # pylint: disable=W0613
|
||||
def showNodes(self, includeSelf: bool=True, file=sys.stdout) -> str: # pylint: disable=W0613
|
||||
"""Show table summary of nodes in mesh"""
|
||||
|
||||
def formatFloat(value, precision=2, unit=""):
|
||||
def formatFloat(value, precision=2, unit="") -> Optional[str]:
|
||||
"""Format a float value with precision."""
|
||||
return f"{value:.{precision}f}{unit}" if value else None
|
||||
|
||||
def getLH(ts):
|
||||
def getLH(ts) -> Optional[str]:
|
||||
"""Format last heard"""
|
||||
return (
|
||||
datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") if ts else None
|
||||
)
|
||||
|
||||
def getTimeAgo(ts):
|
||||
def getTimeAgo(ts) -> Optional[str]:
|
||||
"""Format how long ago have we heard from this node (aka timeago)."""
|
||||
return (
|
||||
timeago.format(datetime.fromtimestamp(ts), datetime.now())
|
||||
if ts
|
||||
else None
|
||||
)
|
||||
if ts is None:
|
||||
return None
|
||||
delta = datetime.now() - datetime.fromtimestamp(ts)
|
||||
delta_secs = int(delta.total_seconds())
|
||||
if delta_secs < 0:
|
||||
return None # not handling a timestamp from the future
|
||||
return _timeago(delta_secs)
|
||||
|
||||
rows = []
|
||||
rows: List[Dict[str, Any]] = []
|
||||
if self.nodesByNum:
|
||||
logging.debug(f"self.nodes:{self.nodes}")
|
||||
for node in self.nodesByNum.values():
|
||||
if not includeSelf and node["num"] == self.localNode.nodeNum:
|
||||
continue
|
||||
|
||||
row = {"N": 0, "User": f"UNK: {node['num']}", "ID": f"!{node['num']:08x}"}
|
||||
presumptive_id = f"!{node['num']:08x}"
|
||||
row = {"N": 0, "User": f"Meshtastic {presumptive_id[-4:]}", "ID": presumptive_id}
|
||||
|
||||
user = node.get("user")
|
||||
if user:
|
||||
@@ -172,6 +205,7 @@ class MeshInterface:
|
||||
"User": user.get("longName", "N/A"),
|
||||
"AKA": user.get("shortName", "N/A"),
|
||||
"ID": user["id"],
|
||||
"Hardware": user.get("hwModel", "UNSET")
|
||||
}
|
||||
)
|
||||
|
||||
@@ -208,7 +242,8 @@ class MeshInterface:
|
||||
row.update(
|
||||
{
|
||||
"SNR": formatFloat(node.get("snr"), 2, " dB"),
|
||||
"Channel": node.get("channel"),
|
||||
"Hops Away": node.get("hopsAway", "0/unknown"),
|
||||
"Channel": node.get("channel", 0),
|
||||
"LastHeard": getLH(node.get("lastHeard")),
|
||||
"Since": getTimeAgo(node.get("lastHeard")),
|
||||
}
|
||||
@@ -224,7 +259,7 @@ class MeshInterface:
|
||||
print(table)
|
||||
return table
|
||||
|
||||
def getNode(self, nodeId, requestChannels=True):
|
||||
def getNode(self, nodeId: str, requestChannels: bool=True) -> meshtastic.node.Node:
|
||||
"""Return a node object which contains device settings and channel info"""
|
||||
if nodeId in (LOCAL_ADDR, BROADCAST_ADDR):
|
||||
return self.localNode
|
||||
@@ -241,11 +276,11 @@ class MeshInterface:
|
||||
def sendText(
|
||||
self,
|
||||
text: str,
|
||||
destinationId=BROADCAST_ADDR,
|
||||
wantAck=False,
|
||||
wantResponse=False,
|
||||
onResponse=None,
|
||||
channelIndex=0,
|
||||
destinationId: Union[int, str]=BROADCAST_ADDR,
|
||||
wantAck: bool=False,
|
||||
wantResponse: bool=False,
|
||||
onResponse: Optional[Callable[[dict], Any]]=None,
|
||||
channelIndex: int=0,
|
||||
):
|
||||
"""Send a utf8 string to some other node, if the node has a display it
|
||||
will also be shown on the device.
|
||||
@@ -280,12 +315,13 @@ class MeshInterface:
|
||||
def sendData(
|
||||
self,
|
||||
data,
|
||||
destinationId=BROADCAST_ADDR,
|
||||
portNum=portnums_pb2.PortNum.PRIVATE_APP,
|
||||
wantAck=False,
|
||||
wantResponse=False,
|
||||
onResponse=None,
|
||||
channelIndex=0,
|
||||
destinationId: Union[int, str]=BROADCAST_ADDR,
|
||||
portNum: portnums_pb2.PortNum.ValueType=portnums_pb2.PortNum.PRIVATE_APP,
|
||||
wantAck: bool=False,
|
||||
wantResponse: bool=False,
|
||||
onResponse: Optional[Callable[[dict], Any]]=None,
|
||||
onResponseAckPermitted: bool=False,
|
||||
channelIndex: int=0,
|
||||
):
|
||||
"""Send a data packet to some other node
|
||||
|
||||
@@ -304,6 +340,10 @@ class MeshInterface:
|
||||
onResponse -- A closure of the form funct(packet), that will be
|
||||
called when a response packet arrives (or the transaction
|
||||
is NAKed due to non receipt)
|
||||
onResponseAckPermitted -- should the onResponse callback be called
|
||||
for regular ACKs (True) or just data responses & NAKs (False)
|
||||
Note that if the onResponse callback is called 'onAckNak' this
|
||||
will implicitly be true.
|
||||
channelIndex - channel number to use
|
||||
|
||||
Returns the sent packet. The id field will be populated in this packet
|
||||
@@ -334,19 +374,21 @@ class MeshInterface:
|
||||
meshPacket.id = self._generatePacketId()
|
||||
|
||||
if onResponse is not None:
|
||||
self._addResponseHandler(meshPacket.id, onResponse)
|
||||
logging.debug(f"Setting a response handler for requestId {meshPacket.id}")
|
||||
self._addResponseHandler(meshPacket.id, onResponse, ackPermitted=onResponseAckPermitted)
|
||||
p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck)
|
||||
return p
|
||||
|
||||
def sendPosition(
|
||||
self,
|
||||
latitude=0.0,
|
||||
longitude=0.0,
|
||||
altitude=0,
|
||||
timeSec=0,
|
||||
destinationId=BROADCAST_ADDR,
|
||||
wantAck=False,
|
||||
wantResponse=False,
|
||||
latitude: float=0.0,
|
||||
longitude: float=0.0,
|
||||
altitude: int=0,
|
||||
timeSec: int=0,
|
||||
destinationId: Union[int, str]=BROADCAST_ADDR,
|
||||
wantAck: bool=False,
|
||||
wantResponse: bool=False,
|
||||
channelIndex: int=0,
|
||||
):
|
||||
"""
|
||||
Send a position packet to some other node (normally a broadcast)
|
||||
@@ -373,19 +415,57 @@ class MeshInterface:
|
||||
logging.debug(f"p.altitude:{p.altitude}")
|
||||
|
||||
if timeSec == 0:
|
||||
timeSec = time.time() # returns unix timestamp in seconds
|
||||
p.time = int(timeSec)
|
||||
timeSec = int(time.time()) # returns unix timestamp in seconds
|
||||
p.time = timeSec
|
||||
logging.debug(f"p.time:{p.time}")
|
||||
|
||||
return self.sendData(
|
||||
if wantResponse:
|
||||
onResponse = self.onResponsePosition
|
||||
else:
|
||||
onResponse = None
|
||||
|
||||
d = self.sendData(
|
||||
p,
|
||||
destinationId,
|
||||
portNum=portnums_pb2.PortNum.POSITION_APP,
|
||||
wantAck=wantAck,
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForPosition()
|
||||
return d
|
||||
|
||||
def sendTraceRoute(self, dest, hopLimit):
|
||||
def onResponsePosition(self, p):
|
||||
"""on response for position"""
|
||||
if p["decoded"]["portnum"] == 'POSITION_APP':
|
||||
self._acknowledgment.receivedPosition = True
|
||||
position = mesh_pb2.Position()
|
||||
position.ParseFromString(p["decoded"]["payload"])
|
||||
|
||||
ret = "Position received: "
|
||||
if position.latitude_i != 0 and position.longitude_i != 0:
|
||||
ret += f"({position.latitude_i * 10**-7}, {position.longitude_i * 10**-7})"
|
||||
else:
|
||||
ret += "(unknown)"
|
||||
if position.altitude != 0:
|
||||
ret += f" {position.altitude}m"
|
||||
|
||||
if position.precision_bits not in [0,32]:
|
||||
ret += f" precision:{position.precision_bits}"
|
||||
elif position.precision_bits == 32:
|
||||
ret += " full precision"
|
||||
elif position.precision_bits == 0:
|
||||
ret += " position disabled"
|
||||
|
||||
print(ret)
|
||||
|
||||
elif p["decoded"]["portnum"] == 'ROUTING_APP':
|
||||
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
|
||||
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
|
||||
|
||||
def sendTraceRoute(self, dest: Union[int, str], hopLimit: int, channelIndex: int=0):
|
||||
"""Send the trace route"""
|
||||
r = mesh_pb2.RouteDiscovery()
|
||||
self.sendData(
|
||||
@@ -394,12 +474,13 @@ class MeshInterface:
|
||||
portNum=portnums_pb2.PortNum.TRACEROUTE_APP,
|
||||
wantResponse=True,
|
||||
onResponse=self.onResponseTraceRoute,
|
||||
channelIndex=channelIndex,
|
||||
)
|
||||
# extend timeout based on number of nodes, limit by configured hopLimit
|
||||
waitFactor = min(len(self.nodes) - 1, hopLimit)
|
||||
waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit)
|
||||
self.waitForTraceRoute(waitFactor)
|
||||
|
||||
def onResponseTraceRoute(self, p):
|
||||
def onResponseTraceRoute(self, p: dict):
|
||||
"""on response for trace route"""
|
||||
routeDiscovery = mesh_pb2.RouteDiscovery()
|
||||
routeDiscovery.ParseFromString(p["decoded"]["payload"])
|
||||
@@ -415,48 +496,45 @@ class MeshInterface:
|
||||
|
||||
self._acknowledgment.receivedTraceRoute = True
|
||||
|
||||
def sendTelemetry(self, destinationId=BROADCAST_ADDR, wantResponse=False):
|
||||
def sendTelemetry(self, destinationId: Union[int,str]=BROADCAST_ADDR, wantResponse: bool=False, channelIndex: int=0):
|
||||
"""Send telemetry and optionally ask for a response"""
|
||||
r = telemetry_pb2.Telemetry()
|
||||
|
||||
node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum)
|
||||
if node is not None:
|
||||
metrics = node.get("deviceMetrics")
|
||||
if metrics:
|
||||
batteryLevel = metrics.get("batteryLevel")
|
||||
if batteryLevel is not None:
|
||||
r.device_metrics.battery_level = batteryLevel
|
||||
voltage = metrics.get("voltage")
|
||||
if voltage is not None:
|
||||
r.device_metrics.voltage = voltage
|
||||
channel_utilization = metrics.get("channelUtilization")
|
||||
if channel_utilization is not None:
|
||||
r.device_metrics.channel_utilization = channel_utilization
|
||||
air_util_tx = metrics.get("airUtilTx")
|
||||
if air_util_tx is not None:
|
||||
r.device_metrics.air_util_tx = air_util_tx
|
||||
if self.nodes is not None:
|
||||
node = next(n for n in self.nodes.values() if n["num"] == self.localNode.nodeNum)
|
||||
if node is not None:
|
||||
metrics = node.get("deviceMetrics")
|
||||
if metrics:
|
||||
batteryLevel = metrics.get("batteryLevel")
|
||||
if batteryLevel is not None:
|
||||
r.device_metrics.battery_level = batteryLevel
|
||||
voltage = metrics.get("voltage")
|
||||
if voltage is not None:
|
||||
r.device_metrics.voltage = voltage
|
||||
channel_utilization = metrics.get("channelUtilization")
|
||||
if channel_utilization is not None:
|
||||
r.device_metrics.channel_utilization = channel_utilization
|
||||
air_util_tx = metrics.get("airUtilTx")
|
||||
if air_util_tx is not None:
|
||||
r.device_metrics.air_util_tx = air_util_tx
|
||||
|
||||
if wantResponse:
|
||||
onResponse = self.onResponseTelemetry
|
||||
else:
|
||||
onResponse = None
|
||||
|
||||
if destinationId.startswith("!"):
|
||||
destinationId = int(destinationId[1:], 16)
|
||||
else:
|
||||
destinationId = int(destinationId)
|
||||
|
||||
self.sendData(
|
||||
r,
|
||||
destinationId=destinationId,
|
||||
portNum=portnums_pb2.PortNum.TELEMETRY_APP,
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForTelemetry()
|
||||
|
||||
def onResponseTelemetry(self, p):
|
||||
def onResponseTelemetry(self, p: dict):
|
||||
"""on response for telemetry"""
|
||||
if p["decoded"]["portnum"] == 'TELEMETRY_APP':
|
||||
self._acknowledgment.receivedTelemetry = True
|
||||
@@ -479,10 +557,10 @@ class MeshInterface:
|
||||
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE':
|
||||
our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
|
||||
|
||||
def _addResponseHandler(self, requestId, callback):
|
||||
self.responseHandlers[requestId] = ResponseHandler(callback)
|
||||
def _addResponseHandler(self, requestId: int, callback: Callable[[dict], Any], ackPermitted: bool=False):
|
||||
self.responseHandlers[requestId] = ResponseHandler(callback=callback, ackPermitted=ackPermitted)
|
||||
|
||||
def _sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR, wantAck=False):
|
||||
def _sendPacket(self, meshPacket: mesh_pb2.MeshPacket, destinationId: Union[int,str]=BROADCAST_ADDR, wantAck: bool=False):
|
||||
"""Send a MeshPacket to the specified node (or if unspecified, broadcast).
|
||||
You probably don't want this - use sendData instead.
|
||||
|
||||
@@ -496,7 +574,7 @@ class MeshInterface:
|
||||
|
||||
toRadio = mesh_pb2.ToRadio()
|
||||
|
||||
nodeNum = 0
|
||||
nodeNum: int = 0
|
||||
if destinationId is None:
|
||||
our_exit("Warning: destinationId must not be None")
|
||||
elif isinstance(destinationId, int):
|
||||
@@ -514,9 +592,10 @@ class MeshInterface:
|
||||
else:
|
||||
if self.nodes:
|
||||
node = self.nodes.get(destinationId)
|
||||
if not node:
|
||||
if node is None:
|
||||
our_exit(f"Warning: NodeId {destinationId} not found in DB")
|
||||
nodeNum = node["num"]
|
||||
else:
|
||||
nodeNum = node["num"]
|
||||
else:
|
||||
logging.warning("Warning: There were no self.nodes.")
|
||||
|
||||
@@ -568,9 +647,15 @@ class MeshInterface:
|
||||
if not success:
|
||||
raise MeshInterface.MeshInterfaceError("Timed out waiting for telemetry")
|
||||
|
||||
def getMyNodeInfo(self):
|
||||
def waitForPosition(self):
|
||||
"""Wait for position"""
|
||||
success = self._timeout.waitForPosition(self._acknowledgment)
|
||||
if not success:
|
||||
raise MeshInterface.MeshInterfaceError("Timed out waiting for position")
|
||||
|
||||
def getMyNodeInfo(self) -> Optional[Dict]:
|
||||
"""Get info about my node."""
|
||||
if self.myInfo is None:
|
||||
if self.myInfo is None or self.nodesByNum is None:
|
||||
return None
|
||||
logging.debug(f"self.nodesByNum:{self.nodesByNum}")
|
||||
return self.nodesByNum.get(self.myInfo.my_node_num)
|
||||
@@ -607,7 +692,7 @@ class MeshInterface:
|
||||
if self.failure:
|
||||
raise self.failure
|
||||
|
||||
def _generatePacketId(self):
|
||||
def _generatePacketId(self) -> int:
|
||||
"""Get a new unique packet ID"""
|
||||
if self.currentPacketId is None:
|
||||
raise MeshInterface.MeshInterfaceError("Not connected yet, can not generate packet")
|
||||
@@ -634,6 +719,7 @@ class MeshInterface:
|
||||
self.heartbeatTimer = threading.Timer(i, callback)
|
||||
self.heartbeatTimer.start()
|
||||
p = mesh_pb2.ToRadio()
|
||||
p.heartbeat.CopyFrom(mesh_pb2.Heartbeat())
|
||||
self._sendToRadio(p)
|
||||
|
||||
callback() # run our periodic callback now, it will make another timer if necessary
|
||||
@@ -657,9 +743,11 @@ class MeshInterface:
|
||||
self.myInfo = None
|
||||
self.nodes = {} # nodes keyed by ID
|
||||
self.nodesByNum = {} # nodes keyed by nodenum
|
||||
self._localChannels = [] # empty until we start getting channels pushed from the device (during config)
|
||||
|
||||
startConfig = mesh_pb2.ToRadio()
|
||||
self.configId = random.randint(0, 0xFFFFFFFF)
|
||||
if self.configId is None or not self.noNodes:
|
||||
self.configId = random.randint(0, 0xFFFFFFFF)
|
||||
startConfig.want_config_id = self.configId
|
||||
self._sendToRadio(startConfig)
|
||||
|
||||
@@ -669,18 +757,18 @@ class MeshInterface:
|
||||
m.disconnect = True
|
||||
self._sendToRadio(m)
|
||||
|
||||
def _queueHasFreeSpace(self):
|
||||
def _queueHasFreeSpace(self) -> bool:
|
||||
# We never got queueStatus, maybe the firmware is old
|
||||
if self.queueStatus is None:
|
||||
return True
|
||||
return self.queueStatus.free > 0
|
||||
|
||||
def _queueClaim(self):
|
||||
def _queueClaim(self) -> None:
|
||||
if self.queueStatus is None:
|
||||
return
|
||||
self.queueStatus.free -= 1
|
||||
|
||||
def _sendToRadio(self, toRadio):
|
||||
def _sendToRadio(self, toRadio: mesh_pb2.ToRadio) -> None:
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
if self.noProto:
|
||||
logging.warning(
|
||||
@@ -729,18 +817,23 @@ class MeshInterface:
|
||||
self.queue[packetId] = packet
|
||||
# logging.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
|
||||
def _sendToRadioImpl(self, toRadio):
|
||||
def _sendToRadioImpl(self, toRadio: mesh_pb2.ToRadio) -> None:
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
logging.error(f"Subclass must provide toradio: {toRadio}")
|
||||
|
||||
def _handleConfigComplete(self):
|
||||
def _handleConfigComplete(self) -> None:
|
||||
"""
|
||||
Done with initial config messages, now send regular MeshPackets
|
||||
to ask for settings and channels
|
||||
"""
|
||||
self.localNode.requestChannels()
|
||||
# This is no longer necessary because the current protocol statemachine has already proactively sent us the locally visible channels
|
||||
# self.localNode.requestChannels()
|
||||
self.localNode.setChannels(self._localChannels)
|
||||
|
||||
def _handleQueueStatusFromRadio(self, queueStatus):
|
||||
# the following should only be called after we have settings and channels
|
||||
self._connected() # Tell everyone else we are ready to go
|
||||
|
||||
def _handleQueueStatusFromRadio(self, queueStatus) -> None:
|
||||
self.queueStatus = queueStatus
|
||||
logging.debug(
|
||||
f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} "
|
||||
@@ -788,16 +881,18 @@ class MeshInterface:
|
||||
logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
|
||||
|
||||
elif fromRadio.HasField("node_info"):
|
||||
node = asDict["nodeInfo"]
|
||||
logging.debug(f"Received nodeinfo: {asDict['nodeInfo']}")
|
||||
|
||||
node = self._getOrCreateByNum(asDict["nodeInfo"]["num"])
|
||||
node.update(asDict["nodeInfo"])
|
||||
try:
|
||||
newpos = self._fixupPosition(node["position"])
|
||||
node["position"] = newpos
|
||||
except:
|
||||
logging.debug("Node without position")
|
||||
|
||||
logging.debug(f"Received nodeinfo: {node}")
|
||||
|
||||
self.nodesByNum[node["num"]] = node
|
||||
# no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum
|
||||
#self.nodesByNum[node["num"]] = node
|
||||
if "user" in node: # Some nodes might not have user/ids assigned yet
|
||||
if "id" in node["user"]:
|
||||
self.nodes[node["user"]["id"]] = node
|
||||
@@ -811,21 +906,36 @@ class MeshInterface:
|
||||
# stream API fromRadio.config_complete_id
|
||||
logging.debug(f"Config complete ID {self.configId}")
|
||||
self._handleConfigComplete()
|
||||
|
||||
elif fromRadio.HasField("channel"):
|
||||
self._handleChannel(fromRadio.channel)
|
||||
elif fromRadio.HasField("packet"):
|
||||
self._handlePacketFromRadio(fromRadio.packet)
|
||||
|
||||
elif fromRadio.HasField("queueStatus"):
|
||||
self._handleQueueStatusFromRadio(fromRadio.queueStatus)
|
||||
|
||||
elif fromRadio.rebooted:
|
||||
elif fromRadio.HasField("mqttClientProxyMessage"):
|
||||
publishingThread.queueWork(
|
||||
lambda: pub.sendMessage(
|
||||
"meshtastic.mqttclientproxymessage", proxymessage=fromRadio.mqttClientProxyMessage, interface=self
|
||||
)
|
||||
)
|
||||
|
||||
elif fromRadio.HasField("xmodemPacket"):
|
||||
publishingThread.queueWork(
|
||||
lambda: pub.sendMessage(
|
||||
"meshtastic.xmodempacket", packet=fromRadio.xmodemPacket, interface=self
|
||||
)
|
||||
)
|
||||
|
||||
elif fromRadio.HasField("rebooted") and fromRadio.rebooted:
|
||||
# Tell clients the device went away. Careful not to call the overridden
|
||||
# subclass version that closes the serial port
|
||||
MeshInterface._disconnected(self)
|
||||
|
||||
self._startConfig() # redownload the node db etc...
|
||||
|
||||
elif fromRadio.config or fromRadio.moduleConfig:
|
||||
elif fromRadio.HasField("config") or fromRadio.HasField("moduleConfig"):
|
||||
if fromRadio.config.HasField("device"):
|
||||
self.localNode.localConfig.device.CopyFrom(fromRadio.config.device)
|
||||
elif fromRadio.config.HasField("position"):
|
||||
@@ -853,6 +963,10 @@ class MeshInterface:
|
||||
self.localNode.moduleConfig.external_notification.CopyFrom(
|
||||
fromRadio.moduleConfig.external_notification
|
||||
)
|
||||
elif fromRadio.moduleConfig.HasField("store_forward"):
|
||||
self.localNode.moduleConfig.store_forward.CopyFrom(
|
||||
fromRadio.moduleConfig.store_forward
|
||||
)
|
||||
elif fromRadio.moduleConfig.HasField("range_test"):
|
||||
self.localNode.moduleConfig.range_test.CopyFrom(
|
||||
fromRadio.moduleConfig.range_test
|
||||
@@ -891,7 +1005,7 @@ class MeshInterface:
|
||||
else:
|
||||
logging.debug("Unexpected FromRadio payload")
|
||||
|
||||
def _fixupPosition(self, position):
|
||||
def _fixupPosition(self, position: Dict) -> Dict:
|
||||
"""Convert integer lat/lon into floats
|
||||
|
||||
Arguments:
|
||||
@@ -899,9 +1013,9 @@ class MeshInterface:
|
||||
Returns the position with the updated keys
|
||||
"""
|
||||
if "latitudeI" in position:
|
||||
position["latitude"] = position["latitudeI"] * 1e-7
|
||||
position["latitude"] = float(position["latitudeI"] * Decimal("1e-7"))
|
||||
if "longitudeI" in position:
|
||||
position["longitude"] = position["longitudeI"] * 1e-7
|
||||
position["longitude"] = float(position["longitudeI"] * Decimal("1e-7"))
|
||||
return position
|
||||
|
||||
def _nodeNumToId(self, num):
|
||||
@@ -930,10 +1044,23 @@ class MeshInterface:
|
||||
if nodeNum in self.nodesByNum:
|
||||
return self.nodesByNum[nodeNum]
|
||||
else:
|
||||
n = {"num": nodeNum} # Create a minimal node db entry
|
||||
presumptive_id = f"!{nodeNum:08x}"
|
||||
n = {
|
||||
"num": nodeNum,
|
||||
"user": {
|
||||
"id": presumptive_id,
|
||||
"longName": f"Meshtastic {presumptive_id[-4:]}",
|
||||
"shortName": f"{presumptive_id[-4:]}",
|
||||
"hwModel": "UNSET"
|
||||
}
|
||||
} # Create a minimal node db entry
|
||||
self.nodesByNum[nodeNum] = n
|
||||
return n
|
||||
|
||||
def _handleChannel(self, channel):
|
||||
"""During initial config the local node will proactively send all N (8) channels it knows"""
|
||||
self._localChannels.append(channel)
|
||||
|
||||
def _handlePacketFromRadio(self, meshPacket, hack=False):
|
||||
"""Handle a MeshPacket that just arrived from the radio
|
||||
|
||||
@@ -1030,15 +1157,19 @@ class MeshInterface:
|
||||
# Is this message in response to a request, if so, look for a handler
|
||||
requestId = decoded.get("requestId")
|
||||
if requestId is not None:
|
||||
# We ignore ACK packets, but send NAKs and data responses to the handlers
|
||||
logging.debug(f"Got a response for requestId {requestId}")
|
||||
# We ignore ACK packets unless the callback is named `onAckNak`
|
||||
# or the handler is set as ackPermitted, but send NAKs and
|
||||
# other, data-containing responses to the handlers
|
||||
routing = decoded.get("routing")
|
||||
isAck = routing is not None and ("errorReason" not in routing)
|
||||
if not isAck:
|
||||
# we keep the responseHandler in dict until we get a non ack
|
||||
handler = self.responseHandlers.pop(requestId, None)
|
||||
if handler is not None:
|
||||
if not isAck or (isAck and handler.__name__ == "onAckNak"):
|
||||
handler.callback(asDict)
|
||||
isAck = routing is not None and ("errorReason" not in routing or routing["errorReason"] == "NONE")
|
||||
# we keep the responseHandler in dict until we actually call it
|
||||
handler = self.responseHandlers.get(requestId, None)
|
||||
if handler is not None:
|
||||
if (not isAck) or handler.callback.__name__ == "onAckNak" or handler.ackPermitted:
|
||||
handler = self.responseHandlers.pop(requestId, None)
|
||||
logging.debug(f"Calling response handler for requestId {requestId}")
|
||||
handler.callback(asDict)
|
||||
|
||||
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
|
||||
publishingThread.queueWork(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -112,6 +112,14 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
||||
"""
|
||||
LoRAType device: https://loratype.org/
|
||||
"""
|
||||
WIPHONE: _HardwareModel.ValueType # 20
|
||||
"""
|
||||
wiphone https://www.wiphone.io/
|
||||
"""
|
||||
WIO_WM1110: _HardwareModel.ValueType # 21
|
||||
"""
|
||||
WIO Tracker WM1110 family from Seeed Studio. Includes wio-1110-tracker and wio-1110-sdk
|
||||
"""
|
||||
STATION_G1: _HardwareModel.ValueType # 25
|
||||
"""
|
||||
B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
|
||||
@@ -267,6 +275,25 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
||||
Teledatics TD-LORAC NRF52840 based M.2 LoRA module
|
||||
Compatible with the TD-WRLS development board
|
||||
"""
|
||||
CDEBYTE_EORA_S3: _HardwareModel.ValueType # 61
|
||||
"""
|
||||
CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3
|
||||
"""
|
||||
TWC_MESH_V4: _HardwareModel.ValueType # 62
|
||||
"""
|
||||
TWC_MESH_V4
|
||||
Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS
|
||||
"""
|
||||
NRF52_PROMICRO_DIY: _HardwareModel.ValueType # 63
|
||||
"""
|
||||
NRF52_PROMICRO_DIY
|
||||
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
|
||||
"""
|
||||
RADIOMASTER_900_BANDIT_NANO: _HardwareModel.ValueType # 64
|
||||
"""
|
||||
RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
|
||||
ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
|
||||
"""
|
||||
PRIVATE_HW: _HardwareModel.ValueType # 255
|
||||
"""
|
||||
------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -364,6 +391,14 @@ LORA_TYPE: HardwareModel.ValueType # 19
|
||||
"""
|
||||
LoRAType device: https://loratype.org/
|
||||
"""
|
||||
WIPHONE: HardwareModel.ValueType # 20
|
||||
"""
|
||||
wiphone https://www.wiphone.io/
|
||||
"""
|
||||
WIO_WM1110: HardwareModel.ValueType # 21
|
||||
"""
|
||||
WIO Tracker WM1110 family from Seeed Studio. Includes wio-1110-tracker and wio-1110-sdk
|
||||
"""
|
||||
STATION_G1: HardwareModel.ValueType # 25
|
||||
"""
|
||||
B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
|
||||
@@ -519,6 +554,25 @@ TD_LORAC: HardwareModel.ValueType # 60
|
||||
Teledatics TD-LORAC NRF52840 based M.2 LoRA module
|
||||
Compatible with the TD-WRLS development board
|
||||
"""
|
||||
CDEBYTE_EORA_S3: HardwareModel.ValueType # 61
|
||||
"""
|
||||
CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3
|
||||
"""
|
||||
TWC_MESH_V4: HardwareModel.ValueType # 62
|
||||
"""
|
||||
TWC_MESH_V4
|
||||
Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS
|
||||
"""
|
||||
NRF52_PROMICRO_DIY: HardwareModel.ValueType # 63
|
||||
"""
|
||||
NRF52_PROMICRO_DIY
|
||||
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
|
||||
"""
|
||||
RADIOMASTER_900_BANDIT_NANO: HardwareModel.ValueType # 64
|
||||
"""
|
||||
RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
|
||||
ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
|
||||
"""
|
||||
PRIVATE_HW: HardwareModel.ValueType # 255
|
||||
"""
|
||||
------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -2371,3 +2425,102 @@ class NodeRemoteHardwarePin(google.protobuf.message.Message):
|
||||
def ClearField(self, field_name: typing_extensions.Literal["node_num", b"node_num", "pin", b"pin"]) -> None: ...
|
||||
|
||||
global___NodeRemoteHardwarePin = NodeRemoteHardwarePin
|
||||
|
||||
@typing_extensions.final
|
||||
class ChunkedPayload(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
PAYLOAD_ID_FIELD_NUMBER: builtins.int
|
||||
CHUNK_COUNT_FIELD_NUMBER: builtins.int
|
||||
CHUNK_INDEX_FIELD_NUMBER: builtins.int
|
||||
PAYLOAD_CHUNK_FIELD_NUMBER: builtins.int
|
||||
payload_id: builtins.int
|
||||
"""
|
||||
The ID of the entire payload
|
||||
"""
|
||||
chunk_count: builtins.int
|
||||
"""
|
||||
The total number of chunks in the payload
|
||||
"""
|
||||
chunk_index: builtins.int
|
||||
"""
|
||||
The current chunk index in the total
|
||||
"""
|
||||
payload_chunk: builtins.bytes
|
||||
"""
|
||||
The binary data of the current chunk
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
payload_id: builtins.int = ...,
|
||||
chunk_count: builtins.int = ...,
|
||||
chunk_index: builtins.int = ...,
|
||||
payload_chunk: builtins.bytes = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["chunk_count", b"chunk_count", "chunk_index", b"chunk_index", "payload_chunk", b"payload_chunk", "payload_id", b"payload_id"]) -> None: ...
|
||||
|
||||
global___ChunkedPayload = ChunkedPayload
|
||||
|
||||
@typing_extensions.final
|
||||
class resend_chunks(google.protobuf.message.Message):
|
||||
"""
|
||||
Wrapper message for broken repeated oneof support
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
CHUNKS_FIELD_NUMBER: builtins.int
|
||||
@property
|
||||
def chunks(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
chunks: collections.abc.Iterable[builtins.int] | None = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["chunks", b"chunks"]) -> None: ...
|
||||
|
||||
global___resend_chunks = resend_chunks
|
||||
|
||||
@typing_extensions.final
|
||||
class ChunkedPayloadResponse(google.protobuf.message.Message):
|
||||
"""
|
||||
Responses to a ChunkedPayload request
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
PAYLOAD_ID_FIELD_NUMBER: builtins.int
|
||||
REQUEST_TRANSFER_FIELD_NUMBER: builtins.int
|
||||
ACCEPT_TRANSFER_FIELD_NUMBER: builtins.int
|
||||
RESEND_CHUNKS_FIELD_NUMBER: builtins.int
|
||||
payload_id: builtins.int
|
||||
"""
|
||||
The ID of the entire payload
|
||||
"""
|
||||
request_transfer: builtins.bool
|
||||
"""
|
||||
Request to transfer chunked payload
|
||||
"""
|
||||
accept_transfer: builtins.bool
|
||||
"""
|
||||
Accept the transfer chunked payload
|
||||
"""
|
||||
@property
|
||||
def resend_chunks(self) -> global___resend_chunks:
|
||||
"""
|
||||
Request missing indexes in the chunked payload
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
payload_id: builtins.int = ...,
|
||||
request_transfer: builtins.bool = ...,
|
||||
accept_transfer: builtins.bool = ...,
|
||||
resend_chunks: global___resend_chunks | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing_extensions.Literal["accept_transfer", b"accept_transfer", "payload_variant", b"payload_variant", "request_transfer", b"request_transfer", "resend_chunks", b"resend_chunks"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["accept_transfer", b"accept_transfer", "payload_id", b"payload_id", "payload_variant", b"payload_variant", "request_transfer", b"request_transfer", "resend_chunks", b"resend_chunks"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing_extensions.Literal["payload_variant", b"payload_variant"]) -> typing_extensions.Literal["request_transfer", "accept_transfer", "resend_chunks"] | None: ...
|
||||
|
||||
global___ChunkedPayloadResponse = ChunkedPayloadResponse
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -405,6 +405,8 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
|
||||
ENABLED_FIELD_NUMBER: builtins.int
|
||||
PAXCOUNTER_UPDATE_INTERVAL_FIELD_NUMBER: builtins.int
|
||||
WIFI_THRESHOLD_FIELD_NUMBER: builtins.int
|
||||
BLE_THRESHOLD_FIELD_NUMBER: builtins.int
|
||||
enabled: builtins.bool
|
||||
"""
|
||||
Enable the Paxcounter Module
|
||||
@@ -414,13 +416,23 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
Interval in seconds of how often we should try to send our
|
||||
metrics to the mesh
|
||||
"""
|
||||
wifi_threshold: builtins.int
|
||||
"""
|
||||
WiFi RSSI threshold. Defaults to -80
|
||||
"""
|
||||
ble_threshold: builtins.int
|
||||
"""
|
||||
BLE RSSI threshold. Defaults to -80
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
enabled: builtins.bool = ...,
|
||||
paxcounter_update_interval: builtins.int = ...,
|
||||
wifi_threshold: builtins.int = ...,
|
||||
ble_threshold: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["enabled", b"enabled", "paxcounter_update_interval", b"paxcounter_update_interval"]) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["ble_threshold", b"ble_threshold", "enabled", b"enabled", "paxcounter_update_interval", b"paxcounter_update_interval", "wifi_threshold", b"wifi_threshold"]) -> None: ...
|
||||
|
||||
@typing_extensions.final
|
||||
class SerialConfig(google.protobuf.message.Message):
|
||||
|
||||
37
meshtastic/mt_config.py
Normal file
37
meshtastic/mt_config.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Globals singleton class.
|
||||
|
||||
The Global object is gone, as are all its setters and getters. Instead the
|
||||
module itself is the singleton namespace, which can be imported into
|
||||
whichever module is used. The associated tests have also been removed,
|
||||
since we now rely on built in Python mechanisms.
|
||||
|
||||
This is intended to make the Python read more naturally, and to make the
|
||||
intention of the code clearer and more compact. It is merely a sticking
|
||||
plaster over the use of shared mt_config, but the coupling issues wil be dealt
|
||||
with rather more easily once the code is simplified by this change.
|
||||
|
||||
"""
|
||||
|
||||
def reset():
|
||||
"""
|
||||
Restore the namespace to pristine condition.
|
||||
"""
|
||||
# pylint: disable=W0603
|
||||
global args, parser, channel_index, logfile, tunnelInstance, camel_case
|
||||
args = None
|
||||
parser = None
|
||||
channel_index = None
|
||||
logfile = None
|
||||
tunnelInstance = None
|
||||
# TODO: to migrate to camel_case for v1.3 change this value to True
|
||||
camel_case = False
|
||||
|
||||
# These assignments are used instead of calling reset()
|
||||
# purely to shut pylint up.
|
||||
args = None
|
||||
parser = None
|
||||
channel_index = None
|
||||
logfile = None
|
||||
tunnelInstance = None
|
||||
camel_case = False
|
||||
@@ -5,7 +5,9 @@ import base64
|
||||
import logging
|
||||
import time
|
||||
|
||||
from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, portnums_pb2
|
||||
from typing import Union
|
||||
|
||||
from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, mesh_pb2, portnums_pb2
|
||||
from meshtastic.util import (
|
||||
Timeout,
|
||||
camel_to_snake,
|
||||
@@ -62,14 +64,19 @@ class Node:
|
||||
"""Show human readable description of our node"""
|
||||
prefs = ""
|
||||
if self.localConfig:
|
||||
prefs = message_to_json(self.localConfig)
|
||||
prefs = message_to_json(self.localConfig, multiline=True)
|
||||
print(f"Preferences: {prefs}\n")
|
||||
prefs = ""
|
||||
if self.moduleConfig:
|
||||
prefs = message_to_json(self.moduleConfig)
|
||||
prefs = message_to_json(self.moduleConfig, multiline=True)
|
||||
print(f"Module preferences: {prefs}\n")
|
||||
self.showChannels()
|
||||
|
||||
def setChannels(self, channels):
|
||||
"""Set the channels for this node"""
|
||||
self.channels = channels
|
||||
self._fixupChannels()
|
||||
|
||||
def requestChannels(self):
|
||||
"""Send regular MeshPackets to ask channels."""
|
||||
logging.debug(f"requestChannels for nodeNum:{self.nodeNum}")
|
||||
@@ -311,6 +318,8 @@ class Node:
|
||||
):
|
||||
channelSet.settings.append(c.settings)
|
||||
|
||||
if len(self.localConfig.ListFields()) == 0:
|
||||
self.requestConfig(self.localConfig.DESCRIPTOR.fields_by_name.get('lora'))
|
||||
channelSet.lora_config.CopyFrom(self.localConfig.lora)
|
||||
some_bytes = channelSet.SerializeToString()
|
||||
s = base64.urlsafe_b64encode(some_bytes).decode("ascii")
|
||||
@@ -567,6 +576,19 @@ class Node:
|
||||
onResponse = self.onAckNak
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def enterDFUMode(self):
|
||||
"""Tell the node to enter DFU mode (NRF52)."""
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.enter_dfu_mode_request = True
|
||||
logging.info(f"Telling node to enable DFU mode")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
else:
|
||||
onResponse = self.onAckNak
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def shutdown(self, secs: int = 10):
|
||||
"""Tell the node to shutdown."""
|
||||
p = admin_pb2.AdminMessage()
|
||||
@@ -586,9 +608,10 @@ class Node:
|
||||
p.get_device_metadata_request = True
|
||||
logging.info(f"Requesting device metadata")
|
||||
|
||||
return self._sendAdmin(
|
||||
self._sendAdmin(
|
||||
p, wantResponse=True, onResponse=self.onRequestGetMetadata
|
||||
)
|
||||
self.iface.waitForAckNak()
|
||||
|
||||
def factoryReset(self):
|
||||
"""Tell the node to factory reset."""
|
||||
@@ -603,6 +626,23 @@ class Node:
|
||||
onResponse = self.onAckNak
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def removeNode(self, nodeId: Union[int, str]):
|
||||
"""Tell the node to remove a specific node by ID"""
|
||||
if isinstance(nodeId, str):
|
||||
if nodeId.startswith("!"):
|
||||
nodeId = int(nodeId[1:], 16)
|
||||
else:
|
||||
nodeId = int(nodeId)
|
||||
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.remove_by_nodenum = nodeId
|
||||
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
else:
|
||||
onResponse = self.onAckNak
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def resetNodeDb(self):
|
||||
"""Tell the node to reset its list of nodes."""
|
||||
p = admin_pb2.AdminMessage()
|
||||
@@ -616,11 +656,43 @@ class Node:
|
||||
onResponse = self.onAckNak
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def setFixedPosition(self, lat: Union[int, float], lon: Union[int, float], alt: int):
|
||||
"""Tell the node to set fixed position to the provided value and enable the fixed position setting"""
|
||||
if self != self.iface.localNode:
|
||||
logging.error("Setting position of remote nodes is not supported.")
|
||||
return None
|
||||
|
||||
p = mesh_pb2.Position()
|
||||
if isinstance(lat, float) and lat != 0.0:
|
||||
p.latitude_i = int(lat / 1e-7)
|
||||
elif isinstance(lat, int) and lat != 0:
|
||||
p.latitude_i = lat
|
||||
|
||||
if isinstance(lon, float) and lon != 0.0:
|
||||
p.longitude_i = int(lon / 1e-7)
|
||||
elif isinstance(lon, int) and lon != 0:
|
||||
p.longitude_i = lon
|
||||
|
||||
if alt != 0:
|
||||
p.altitude = alt
|
||||
|
||||
a = admin_pb2.AdminMessage()
|
||||
a.set_fixed_position.CopyFrom(p)
|
||||
return self._sendAdmin(a)
|
||||
|
||||
def removeFixedPosition(self):
|
||||
"""Tell the node to remove the fixed position and set the fixed position setting to false"""
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.remove_fixed_position = True
|
||||
logging.info(f"Telling node to remove fixed position")
|
||||
|
||||
return self._sendAdmin(p)
|
||||
|
||||
def _fixupChannels(self):
|
||||
"""Fixup indexes and add disabled channels as needed"""
|
||||
|
||||
# Add extra disabled channels as needed
|
||||
# TODO: These 2 lines seem to not do anything.
|
||||
# This is needed because the protobufs will have index **missing** if the channel number is zero
|
||||
for index, ch in enumerate(self.channels):
|
||||
ch.index = index # fixup indexes
|
||||
|
||||
@@ -642,24 +714,30 @@ class Node:
|
||||
"""Handle the response packet for requesting device metadata getMetadata()"""
|
||||
logging.debug(f"onRequestGetMetadata() p:{p}")
|
||||
|
||||
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
|
||||
portnums_pb2.PortNum.ROUTING_APP
|
||||
):
|
||||
if "routing" in p["decoded"]:
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
logging.warning(
|
||||
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
|
||||
)
|
||||
self._timeout.expireTime = time.time() # Do not wait any longer
|
||||
return # Don't try to parse this routing message
|
||||
logging.debug(f"Retrying metadata request.")
|
||||
self.getMetadata()
|
||||
return
|
||||
print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
|
||||
self.iface._acknowledgment.receivedNak = True
|
||||
else:
|
||||
self.iface._acknowledgment.receivedAck = True
|
||||
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
|
||||
portnums_pb2.PortNum.ROUTING_APP
|
||||
):
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
logging.warning(
|
||||
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
|
||||
)
|
||||
self._timeout.expireTime = time.time() # Do not wait any longer
|
||||
return # Don't try to parse this routing message
|
||||
logging.debug(f"Retrying metadata request.")
|
||||
self.getMetadata()
|
||||
return
|
||||
|
||||
c = p["decoded"]["admin"]["raw"].get_device_metadata_response
|
||||
self._timeout.reset() # We made forward progress
|
||||
logging.debug(f"Received metadata {stripnl(c)}")
|
||||
print(f"\nfirmware_version: {c.firmware_version}")
|
||||
print(f"device_state_version: {c.device_state_version}")
|
||||
c = p["decoded"]["admin"]["raw"].get_device_metadata_response
|
||||
self._timeout.reset() # We made forward progress
|
||||
logging.debug(f"Received metadata {stripnl(c)}")
|
||||
print(f"\nfirmware_version: {c.firmware_version}")
|
||||
print(f"device_state_version: {c.device_state_version}")
|
||||
|
||||
def onResponseRequestChannel(self, p):
|
||||
"""Handle the response packet for requesting a channel _requestChannel()"""
|
||||
@@ -692,9 +770,6 @@ class Node:
|
||||
|
||||
self.channels = self.partialChannels
|
||||
self._fixupChannels()
|
||||
|
||||
# FIXME, the following should only be called after we have settings and channels
|
||||
self.iface._connected() # Tell everyone else we are ready to go
|
||||
else:
|
||||
self._requestChannel(index + 1)
|
||||
|
||||
@@ -740,9 +815,9 @@ class Node:
|
||||
def _sendAdmin(
|
||||
self,
|
||||
p: admin_pb2.AdminMessage,
|
||||
wantResponse=True,
|
||||
wantResponse: bool=True,
|
||||
onResponse=None,
|
||||
adminIndex=0,
|
||||
adminIndex: int=0,
|
||||
):
|
||||
"""Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import logging
|
||||
import platform
|
||||
import time
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import serial # type: ignore[import-untyped]
|
||||
|
||||
import meshtastic.util
|
||||
@@ -16,7 +18,7 @@ if platform.system() != "Windows":
|
||||
class SerialInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a serial link"""
|
||||
|
||||
def __init__(self, devPath=None, debugOut=None, noProto=False, connectNow=True):
|
||||
def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto=False, connectNow=True, noNodes: bool=False):
|
||||
"""Constructor, opens a connection to a specified serial port, or if unspecified try to
|
||||
find one Meshtastic device by probing
|
||||
|
||||
@@ -26,7 +28,7 @@ class SerialInterface(StreamInterface):
|
||||
"""
|
||||
self.noProto = noProto
|
||||
|
||||
self.devPath = devPath
|
||||
self.devPath: Optional[str] = devPath
|
||||
|
||||
if self.devPath is None:
|
||||
ports = meshtastic.util.findPorts(True)
|
||||
@@ -60,7 +62,7 @@ class SerialInterface(StreamInterface):
|
||||
time.sleep(0.1)
|
||||
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
|
||||
)
|
||||
|
||||
def close(self):
|
||||
|
||||
@@ -19,7 +19,7 @@ MAX_TO_FROM_RADIO_SIZE = 512
|
||||
class StreamInterface(MeshInterface):
|
||||
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
|
||||
|
||||
def __init__(self, debugOut=None, noProto=False, connectNow=True):
|
||||
def __init__(self, debugOut=None, noProto=False, connectNow=True, noNodes=False):
|
||||
"""Constructor, opens a connection to self.stream
|
||||
|
||||
Keyword Arguments:
|
||||
@@ -43,7 +43,7 @@ class StreamInterface(MeshInterface):
|
||||
# FIXME, figure out why daemon=True causes reader thread to exit too early
|
||||
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True)
|
||||
|
||||
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto)
|
||||
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes)
|
||||
|
||||
# Start the reader thread after superclass constructor completes init
|
||||
if connectNow:
|
||||
|
||||
@@ -17,6 +17,7 @@ class TCPInterface(StreamInterface):
|
||||
noProto=False,
|
||||
connectNow=True,
|
||||
portNumber=4403,
|
||||
noNodes:bool=False,
|
||||
):
|
||||
"""Constructor, opens a connection to a specified IP address/hostname
|
||||
|
||||
@@ -38,7 +39,7 @@ class TCPInterface(StreamInterface):
|
||||
self.socket = None
|
||||
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
|
||||
)
|
||||
|
||||
def _socket_shutdown(self):
|
||||
|
||||
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"i\n\rDeviceMetrics\x12\x15\n\rbattery_level\x18\x01 \x01(\r\x12\x0f\n\x07voltage\x18\x02 \x01(\x02\x12\x1b\n\x13\x63hannel_utilization\x18\x03 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02\"\x9b\x01\n\x12\x45nvironmentMetrics\x12\x13\n\x0btemperature\x18\x01 \x01(\x02\x12\x19\n\x11relative_humidity\x18\x02 \x01(\x02\x12\x1b\n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02\x12\x16\n\x0egas_resistance\x18\x04 \x01(\x02\x12\x0f\n\x07voltage\x18\x05 \x01(\x02\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x02\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xe0\x01\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ameshtastic/telemetry.proto\x12\nmeshtastic\"\x81\x01\n\rDeviceMetrics\x12\x15\n\rbattery_level\x18\x01 \x01(\r\x12\x0f\n\x07voltage\x18\x02 \x01(\x02\x12\x1b\n\x13\x63hannel_utilization\x18\x03 \x01(\x02\x12\x13\n\x0b\x61ir_util_tx\x18\x04 \x01(\x02\x12\x16\n\x0euptime_seconds\x18\x05 \x01(\r\"\xa6\x02\n\x12\x45nvironmentMetrics\x12\x13\n\x0btemperature\x18\x01 \x01(\x02\x12\x19\n\x11relative_humidity\x18\x02 \x01(\x02\x12\x1b\n\x13\x62\x61rometric_pressure\x18\x03 \x01(\x02\x12\x16\n\x0egas_resistance\x18\x04 \x01(\x02\x12\x0f\n\x07voltage\x18\x05 \x01(\x02\x12\x0f\n\x07\x63urrent\x18\x06 \x01(\x02\x12\x0b\n\x03iaq\x18\x07 \x01(\r\x12\x10\n\x08\x64istance\x18\x08 \x01(\x02\x12\x0b\n\x03lux\x18\t \x01(\x02\x12\x11\n\twhite_lux\x18\n \x01(\x02\x12\x0e\n\x06ir_lux\x18\x0b \x01(\x02\x12\x0e\n\x06uv_lux\x18\x0c \x01(\x02\x12\x16\n\x0ewind_direction\x18\r \x01(\r\x12\x12\n\nwind_speed\x18\x0e \x01(\x02\"\x8c\x01\n\x0cPowerMetrics\x12\x13\n\x0b\x63h1_voltage\x18\x01 \x01(\x02\x12\x13\n\x0b\x63h1_current\x18\x02 \x01(\x02\x12\x13\n\x0b\x63h2_voltage\x18\x03 \x01(\x02\x12\x13\n\x0b\x63h2_current\x18\x04 \x01(\x02\x12\x13\n\x0b\x63h3_voltage\x18\x05 \x01(\x02\x12\x13\n\x0b\x63h3_current\x18\x06 \x01(\x02\"\xbf\x02\n\x11\x41irQualityMetrics\x12\x15\n\rpm10_standard\x18\x01 \x01(\r\x12\x15\n\rpm25_standard\x18\x02 \x01(\r\x12\x16\n\x0epm100_standard\x18\x03 \x01(\r\x12\x1a\n\x12pm10_environmental\x18\x04 \x01(\r\x12\x1a\n\x12pm25_environmental\x18\x05 \x01(\r\x12\x1b\n\x13pm100_environmental\x18\x06 \x01(\r\x12\x16\n\x0eparticles_03um\x18\x07 \x01(\r\x12\x16\n\x0eparticles_05um\x18\x08 \x01(\r\x12\x16\n\x0eparticles_10um\x18\t \x01(\r\x12\x16\n\x0eparticles_25um\x18\n \x01(\r\x12\x16\n\x0eparticles_50um\x18\x0b \x01(\r\x12\x17\n\x0fparticles_100um\x18\x0c \x01(\r\"\x89\x02\n\tTelemetry\x12\x0c\n\x04time\x18\x01 \x01(\x07\x12\x33\n\x0e\x64\x65vice_metrics\x18\x02 \x01(\x0b\x32\x19.meshtastic.DeviceMetricsH\x00\x12=\n\x13\x65nvironment_metrics\x18\x03 \x01(\x0b\x32\x1e.meshtastic.EnvironmentMetricsH\x00\x12<\n\x13\x61ir_quality_metrics\x18\x04 \x01(\x0b\x32\x1d.meshtastic.AirQualityMetricsH\x00\x12\x31\n\rpower_metrics\x18\x05 \x01(\x0b\x32\x18.meshtastic.PowerMetricsH\x00\x42\t\n\x07variant*\xdd\x02\n\x13TelemetrySensorType\x12\x10\n\x0cSENSOR_UNSET\x10\x00\x12\n\n\x06\x42ME280\x10\x01\x12\n\n\x06\x42ME680\x10\x02\x12\x0b\n\x07MCP9808\x10\x03\x12\n\n\x06INA260\x10\x04\x12\n\n\x06INA219\x10\x05\x12\n\n\x06\x42MP280\x10\x06\x12\t\n\x05SHTC3\x10\x07\x12\t\n\x05LPS22\x10\x08\x12\x0b\n\x07QMC6310\x10\t\x12\x0b\n\x07QMI8658\x10\n\x12\x0c\n\x08QMC5883L\x10\x0b\x12\t\n\x05SHT31\x10\x0c\x12\x0c\n\x08PMSA003I\x10\r\x12\x0b\n\x07INA3221\x10\x0e\x12\n\n\x06\x42MP085\x10\x0f\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\x12\x0c\n\x08VEML7700\x10\x12\x12\x0c\n\x08MLX90632\x10\x13\x12\x0b\n\x07OPT3001\x10\x14\x12\x0c\n\x08LTR390UV\x10\x15\x12\x0e\n\nTSL25911FN\x10\x16\x12\t\n\x05\x41HT10\x10\x17\x12\x10\n\x0c\x44\x46ROBOT_LARK\x10\x18\x42\x64\n\x13\x63om.geeksville.meshB\x0fTelemetryProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals())
|
||||
@@ -21,16 +21,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_TELEMETRYSENSORTYPE._serialized_start=1041
|
||||
_TELEMETRYSENSORTYPE._serialized_end=1265
|
||||
_DEVICEMETRICS._serialized_start=42
|
||||
_DEVICEMETRICS._serialized_end=147
|
||||
_ENVIRONMENTMETRICS._serialized_start=150
|
||||
_ENVIRONMENTMETRICS._serialized_end=305
|
||||
_POWERMETRICS._serialized_start=308
|
||||
_POWERMETRICS._serialized_end=448
|
||||
_AIRQUALITYMETRICS._serialized_start=451
|
||||
_AIRQUALITYMETRICS._serialized_end=770
|
||||
_TELEMETRY._serialized_start=773
|
||||
_TELEMETRY._serialized_end=1038
|
||||
_TELEMETRYSENSORTYPE._serialized_start=1205
|
||||
_TELEMETRYSENSORTYPE._serialized_end=1554
|
||||
_DEVICEMETRICS._serialized_start=43
|
||||
_DEVICEMETRICS._serialized_end=172
|
||||
_ENVIRONMENTMETRICS._serialized_start=175
|
||||
_ENVIRONMENTMETRICS._serialized_end=469
|
||||
_POWERMETRICS._serialized_start=472
|
||||
_POWERMETRICS._serialized_end=612
|
||||
_AIRQUALITYMETRICS._serialized_start=615
|
||||
_AIRQUALITYMETRICS._serialized_end=934
|
||||
_TELEMETRY._serialized_start=937
|
||||
_TELEMETRY._serialized_end=1202
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -86,6 +86,42 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
||||
"""
|
||||
BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280)
|
||||
"""
|
||||
RCWL9620: _TelemetrySensorType.ValueType # 16
|
||||
"""
|
||||
RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
|
||||
"""
|
||||
SHT4X: _TelemetrySensorType.ValueType # 17
|
||||
"""
|
||||
Sensirion High accuracy temperature and humidity
|
||||
"""
|
||||
VEML7700: _TelemetrySensorType.ValueType # 18
|
||||
"""
|
||||
VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
|
||||
"""
|
||||
MLX90632: _TelemetrySensorType.ValueType # 19
|
||||
"""
|
||||
MLX90632 non-contact IR temperature sensor.
|
||||
"""
|
||||
OPT3001: _TelemetrySensorType.ValueType # 20
|
||||
"""
|
||||
TI OPT3001 Ambient Light Sensor
|
||||
"""
|
||||
LTR390UV: _TelemetrySensorType.ValueType # 21
|
||||
"""
|
||||
Lite On LTR-390UV-01 UV Light Sensor
|
||||
"""
|
||||
TSL25911FN: _TelemetrySensorType.ValueType # 22
|
||||
"""
|
||||
AMS TSL25911FN RGB Light Sensor
|
||||
"""
|
||||
AHT10: _TelemetrySensorType.ValueType # 23
|
||||
"""
|
||||
AHT10 Integrated temperature and humidity sensor
|
||||
"""
|
||||
DFROBOT_LARK: _TelemetrySensorType.ValueType # 24
|
||||
"""
|
||||
DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction)
|
||||
"""
|
||||
|
||||
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
|
||||
"""
|
||||
@@ -156,6 +192,42 @@ BMP085: TelemetrySensorType.ValueType # 15
|
||||
"""
|
||||
BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280)
|
||||
"""
|
||||
RCWL9620: TelemetrySensorType.ValueType # 16
|
||||
"""
|
||||
RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
|
||||
"""
|
||||
SHT4X: TelemetrySensorType.ValueType # 17
|
||||
"""
|
||||
Sensirion High accuracy temperature and humidity
|
||||
"""
|
||||
VEML7700: TelemetrySensorType.ValueType # 18
|
||||
"""
|
||||
VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
|
||||
"""
|
||||
MLX90632: TelemetrySensorType.ValueType # 19
|
||||
"""
|
||||
MLX90632 non-contact IR temperature sensor.
|
||||
"""
|
||||
OPT3001: TelemetrySensorType.ValueType # 20
|
||||
"""
|
||||
TI OPT3001 Ambient Light Sensor
|
||||
"""
|
||||
LTR390UV: TelemetrySensorType.ValueType # 21
|
||||
"""
|
||||
Lite On LTR-390UV-01 UV Light Sensor
|
||||
"""
|
||||
TSL25911FN: TelemetrySensorType.ValueType # 22
|
||||
"""
|
||||
AMS TSL25911FN RGB Light Sensor
|
||||
"""
|
||||
AHT10: TelemetrySensorType.ValueType # 23
|
||||
"""
|
||||
AHT10 Integrated temperature and humidity sensor
|
||||
"""
|
||||
DFROBOT_LARK: TelemetrySensorType.ValueType # 24
|
||||
"""
|
||||
DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction)
|
||||
"""
|
||||
global___TelemetrySensorType = TelemetrySensorType
|
||||
|
||||
@typing_extensions.final
|
||||
@@ -170,6 +242,7 @@ class DeviceMetrics(google.protobuf.message.Message):
|
||||
VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CHANNEL_UTILIZATION_FIELD_NUMBER: builtins.int
|
||||
AIR_UTIL_TX_FIELD_NUMBER: builtins.int
|
||||
UPTIME_SECONDS_FIELD_NUMBER: builtins.int
|
||||
battery_level: builtins.int
|
||||
"""
|
||||
0-100 (>100 means powered)
|
||||
@@ -186,6 +259,10 @@ class DeviceMetrics(google.protobuf.message.Message):
|
||||
"""
|
||||
Percent of airtime for transmission used within the last hour.
|
||||
"""
|
||||
uptime_seconds: builtins.int
|
||||
"""
|
||||
How long the device has been running since the last reboot (in seconds)
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -193,8 +270,9 @@ class DeviceMetrics(google.protobuf.message.Message):
|
||||
voltage: builtins.float = ...,
|
||||
channel_utilization: builtins.float = ...,
|
||||
air_util_tx: builtins.float = ...,
|
||||
uptime_seconds: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["air_util_tx", b"air_util_tx", "battery_level", b"battery_level", "channel_utilization", b"channel_utilization", "voltage", b"voltage"]) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["air_util_tx", b"air_util_tx", "battery_level", b"battery_level", "channel_utilization", b"channel_utilization", "uptime_seconds", b"uptime_seconds", "voltage", b"voltage"]) -> None: ...
|
||||
|
||||
global___DeviceMetrics = DeviceMetrics
|
||||
|
||||
@@ -212,6 +290,14 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
||||
GAS_RESISTANCE_FIELD_NUMBER: builtins.int
|
||||
VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CURRENT_FIELD_NUMBER: builtins.int
|
||||
IAQ_FIELD_NUMBER: builtins.int
|
||||
DISTANCE_FIELD_NUMBER: builtins.int
|
||||
LUX_FIELD_NUMBER: builtins.int
|
||||
WHITE_LUX_FIELD_NUMBER: builtins.int
|
||||
IR_LUX_FIELD_NUMBER: builtins.int
|
||||
UV_LUX_FIELD_NUMBER: builtins.int
|
||||
WIND_DIRECTION_FIELD_NUMBER: builtins.int
|
||||
WIND_SPEED_FIELD_NUMBER: builtins.int
|
||||
temperature: builtins.float
|
||||
"""
|
||||
Temperature measured
|
||||
@@ -236,6 +322,40 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
||||
"""
|
||||
Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
|
||||
"""
|
||||
iaq: builtins.int
|
||||
"""
|
||||
relative scale IAQ value as measured by Bosch BME680 . value 0-500.
|
||||
Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here.
|
||||
"""
|
||||
distance: builtins.float
|
||||
"""
|
||||
RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm.
|
||||
"""
|
||||
lux: builtins.float
|
||||
"""
|
||||
VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
|
||||
"""
|
||||
white_lux: builtins.float
|
||||
"""
|
||||
VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor.
|
||||
"""
|
||||
ir_lux: builtins.float
|
||||
"""
|
||||
Infrared lux
|
||||
"""
|
||||
uv_lux: builtins.float
|
||||
"""
|
||||
Ultraviolet lux
|
||||
"""
|
||||
wind_direction: builtins.int
|
||||
"""
|
||||
Wind direction in degrees
|
||||
0 degrees = North, 90 = East, etc...
|
||||
"""
|
||||
wind_speed: builtins.float
|
||||
"""
|
||||
Wind speed in m/s
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -245,8 +365,16 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
||||
gas_resistance: builtins.float = ...,
|
||||
voltage: builtins.float = ...,
|
||||
current: builtins.float = ...,
|
||||
iaq: builtins.int = ...,
|
||||
distance: builtins.float = ...,
|
||||
lux: builtins.float = ...,
|
||||
white_lux: builtins.float = ...,
|
||||
ir_lux: builtins.float = ...,
|
||||
uv_lux: builtins.float = ...,
|
||||
wind_direction: builtins.int = ...,
|
||||
wind_speed: builtins.float = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["barometric_pressure", b"barometric_pressure", "current", b"current", "gas_resistance", b"gas_resistance", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "voltage", b"voltage"]) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "relative_humidity", b"relative_humidity", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_speed", b"wind_speed"]) -> None: ...
|
||||
|
||||
global___EnvironmentMetrics = EnvironmentMetrics
|
||||
|
||||
|
||||
@@ -5,18 +5,18 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from meshtastic.__main__ import Globals
|
||||
from meshtastic import mt_config
|
||||
|
||||
from ..mesh_interface import MeshInterface
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_globals():
|
||||
"""Fixture to reset globals."""
|
||||
def reset_mt_config():
|
||||
"""Fixture to reset mt_config."""
|
||||
parser = None
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
Globals.getInstance().reset()
|
||||
Globals.getInstance().set_parser(parser)
|
||||
mt_config.reset()
|
||||
mt_config.parser = parser
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Meshtastic unit tests for globals.py
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from ..globals import Globals
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_globals_get_instaance():
|
||||
"""Test that we can instantiate a Globals instance"""
|
||||
ourglobals = Globals.getInstance()
|
||||
ourglobals2 = Globals.getInstance()
|
||||
assert ourglobals == ourglobals2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_globals_there_can_be_only_one():
|
||||
"""Test that we can cannot create two Globals instances"""
|
||||
# if we have an instance, delete it
|
||||
Globals.getInstance()
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
# try to create another instance
|
||||
Globals()
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
@@ -6,9 +6,8 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from meshtastic import _onNodeInfoReceive, _onPositionReceive, _onTextReceive
|
||||
from meshtastic import _onNodeInfoReceive, _onPositionReceive, _onTextReceive, mt_config
|
||||
|
||||
from ..globals import Globals
|
||||
from ..serial_interface import SerialInterface
|
||||
|
||||
|
||||
@@ -16,7 +15,7 @@ from ..serial_interface import SerialInterface
|
||||
def test_init_onTextReceive_with_exception(caplog):
|
||||
"""Test _onTextReceive"""
|
||||
args = MagicMock()
|
||||
Globals.getInstance().set_args(args)
|
||||
mt_config.args = args
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
packet = {}
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
@@ -29,7 +28,7 @@ def test_init_onTextReceive_with_exception(caplog):
|
||||
def test_init_onPositionReceive(caplog):
|
||||
"""Test _onPositionReceive"""
|
||||
args = MagicMock()
|
||||
Globals.getInstance().set_args(args)
|
||||
mt_config.args = args
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
packet = {"from": "foo", "decoded": {"position": {}}}
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
@@ -41,11 +40,11 @@ def test_init_onPositionReceive(caplog):
|
||||
def test_init_onNodeInfoReceive(caplog, iface_with_nodes):
|
||||
"""Test _onNodeInfoReceive"""
|
||||
args = MagicMock()
|
||||
Globals.getInstance().set_args(args)
|
||||
mt_config.args = args
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
packet = {
|
||||
"from": "foo",
|
||||
"from": 4808675309,
|
||||
"decoded": {
|
||||
"user": {
|
||||
"id": "bar",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,10 @@ import re
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from .. import mesh_pb2, BROADCAST_ADDR, LOCAL_ADDR
|
||||
from ..mesh_interface import MeshInterface
|
||||
from .. import mesh_pb2, config_pb2, BROADCAST_ADDR, LOCAL_ADDR
|
||||
from ..mesh_interface import MeshInterface, _timeago
|
||||
from ..node import Node
|
||||
|
||||
# TODO
|
||||
@@ -16,16 +17,17 @@ from ..util import Timeout
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_MeshInterface(capsys):
|
||||
"""Test that we can instantiate a MeshInterface"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
|
||||
nodes = {
|
||||
"!9388f81c": {
|
||||
"num": 2475227164,
|
||||
NODE_ID = "!9388f81c"
|
||||
NODE_NUM = 2475227164
|
||||
node = {
|
||||
"num": NODE_NUM,
|
||||
"user": {
|
||||
"id": "!9388f81c",
|
||||
"id": NODE_ID,
|
||||
"longName": "Unknown f81c",
|
||||
"shortName": "?1C",
|
||||
"macaddr": "RBeTiPgc",
|
||||
@@ -34,14 +36,16 @@ def test_MeshInterface(capsys):
|
||||
"position": {},
|
||||
"lastHeard": 1640204888,
|
||||
}
|
||||
}
|
||||
|
||||
iface.nodesByNum = {2475227164: nodes["!9388f81c"]}
|
||||
iface.nodes = nodes
|
||||
|
||||
iface.nodes = {NODE_ID: node}
|
||||
iface.nodesByNum = {NODE_NUM: node}
|
||||
|
||||
myInfo = MagicMock()
|
||||
iface.myInfo = myInfo
|
||||
|
||||
iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
|
||||
|
||||
iface.showInfo()
|
||||
iface.localNode.showInfo()
|
||||
iface.showNodes()
|
||||
@@ -57,7 +61,7 @@ def test_MeshInterface(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getMyUser(iface_with_nodes):
|
||||
"""Test getMyUser()"""
|
||||
iface = iface_with_nodes
|
||||
@@ -68,7 +72,7 @@ def test_getMyUser(iface_with_nodes):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getLongName(iface_with_nodes):
|
||||
"""Test getLongName()"""
|
||||
iface = iface_with_nodes
|
||||
@@ -78,7 +82,7 @@ def test_getLongName(iface_with_nodes):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getShortName(iface_with_nodes):
|
||||
"""Test getShortName()."""
|
||||
iface = iface_with_nodes
|
||||
@@ -88,7 +92,7 @@ def test_getShortName(iface_with_nodes):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handlePacketFromRadio_no_from(capsys):
|
||||
"""Test _handlePacketFromRadio with no 'from' in the mesh packet."""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -100,7 +104,7 @@ def test_handlePacketFromRadio_no_from(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handlePacketFromRadio_with_a_portnum(caplog):
|
||||
"""Test _handlePacketFromRadio with a portnum
|
||||
Since we have an attribute called 'from', we cannot simply 'set' it.
|
||||
@@ -116,7 +120,7 @@ def test_handlePacketFromRadio_with_a_portnum(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handlePacketFromRadio_no_portnum(caplog):
|
||||
"""Test _handlePacketFromRadio without a portnum"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -128,7 +132,7 @@ def test_handlePacketFromRadio_no_portnum(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getNode_with_local():
|
||||
"""Test getNode"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -137,7 +141,7 @@ def test_getNode_with_local():
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getNode_not_local(caplog):
|
||||
"""Test getNode not local"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -150,7 +154,7 @@ def test_getNode_not_local(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getNode_not_local_timeout(capsys):
|
||||
"""Test getNode not local, simulate timeout"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -167,7 +171,7 @@ def test_getNode_not_local_timeout(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPosition(caplog):
|
||||
"""Test sendPosition"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -179,7 +183,7 @@ def test_sendPosition(caplog):
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unit
|
||||
# @pytest.mark.usefixtures("reset_globals")
|
||||
# @pytest.mark.usefixtures("reset_mt_config")
|
||||
# def test_close_with_heartbeatTimer(caplog):
|
||||
# """Test close() with heartbeatTimer"""
|
||||
# iface = MeshInterface(noProto=True)
|
||||
@@ -197,7 +201,7 @@ def test_sendPosition(caplog):
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unit
|
||||
# @pytest.mark.usefixtures("reset_globals")
|
||||
# @pytest.mark.usefixtures("reset_mt_config")
|
||||
# def test_handleFromRadio_empty_payload(caplog):
|
||||
# """Test _handleFromRadio"""
|
||||
# iface = MeshInterface(noProto=True)
|
||||
@@ -208,7 +212,7 @@ def test_sendPosition(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handleFromRadio_with_my_info(caplog):
|
||||
"""Test _handleFromRadio with my_info"""
|
||||
# Note: I captured the '--debug --info' for the bytes below.
|
||||
@@ -233,7 +237,7 @@ def test_handleFromRadio_with_my_info(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handleFromRadio_with_node_info(caplog, capsys):
|
||||
"""Test _handleFromRadio with node_info"""
|
||||
# Note: I captured the '--debug --info' for the bytes below.
|
||||
@@ -269,7 +273,7 @@ def test_handleFromRadio_with_node_info(caplog, capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handleFromRadio_with_node_info_tbeam1(caplog, capsys):
|
||||
"""Test _handleFromRadio with node_info"""
|
||||
# Note: Captured the '--debug --info' for the bytes below.
|
||||
@@ -293,7 +297,7 @@ def test_handleFromRadio_with_node_info_tbeam1(caplog, capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handleFromRadio_with_node_info_tbeam_with_bad_data(caplog):
|
||||
"""Test _handleFromRadio with node_info with some bad data (issue#172) - ensure we do not throw exception"""
|
||||
# Note: Captured the '--debug --info' for the bytes below.
|
||||
@@ -305,7 +309,7 @@ def test_handleFromRadio_with_node_info_tbeam_with_bad_data(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_MeshInterface_sendToRadioImpl(caplog):
|
||||
"""Test _sendToRadioImp()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -316,7 +320,7 @@ def test_MeshInterface_sendToRadioImpl(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_MeshInterface_sendToRadio_no_proto(caplog):
|
||||
"""Test sendToRadio()"""
|
||||
iface = MeshInterface()
|
||||
@@ -327,7 +331,7 @@ def test_MeshInterface_sendToRadio_no_proto(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendData_too_long(caplog):
|
||||
"""Test when data payload is too big"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -352,7 +356,7 @@ def test_sendData_too_long(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendData_unknown_app(capsys):
|
||||
"""Test sendData when unknown app"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -366,7 +370,7 @@ def test_sendData_unknown_app(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPosition_with_a_position(caplog):
|
||||
"""Test sendPosition when lat/long/alt"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -378,7 +382,7 @@ def test_sendPosition_with_a_position(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_no_destination(capsys):
|
||||
"""Test _sendPacket()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -392,7 +396,7 @@ def test_sendPacket_with_no_destination(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_destination_as_int(caplog):
|
||||
"""Test _sendPacket() with int as a destination"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -403,7 +407,7 @@ def test_sendPacket_with_destination_as_int(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_destination_starting_with_a_bang(caplog):
|
||||
"""Test _sendPacket() with int as a destination"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -414,7 +418,7 @@ def test_sendPacket_with_destination_starting_with_a_bang(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog):
|
||||
"""Test _sendPacket() with BROADCAST_ADDR as a destination"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -425,7 +429,7 @@ def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys):
|
||||
"""Test _sendPacket() with LOCAL_ADDR as a destination with no myInfo"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -440,7 +444,7 @@ def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog):
|
||||
"""Test _sendPacket() with LOCAL_ADDR as a destination with myInfo"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -454,7 +458,7 @@ def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_destination_is_blank_with_nodes(capsys, iface_with_nodes):
|
||||
"""Test _sendPacket() with '' as a destination with myInfo"""
|
||||
iface = iface_with_nodes
|
||||
@@ -469,7 +473,7 @@ def test_sendPacket_with_destination_is_blank_with_nodes(capsys, iface_with_node
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_sendPacket_with_destination_is_blank_without_nodes(caplog, iface_with_nodes):
|
||||
"""Test _sendPacket() with '' as a destination with myInfo"""
|
||||
iface = iface_with_nodes
|
||||
@@ -481,7 +485,7 @@ def test_sendPacket_with_destination_is_blank_without_nodes(caplog, iface_with_n
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getMyNodeInfo():
|
||||
"""Test getMyNodeInfo()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -496,7 +500,7 @@ def test_getMyNodeInfo():
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_generatePacketId(capsys):
|
||||
"""Test _generatePacketId() when no currentPacketId (not connected)"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -514,7 +518,7 @@ def test_generatePacketId(capsys):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_fixupPosition_empty_pos():
|
||||
"""Test _fixupPosition()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -524,7 +528,7 @@ def test_fixupPosition_empty_pos():
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_fixupPosition_no_changes_needed():
|
||||
"""Test _fixupPosition()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -534,7 +538,7 @@ def test_fixupPosition_no_changes_needed():
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_fixupPosition():
|
||||
"""Test _fixupPosition()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
@@ -549,7 +553,7 @@ def test_fixupPosition():
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_nodeNumToId(iface_with_nodes):
|
||||
"""Test _nodeNumToId()"""
|
||||
iface = iface_with_nodes
|
||||
@@ -559,7 +563,7 @@ def test_nodeNumToId(iface_with_nodes):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_nodeNumToId_not_found(iface_with_nodes):
|
||||
"""Test _nodeNumToId()"""
|
||||
iface = iface_with_nodes
|
||||
@@ -569,7 +573,7 @@ def test_nodeNumToId_not_found(iface_with_nodes):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_nodeNumToId_to_all(iface_with_nodes):
|
||||
"""Test _nodeNumToId()"""
|
||||
iface = iface_with_nodes
|
||||
@@ -579,17 +583,17 @@ def test_nodeNumToId_to_all(iface_with_nodes):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getOrCreateByNum_minimal(iface_with_nodes):
|
||||
"""Test _getOrCreateByNum()"""
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
tmp = iface._getOrCreateByNum(123)
|
||||
assert tmp == {"num": 123}
|
||||
assert tmp == {"num": 123, "user": {"hwModel": "UNSET", "id": "!0000007b", "shortName": "007b", "longName": "Meshtastic 007b"}}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getOrCreateByNum_not_found(iface_with_nodes):
|
||||
"""Test _getOrCreateByNum()"""
|
||||
iface = iface_with_nodes
|
||||
@@ -600,7 +604,7 @@ def test_getOrCreateByNum_not_found(iface_with_nodes):
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getOrCreateByNum(iface_with_nodes):
|
||||
"""Test _getOrCreateByNum()"""
|
||||
iface = iface_with_nodes
|
||||
@@ -681,3 +685,21 @@ def test_waitConnected_isConnected_timeout(capsys):
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"warn about something", err, re.MULTILINE)
|
||||
assert out == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_timeago():
|
||||
"""Test that the _timeago function returns sane values"""
|
||||
assert _timeago(0) == "now"
|
||||
assert _timeago(1) == "1 sec ago"
|
||||
assert _timeago(15) == "15 secs ago"
|
||||
assert _timeago(333) == "5 mins ago"
|
||||
assert _timeago(99999) == "1 day ago"
|
||||
assert _timeago(9999999) == "3 months ago"
|
||||
assert _timeago(-999) == "now"
|
||||
|
||||
@given(seconds=st.integers())
|
||||
def test_timeago_fuzz(seconds):
|
||||
"""Fuzz _timeago to ensure it works with any integer"""
|
||||
val = _timeago(seconds)
|
||||
assert re.match(r"(now|\d+ (secs?|mins?|hours?|days?|months?|years?))", val)
|
||||
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# from ..admin_pb2 import AdminMessage
|
||||
from .. import localonly_pb2, config_pb2
|
||||
from ..channel_pb2 import Channel # pylint: disable=E0611
|
||||
from ..node import Node
|
||||
from ..serial_interface import SerialInterface
|
||||
@@ -19,21 +19,26 @@ from ..mesh_interface import MeshInterface
|
||||
# from ..util import Timeout
|
||||
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unit
|
||||
# def test_node(capsys):
|
||||
# """Test that we can instantiate a Node"""
|
||||
# anode = Node('foo', 'bar')
|
||||
# radioConfig = RadioConfig()
|
||||
# anode.radioConfig = radioConfig
|
||||
# anode.showChannels()
|
||||
# anode.showInfo()
|
||||
# out, err = capsys.readouterr()
|
||||
# assert re.search(r'Preferences', out)
|
||||
# assert re.search(r'Channels', out)
|
||||
# assert re.search(r'Primary channel URL', out)
|
||||
# assert err == ''
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_node(capsys):
|
||||
"""Test that we can instantiate a Node"""
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
mo.localNode.getChannelByName.return_value = None
|
||||
mo.myInfo.max_channels = 8
|
||||
anode = Node(mo, "bar", noProto=True)
|
||||
lc = localonly_pb2.LocalConfig()
|
||||
anode.localConfig = lc
|
||||
lc.lora.CopyFrom(config_pb2.Config.LoRaConfig())
|
||||
anode.moduleConfig = localonly_pb2.LocalModuleConfig()
|
||||
anode.showInfo()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r'Preferences', out)
|
||||
assert re.search(r'Module preferences', out)
|
||||
assert re.search(r'Channels', out)
|
||||
assert re.search(r'Primary channel URL', out)
|
||||
assert not re.search(r'remote node', out)
|
||||
assert err == ''
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unit
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import mock_open, patch
|
||||
import pytest
|
||||
|
||||
from ..serial_interface import SerialInterface
|
||||
from .. import config_pb2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -20,6 +21,7 @@ def test_SerialInterface_single_port(
|
||||
):
|
||||
"""Test that we can instantiate a SerialInterface with a single port"""
|
||||
iface = SerialInterface(noProto=True)
|
||||
iface.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
|
||||
iface.showInfo()
|
||||
iface.localNode.showInfo()
|
||||
iface.close()
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_StreamInterface():
|
||||
|
||||
# Note: This takes a bit, so moving from unit to slow
|
||||
@pytest.mark.unitslow
|
||||
@pytest.mark.usefixtures("reset_globals")
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_with_noProto(caplog):
|
||||
"""Test that we can instantiate a StreamInterface based on nonProto
|
||||
and we can read/write bytes from a mocked stream
|
||||
@@ -41,7 +41,7 @@ def test_StreamInterface_with_noProto(caplog):
|
||||
### 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
|
||||
# @pytest.mark.usefixtures("reset_globals")
|
||||
# @pytest.mark.usefixtures("reset_mt_config")
|
||||
# def test_sendToRadioImpl(caplog):
|
||||
# """Test _sendToRadioImpl()"""
|
||||
#
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from .. import config_pb2
|
||||
from ..tcp_interface import TCPInterface
|
||||
|
||||
|
||||
@@ -13,6 +14,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.localNode.localConfig.lora.CopyFrom(config_pb2.Config.LoRaConfig())
|
||||
iface.myConnect()
|
||||
iface.showInfo()
|
||||
iface.localNode.showInfo()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Meshtastic unit tests for tunnel.py"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
@@ -7,7 +6,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from ..globals import Globals
|
||||
from meshtastic import mt_config
|
||||
|
||||
from ..tcp_interface import TCPInterface
|
||||
from ..tunnel import Tunnel, onTunnelReceive
|
||||
|
||||
@@ -51,7 +51,7 @@ def test_Tunnel_with_interface(mock_platform_system, caplog, iface_with_nodes):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
with patch("socket.socket"):
|
||||
tun = Tunnel(iface)
|
||||
assert tun == Globals.getInstance().get_tunnelInstance()
|
||||
assert tun == mt_config.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)
|
||||
@@ -65,7 +65,7 @@ def test_onTunnelReceive_from_ourselves(mock_platform_system, caplog, iface_with
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
sys.argv = [""]
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
mt_config.args = sys.argv
|
||||
packet = {"decoded": {"payload": "foo"}, "from": 2475227164}
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = "Linux"
|
||||
@@ -73,7 +73,7 @@ def test_onTunnelReceive_from_ourselves(mock_platform_system, caplog, iface_with
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch("socket.socket"):
|
||||
tun = Tunnel(iface)
|
||||
Globals.getInstance().set_tunnelInstance(tun)
|
||||
mt_config.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)
|
||||
@@ -88,7 +88,7 @@ def test_onTunnelReceive_from_someone_else(
|
||||
iface = iface_with_nodes
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
sys.argv = [""]
|
||||
Globals.getInstance().set_args(sys.argv)
|
||||
mt_config.args = sys.argv
|
||||
packet = {"decoded": {"payload": "foo"}, "from": 123}
|
||||
a_mock = MagicMock()
|
||||
a_mock.return_value = "Linux"
|
||||
@@ -96,7 +96,7 @@ def test_onTunnelReceive_from_someone_else(
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch("socket.socket"):
|
||||
tun = Tunnel(iface)
|
||||
Globals.getInstance().set_tunnelInstance(tun)
|
||||
mt_config.tunnelInstance = tun
|
||||
onTunnelReceive(packet, iface)
|
||||
assert re.search(r"in onTunnelReceive", caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import re
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from meshtastic.supported_device import SupportedDevice
|
||||
from meshtastic.mesh_pb2 import MyNodeInfo
|
||||
@@ -33,6 +34,7 @@ from meshtastic.util import (
|
||||
stripnl,
|
||||
support_info,
|
||||
message_to_json,
|
||||
Acknowledgment
|
||||
)
|
||||
|
||||
|
||||
@@ -63,6 +65,7 @@ def test_fromStr():
|
||||
assert fromStr("123") == 123
|
||||
assert fromStr("abc") == "abc"
|
||||
assert fromStr("123456789") == 123456789
|
||||
assert fromStr("base64:Zm9vIGJhciBiYXo=") == b"foo bar baz"
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
@@ -555,3 +558,39 @@ def test_message_to_json_shows_all():
|
||||
actual = json.loads(message_to_json(MyNodeInfo()))
|
||||
expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0 }
|
||||
assert actual == expected
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_acknowledgement_reset():
|
||||
"""
|
||||
Test that the reset method can set all fields back to False
|
||||
"""
|
||||
test_ack_obj = Acknowledgment()
|
||||
# everything's set to False; let's set it to True to get a good test
|
||||
test_ack_obj.receivedAck = True
|
||||
test_ack_obj.receivedNak = True
|
||||
test_ack_obj.receivedImplAck = True
|
||||
test_ack_obj.receivedTraceRoute = True
|
||||
test_ack_obj.receivedTelemetry = True
|
||||
test_ack_obj.receivedPosition = True
|
||||
test_ack_obj.reset()
|
||||
assert test_ack_obj.receivedAck is False
|
||||
assert test_ack_obj.receivedNak is False
|
||||
assert test_ack_obj.receivedImplAck is False
|
||||
assert test_ack_obj.receivedTraceRoute is False
|
||||
assert test_ack_obj.receivedTelemetry is False
|
||||
assert test_ack_obj.receivedPosition is False
|
||||
|
||||
@given(a_string=st.text(
|
||||
alphabet=st.characters(
|
||||
codec='ascii',
|
||||
min_codepoint=0x5F,
|
||||
max_codepoint=0x7A,
|
||||
exclude_characters=r'`',
|
||||
)).filter(
|
||||
lambda x: x not in [''] and x[0] not in "_" and x[-1] not in '_' and not re.search(r'__', x)
|
||||
))
|
||||
def test_roundtrip_snake_to_camel_camel_to_snake(a_string):
|
||||
"""Test that snake_to_camel and camel_to_snake roundtrip each other"""
|
||||
value0 = snake_to_camel(a_string=a_string)
|
||||
value1 = camel_to_snake(a_string=value0)
|
||||
assert a_string == value1, (a_string, value1)
|
||||
|
||||
@@ -22,16 +22,14 @@ import threading
|
||||
from pubsub import pub # type: ignore[import-untyped]
|
||||
from pytap2 import TapDevice
|
||||
|
||||
from meshtastic import portnums_pb2
|
||||
from meshtastic.globals import Globals
|
||||
from meshtastic import portnums_pb2, mt_config
|
||||
from meshtastic.util import ipstr, readnet_u16
|
||||
|
||||
|
||||
def onTunnelReceive(packet, interface): # pylint: disable=W0613
|
||||
"""Callback for received tunneled messages from mesh."""
|
||||
logging.debug(f"in onTunnelReceive()")
|
||||
our_globals = Globals.getInstance()
|
||||
tunnelInstance = our_globals.get_tunnelInstance()
|
||||
tunnelInstance = mt_config.tunnelInstance
|
||||
tunnelInstance.onReceive(packet)
|
||||
|
||||
|
||||
@@ -67,8 +65,7 @@ class Tunnel:
|
||||
if platform.system() != "Linux":
|
||||
raise Tunnel.TunnelError("Tunnel() can only be run instantiated on a Linux system")
|
||||
|
||||
our_globals = Globals.getInstance()
|
||||
our_globals.set_tunnelInstance(self)
|
||||
mt_config.tunnelInstance = self
|
||||
|
||||
"""A list of chatty UDP services we should never accidentally
|
||||
forward to our slow network"""
|
||||
|
||||
@@ -11,7 +11,10 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
from queue import Queue
|
||||
from typing import List, NoReturn, Union
|
||||
|
||||
from google.protobuf.json_format import MessageToJson
|
||||
from google.protobuf.message import Message
|
||||
|
||||
import packaging.version as pkg_version
|
||||
import requests
|
||||
@@ -21,8 +24,16 @@ import serial.tools.list_ports # type: ignore[import-untyped]
|
||||
from meshtastic.supported_device import supported_devices
|
||||
from meshtastic.version import get_active_version
|
||||
|
||||
"""Some devices such as a seger jlink we never want to accidentally open"""
|
||||
blacklistVids = dict.fromkeys([0x1366])
|
||||
"""Some devices such as a seger jlink or st-link we never want to accidentally open
|
||||
0x1915 NordicSemi (PPK2)
|
||||
"""
|
||||
blacklistVids = dict.fromkeys([0x1366, 0x0483, 0x1915])
|
||||
|
||||
"""Some devices are highly likely to be meshtastic.
|
||||
0x239a RAK4631
|
||||
0x303a Heltec tracker"""
|
||||
whitelistVids = dict.fromkeys([0x239a, 0x303a])
|
||||
|
||||
|
||||
def quoteBooleans(a_string):
|
||||
"""Quote booleans
|
||||
@@ -101,7 +112,7 @@ def pskToString(psk: bytes):
|
||||
return "secret"
|
||||
|
||||
|
||||
def stripnl(s):
|
||||
def stripnl(s) -> str:
|
||||
"""Remove newlines from a string (and remove extra whitespace)"""
|
||||
s = str(s).replace("\n", " ")
|
||||
return " ".join(s.split())
|
||||
@@ -120,26 +131,42 @@ def catchAndIgnore(reason, closure):
|
||||
logging.error(f"Exception thrown in {reason}: {ex}")
|
||||
|
||||
|
||||
def findPorts(eliminate_duplicates=False):
|
||||
def findPorts(eliminate_duplicates: bool=False) -> List[str]:
|
||||
"""Find all ports that might have meshtastic devices
|
||||
eliminate_duplicates will run the eliminate_duplicate_port() on the collection
|
||||
|
||||
Returns:
|
||||
list -- a list of device paths
|
||||
"""
|
||||
l = list(
|
||||
all_ports = serial.tools.list_ports.comports()
|
||||
|
||||
# look for 'likely' meshtastic devices
|
||||
ports = list(
|
||||
map(
|
||||
lambda port: port.device,
|
||||
filter(
|
||||
lambda port: port.vid is not None and port.vid not in blacklistVids,
|
||||
serial.tools.list_ports.comports(),
|
||||
lambda port: port.vid is not None and port.vid in whitelistVids,
|
||||
all_ports,
|
||||
),
|
||||
)
|
||||
)
|
||||
l.sort()
|
||||
|
||||
# if no likely devices, just list everything not blacklisted
|
||||
if len(ports) == 0:
|
||||
ports = list(
|
||||
map(
|
||||
lambda port: port.device,
|
||||
filter(
|
||||
lambda port: port.vid is not None and port.vid not in blacklistVids,
|
||||
all_ports,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
ports.sort()
|
||||
if eliminate_duplicates:
|
||||
l = eliminate_duplicate_port(l)
|
||||
return l
|
||||
ports = eliminate_duplicate_port(ports)
|
||||
return ports
|
||||
|
||||
|
||||
class dotdict(dict):
|
||||
@@ -153,16 +180,16 @@ class dotdict(dict):
|
||||
class Timeout:
|
||||
"""Timeout class"""
|
||||
|
||||
def __init__(self, maxSecs=20):
|
||||
self.expireTime = 0
|
||||
self.sleepInterval = 0.1
|
||||
self.expireTimeout = maxSecs
|
||||
def __init__(self, maxSecs: int=20):
|
||||
self.expireTime: Union[int, float] = 0
|
||||
self.sleepInterval: float = 0.1
|
||||
self.expireTimeout: int = maxSecs
|
||||
|
||||
def reset(self):
|
||||
"""Restart the waitForSet timer"""
|
||||
self.expireTime = time.time() + self.expireTimeout
|
||||
|
||||
def waitForSet(self, target, attrs=()):
|
||||
def waitForSet(self, target, attrs=()) -> bool:
|
||||
"""Block until the specified attributes are set. Returns True if config has been received."""
|
||||
self.reset()
|
||||
while time.time() < self.expireTime:
|
||||
@@ -173,7 +200,7 @@ class Timeout:
|
||||
|
||||
def waitForAckNak(
|
||||
self, acknowledgment, attrs=("receivedAck", "receivedNak", "receivedImplAck")
|
||||
):
|
||||
) -> bool:
|
||||
"""Block until an ACK or NAK has been received. Returns True if ACK or NAK has been received."""
|
||||
self.reset()
|
||||
while time.time() < self.expireTime:
|
||||
@@ -183,7 +210,7 @@ class Timeout:
|
||||
time.sleep(self.sleepInterval)
|
||||
return False
|
||||
|
||||
def waitForTraceRoute(self, waitFactor, acknowledgment, attr="receivedTraceRoute"):
|
||||
def waitForTraceRoute(self, waitFactor, acknowledgment, attr="receivedTraceRoute") -> bool:
|
||||
"""Block until traceroute response is received. Returns True if traceroute response has been received."""
|
||||
self.expireTimeout *= waitFactor
|
||||
self.reset()
|
||||
@@ -194,7 +221,7 @@ class Timeout:
|
||||
time.sleep(self.sleepInterval)
|
||||
return False
|
||||
|
||||
def waitForTelemetry(self, acknowledgment):
|
||||
def waitForTelemetry(self, acknowledgment) -> bool:
|
||||
"""Block until telemetry response is received. Returns True if telemetry response has been received."""
|
||||
self.reset()
|
||||
while time.time() < self.expireTime:
|
||||
@@ -204,6 +231,16 @@ class Timeout:
|
||||
time.sleep(self.sleepInterval)
|
||||
return False
|
||||
|
||||
def waitForPosition(self, acknowledgment) -> bool:
|
||||
"""Block until position response is received. Returns True if position response has been received."""
|
||||
self.reset()
|
||||
while time.time() < self.expireTime:
|
||||
if getattr(acknowledgment, "receivedPosition", None):
|
||||
acknowledgment.reset()
|
||||
return True
|
||||
time.sleep(self.sleepInterval)
|
||||
return False
|
||||
|
||||
class Acknowledgment:
|
||||
"A class that records which type of acknowledgment was just received, if any."
|
||||
|
||||
@@ -214,6 +251,7 @@ class Acknowledgment:
|
||||
self.receivedImplAck = False
|
||||
self.receivedTraceRoute = False
|
||||
self.receivedTelemetry = False
|
||||
self.receivedPosition = False
|
||||
|
||||
def reset(self):
|
||||
"""reset"""
|
||||
@@ -222,6 +260,7 @@ class Acknowledgment:
|
||||
self.receivedImplAck = False
|
||||
self.receivedTraceRoute = False
|
||||
self.receivedTelemetry = False
|
||||
self.receivedPosition = False
|
||||
|
||||
|
||||
class DeferredExecution:
|
||||
@@ -249,7 +288,7 @@ class DeferredExecution:
|
||||
print(traceback.format_exc())
|
||||
|
||||
|
||||
def our_exit(message, return_value=1):
|
||||
def our_exit(message, return_value=1) -> NoReturn:
|
||||
"""Print the message and return a value.
|
||||
return_value defaults to 1 (non-successful)
|
||||
"""
|
||||
@@ -613,6 +652,8 @@ def check_if_newer_version():
|
||||
|
||||
return pypi_version
|
||||
|
||||
def message_to_json(message):
|
||||
"Return protobuf message as JSON. Always print all fields, even when not present in data."
|
||||
return stripnl(MessageToJson(message, always_print_fields_with_no_presence=True))
|
||||
|
||||
def message_to_json(message: Message, multiline: bool=False) -> str:
|
||||
"""Return protobuf message as JSON. Always print all fields, even when not present in data."""
|
||||
json = MessageToJson(message, always_print_fields_with_no_presence=True)
|
||||
return stripnl(json) if not multiline else json
|
||||
|
||||
1555
poetry.lock
generated
Normal file
1555
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Submodule protobufs updated: 68720ed8db...a641c5ce4f
51
pyproject.toml
Normal file
51
pyproject.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
[tool.poetry]
|
||||
name = "meshtastic"
|
||||
version = "2.3.11"
|
||||
description = "Python API & client shell for talking to Meshtastic devices"
|
||||
authors = ["Meshtastic Developers <contact@meshtastic.org>"]
|
||||
license = "GPL-3.0-only"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8,<3.13" # was 3.7 for production but, 3.8 is needed for pytap2, 3.9 is needed for pandas, bleak requires a max of 3.13 for some reason
|
||||
pyserial = "^3.5"
|
||||
protobuf = ">=5.26.0"
|
||||
dotmap = "^1.3.30"
|
||||
pexpect = "^4.9.0"
|
||||
pyqrcode = "^1.2.1"
|
||||
tabulate = "^0.9.0"
|
||||
webencodings = "^0.5.1"
|
||||
requests = "^2.31.0"
|
||||
pyparsing = "^3.1.2"
|
||||
pyyaml = "^6.0.1"
|
||||
pypubsub = "^4.0.3"
|
||||
bleak = "^0.21.1"
|
||||
packaging = "^24.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
hypothesis = "^6.103.2"
|
||||
pytest = "^8.2.2"
|
||||
pytest-cov = "^5.0.0"
|
||||
pdoc3 = "^0.10.0"
|
||||
autopep8 = "^2.1.0"
|
||||
pylint = "^3.2.3"
|
||||
pytap2 = "^2.3.0"
|
||||
pyinstaller = "^6.8.0"
|
||||
mypy = "^1.10.0"
|
||||
mypy-protobuf = "^3.6.0"
|
||||
types-protobuf = "^5.26.0.20240422"
|
||||
types-tabulate = "^0.9.0.20240106"
|
||||
types-requests = "^2.31.0.20240406"
|
||||
types-setuptools = "^69.5.0.20240423"
|
||||
types-pyyaml = "^6.0.12.20240311"
|
||||
|
||||
[tool.poetry.extras]
|
||||
tunnel = ["pytap2"]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
meshtastic = "meshtastic.__main__:main"
|
||||
mesh-tunnel = "meshtastic.__main__:tunnelMain [tunnel]"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -1,29 +0,0 @@
|
||||
markdown
|
||||
pyserial
|
||||
protobuf>=5.26.0
|
||||
dotmap
|
||||
pexpect
|
||||
pyqrcode
|
||||
tabulate
|
||||
timeago
|
||||
webencodings
|
||||
requests
|
||||
pyparsing
|
||||
twine
|
||||
autopep8
|
||||
pylint
|
||||
pytest
|
||||
pytest-cov
|
||||
pyyaml
|
||||
pytap2
|
||||
pdoc3
|
||||
pypubsub
|
||||
bleak
|
||||
packaging
|
||||
mypy
|
||||
mypy-protobuf
|
||||
types-protobuf
|
||||
types-tabulate
|
||||
types-requests
|
||||
types-setuptools
|
||||
types-PyYAML
|
||||
57
setup.py
57
setup.py
@@ -1,57 +0,0 @@
|
||||
# Note: you shouldn't need to run this script manually. It is run implicitly by the pip3 install command.
|
||||
|
||||
import pathlib
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# The directory containing this file
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
# This call to setup() does all the work
|
||||
setup(
|
||||
name="meshtastic",
|
||||
version="2.3.4.post1",
|
||||
description="Python API & client shell for talking to Meshtastic devices",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/meshtastic/python",
|
||||
author="Meshtastic Developers",
|
||||
author_email="contact@meshtastic.org",
|
||||
license="GPL-3.0-only",
|
||||
classifiers=[
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
],
|
||||
packages=["meshtastic"],
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"pyserial>=3.4",
|
||||
"protobuf>=5.26.0",
|
||||
"requests>=2.25.0",
|
||||
"pypubsub>=4.0.3",
|
||||
"dotmap>=1.3.14",
|
||||
"pexpect>=4.6.0",
|
||||
"pyqrcode>=1.2.1",
|
||||
"tabulate>=0.8.9",
|
||||
"timeago>=1.0.15",
|
||||
"pyyaml",
|
||||
"bleak>=0.21.1",
|
||||
"packaging",
|
||||
],
|
||||
extras_require={"tunnel": ["pytap2>=2.0.0"]},
|
||||
python_requires=">=3.7",
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"meshtastic=meshtastic.__main__:main",
|
||||
"mesh-tunnel=meshtastic.__main__:tunnelMain [tunnel]",
|
||||
]
|
||||
},
|
||||
)
|
||||
@@ -1,9 +1,7 @@
|
||||
import time
|
||||
|
||||
import meshtastic
|
||||
import meshtastic.serial_interface
|
||||
|
||||
interface = (
|
||||
meshtastic.SerialInterface()
|
||||
meshtastic.serial_interface.SerialInterface()
|
||||
) # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
|
||||
interface.sendText("hello mesh")
|
||||
interface.close()
|
||||
|
||||
Reference in New Issue
Block a user