Compare commits

...

58 Commits

Author SHA1 Message Date
Ian McEwen
96afa703ba output version number in correct format for github actions, hopefully 2024-06-25 19:07:35 -07:00
Ian McEwen
1b14b1ef20 Use poetry version --short for a valid tag name 2024-06-25 18:58:27 -07:00
Ian McEwen
68836b1af1 Merge pull request #606 from geeksville/pr-poetry
Change build to use poetry
2024-06-25 18:24:27 -07:00
Ian McEwen
195f0c9d90 drop timeago dep, concurrent PR 2024-06-25 18:24:04 -07:00
Ian McEwen
1ff7334385 Merge branch 'master' into pr-poetry 2024-06-25 18:22:02 -07:00
Ian McEwen
b15e27c7b6 Merge pull request #587 from FedericoCeratto/drop-timeago 2024-06-25 18:14:27 -07:00
Ian McEwen
267923fdc5 Add hypothesis fuzzing test for _timeago 2024-06-25 18:14:07 -07:00
Ian McEwen
9ab1b32bdb make pylint happy with a docstring 2024-06-25 18:09:20 -07:00
Ian McEwen
3a4795d3b8 Merge pull request #605 from geeksville/pr-whitelist
Add a whitelist of known meshtastic USB VIDs to use a default serial …
2024-06-25 12:53:39 -07:00
Ian McEwen
c2a2d5a77c Merge pull request #611 from geeksville/pr-fixbitrot
fix bitrot in an old sanity test - use correct namespace
2024-06-25 12:52:21 -07:00
Kevin Hester
b30cde979c fix bitrot in an old sanity test - use correct namespace 2024-06-25 11:31:02 -07:00
Kevin Hester
8456f36c6b add NordicSemi Power Profiler Kit 2 device to the USB blacklist 2024-06-23 17:18:04 -07:00
Kevin Hester
e6a88e055f fix #610: bump nanopb to 0.4.8
Including in the Poetry changes because it touches the same lines
and I want to avoid hand merging ;-)
2024-06-23 08:26:20 -07:00
Kevin Hester
7bea6f6120 For poetry change: need to put venv in our path so mypy protobuf plugin works 2024-06-22 23:25:42 -07:00
Kevin Hester
725de4c2f9 move mypy and type info into dev-time only dependencies thx @njh 2024-06-22 16:43:56 -07:00
geeksville
4203553a44 changes to (hopefully) make release CI actions work
add pyinstaller as a dev dep.  Use it to make "bin/build-bin.sh"
remove old version scripts (no longer needed with poetry)
2024-06-22 09:34:24 -07:00
geeksville
9e319f3c52 update CI scripts to allow running CI on the desktop (see below)
* add script to run 'act' local github actions tool (lets devs check github
actions on their local machine)
* Update various github actions to latest (so they can work with the 'act'
tool)
* change a few places where python version was not properly quoted as a
string (act yaml parser is more strict than the github version)
* update pylint min-version to work with recent github actions
* remove pandas/riden requirement (that's in my other branch for now)
2024-06-22 08:02:33 -07:00
geeksville
cd5913ae6d WIP switch to using Poetry for builds/dev usage. Looks pretty good
still need to update readme and pypi upload
2024-06-22 08:01:54 -07:00
geeksville
ccfb04720f Add a whitelist of known meshtastic USB VIDs to use a default serial ports.
Initially only RAK4631 and heltec tracker are listed
2024-06-21 14:42:29 -07:00
Federico Ceratto
c34d08b0e5 Refactor timeago and add tests
_timeago is not specialized for mesh interfaces so it is factored
out into a private function
2024-06-21 10:28:45 +02:00
Federico Ceratto
b5d1b7612f Replace timeago
Replace the timeago library with a simple function
2024-06-21 09:25:39 +02:00
Ian McEwen
b58094b9ce Merge pull request #602 from ianmcorvidae/improve-acks
Improve ACK handling: pass to `onAckNak` and on request in sendData
2024-06-20 17:16:36 -07:00
Ian McEwen
23f41bff0a Improve ACK handling: correctly pass them to onAckNak handlers, and add a mechanism for other handlers to request acks as well. 2024-06-20 16:52:01 -07:00
Ian McEwen
a1021c4f78 add dotmap back, that test file isn't in tests, it's in meshtastic.test, oops 2024-06-20 16:45:22 -07:00
Ian McEwen
b06329f47e Remove dotmap and pexpect from setup.py, they shouldn't be required 2024-06-20 16:40:46 -07:00
Ian McEwen
53b0e35b0c protobufs: v2.3.12 2024-06-09 19:30:11 -07:00
github-actions
9ac5aeeaf0 bump version 2024-06-08 15:10:49 +00:00
Ian McEwen
5c703aff1d Merge pull request #591 from ianmcorvidae/device-metadata-resp
Wait for response with --device-metadata. Fixes #527
2024-06-08 07:29:45 -07:00
Ian McEwen
5441266565 Wait for response with --device-metadata. Fixes #527 2024-06-08 07:26:20 -07:00
Ian McEwen
890557fa5d Merge branch 'more-tests' 2024-06-05 19:58:03 -07:00
Ian McEwen
e27d210a71 Test with --dest on setlat/remove-position for the error/exit case 2024-06-05 19:57:40 -07:00
Ian McEwen
16c08b8b47 Add simple --remove-position test 2024-06-05 19:54:12 -07:00
Ian McEwen
ebd3c7f5e8 Add test for fromStr base64 branch 2024-06-05 19:48:58 -07:00
Ian McEwen
da0312a5b0 more miscellaneous types 2024-06-05 19:44:18 -07:00
Ian McEwen
919ae8c40f make pylint happy, again 2024-06-05 19:32:45 -07:00
Ian McEwen
dd4fccbc77 Add a fairly simple property-based test as a starting point 2024-06-05 19:29:55 -07:00
Ian McEwen
32682b5230 Merge pull request #589 from ianmcorvidae/nodeless-startup
Allow a faster nodedb-less startup on 2.3.11+ with `--no-nodes`
2024-06-05 18:59:18 -07:00
Ian McEwen
9dab76bb64 quell pylint 2024-06-05 18:56:19 -07:00
Ian McEwen
e6d61c6603 Allow a faster nodedb-less startup on 2.3.11+ with --no-nodes and the magic value from meshtastic/firmware#3949 2024-06-05 18:52:35 -07:00
Ian McEwen
ee857c5128 Merge pull request #588 from nerdenator/quick-coverage
quick-coverage: simple test case just to cover uncovered code.
2024-06-03 23:22:33 -07:00
Nerdenator
87a4bb0888 quick-coverage: fixing linting issues. 2024-06-04 01:15:10 -05:00
Nerdenator
d72cc0e201 quick-coverage: simple test case just to cover uncovered code. 2024-06-04 01:01:32 -05:00
Ian McEwen
b350b9eab9 Update the main module docstring to be a bit more accurate, at least 2024-06-01 23:03:56 -07:00
Ian McEwen
dc112f2f3a protobufs: v2.3.11 2024-06-01 12:04:01 -07:00
github-actions
14ae4eeac1 bump version 2024-06-01 18:47:51 +00:00
Ian McEwen
bbc526d0a8 Merge pull request #584 from ianmcorvidae/improve-fixed-position
Use new fixed position admin messages and add `--remove-position` argument
2024-06-01 00:44:17 -07:00
Ian McEwen
abe98f5079 Merge pull request #585 from ianmcorvidae/position-rounding
Fix rounding of position values when converting from integer to float in _fixupPosition
2024-06-01 00:42:58 -07:00
Ian McEwen
e8dfee8454 Fix rounding of position values when converting from integer to float in _fixupPosition. Fixes #572 2024-05-31 18:57:30 -07:00
Ian McEwen
1746ad15d7 Use new fixed position admin messages and add --remove-position argument. Fixes #525 2024-05-31 18:44:33 -07:00
Ian McEwen
4d67e7fc76 Fix up/add some more types 2024-05-30 17:51:42 -07:00
Ian McEwen
3b112d2f49 Merge pull request #583 from ianmcorvidae/update-unknown-node-setup
Initialize unknown nodes more in line with meshtastic/design#16; show hardware in --nodes
2024-05-30 15:52:16 -07:00
Ian McEwen
93e9c1c66c Initialize unknown nodes more in line with meshtastic/design#16 2024-05-30 13:50:52 -07:00
Ian McEwen
8e641b3186 Merge pull request #581 from 868meshbot/868meshbot-fix-ignore-incoming
Fix the ignore_incoming management BUG 568
2024-05-26 00:03:11 -07:00
Ian McEwen
ed545cd9b4 Merge pull request #580 from todd-herbert/wait-to-disconnect
Add "wait to disconnect" argument
2024-05-26 00:02:08 -07:00
868meshbot
bcd60c9ef7 Update __main__.py
Simple patch to fix the ignore_incoming management aka
https://github.com/meshtastic/python/issues/568
2024-05-25 16:28:16 +01:00
Todd Herbert
c3d044e3f2 Optional pause before disconnecting 2024-05-24 17:10:07 +12:00
Ian McEwen
8d538e8f24 protobufs: v2.3.10 2024-05-18 12:56:41 -07:00
github-actions
fa1a3d7901 bump version 2024-05-18 00:15:11 +00:00
40 changed files with 2406 additions and 421 deletions

View File

@@ -18,30 +18,29 @@ jobs:
- "3.10" - "3.10"
- "3.11" - "3.11"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Python 3 - name: Install Python 3
uses: actions/setup-python@v1 uses: actions/setup-python@v5
- name: Uninstall meshtastic - name: Uninstall meshtastic
run: | run: |
pip3 uninstall meshtastic pip3 uninstall -y meshtastic
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip3 install -r requirements.txt pip3 install poetry
- name: Install meshtastic from local - name: Install meshtastic from local
run: | run: |
pip3 install . poetry install
which meshtastic poetry run meshtastic --version
meshtastic --version
- name: Run pylint - 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 - name: Check types with mypy
run: mypy meshtastic/ run: poetry run mypy meshtastic/
- name: Run tests with pytest - name: Run tests with pytest
run: pytest --cov=meshtastic run: poetry run pytest --cov=meshtastic
- name: Generate coverage report - name: Generate coverage report
run: | run: |
pytest --cov=meshtastic --cov-report=xml poetry run pytest --cov=meshtastic --cov-report=xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
with: with:
@@ -62,11 +61,12 @@ jobs:
- "3.10" - "3.10"
- "3.11" - "3.11"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Python 3 - name: Install Python 3
uses: actions/setup-python@v1 uses: actions/setup-python@v5
- name: Install meshtastic from local - name: Install meshtastic from local
run: | run: |
pip3 install . python -m pip install --upgrade pip
which meshtastic pip3 install poetry
meshtastic --version poetry install
poetry run meshtastic --version

View File

@@ -12,26 +12,36 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip3 install poetry
- name: Bump version - name: Bump version
run: >- run: >-
bin/bump_version.py poetry version patch
- name: Commit updated version.py - name: Commit updated version.
id: commit_updated id: commit_updated
run: | run: |
git config --global user.name 'github-actions' git config --global user.name 'github-actions'
git config --global user.email 'bot@noreply.github.com' 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 remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git add setup.py git add pyproject.toml
git commit -m "bump version" && git push || echo "No changes to commit" 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}' git log -n 1 --pretty=format:"%H" | tail -n 1 | awk '{print "::set-output name=sha::"$0}'
- name: Get version - name: Get version
id: get_version id: get_version
run: >- run: >-
bin/show_version.py poetry version --short | sed 's/^/::set-output name=version::/'
- name: Create GitHub release - name: Create GitHub release
uses: actions/create-release@v1 uses: actions/create-release@v1
@@ -47,26 +57,9 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 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 - name: Build a binary wheel and a source tarball
run: >- run: >-
python -m poetry build
build
--sdist
--wheel
--outdir dist/
.
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@master uses: pypa/gh-action-pypi-publish@master
@@ -79,14 +72,14 @@ jobs:
# needs: release_create # needs: release_create
# steps: # steps:
# - name: Checkout # - name: Checkout
# uses: actions/checkout@v3 # uses: actions/checkout@v4
# with: # with:
# ref: ${{ needs.release_create.outputs.new_sha }} # ref: ${{ needs.release_create.outputs.new_sha }}
# - name: Set up Python 3.9 # - name: Set up Python 3.9
# uses: actions/setup-python@v2 # uses: actions/setup-python@v5
# with: # with:
# python-version: 3.9 # python-version: "3.9"
# - name: Setup code signing # - name: Setup code signing
# env: # env:
@@ -125,21 +118,19 @@ jobs:
needs: release_create needs: release_create
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ needs.release_create.outputs.new_sha }} ref: ${{ needs.release_create.outputs.new_sha }}
- name: Set up Python 3.9 - name: Set up Python 3.9
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: 3.9 python-version: "3.9"
- name: Build - name: Build
run: | run: |
pip install pyinstaller pip install poetry
pip install -r requirements.txt bin/build-bin.sh
pip install .
pyinstaller -F -n meshtastic --collect-all meshtastic meshtastic/__main__.py
- name: Add ubuntu to release - name: Add ubuntu to release
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -166,21 +157,19 @@ jobs:
needs: release_create needs: release_create
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
ref: ${{ needs.release_create.outputs.new_sha }} ref: ${{ needs.release_create.outputs.new_sha }}
- name: Set up Python 3.9 - name: Set up Python 3.9
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: 3.9 python-version: "3.9"
- name: Build - name: Build
run: | run: |
pip install pyinstaller pip install poetry
pip install -r requirements.txt bin/build-bin.sh
pip install .
pyinstaller -F -n meshtastic --collect-all meshtastic meshtastic/__main__.py
- name: Add windows to release - name: Add windows to release
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1

View File

@@ -7,7 +7,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
@@ -18,9 +18,14 @@ jobs:
- name: Download nanopb - name: Download nanopb
run: | run: |
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.6-linux-x86.tar.gz wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
tar xvzf nanopb-0.4.6-linux-x86.tar.gz tar xvzf nanopb-0.4.8-linux-x86.tar.gz
mv nanopb-0.4.6-linux-x86 nanopb-0.4.6 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 - name: Re-generate protocol buffers
run: | run: |

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ venv/
__pycache__ __pycache__
examples/__pycache__ examples/__pycache__
meshtastic.spec meshtastic.spec
.hypothesis/
coverage.xml

9
bin/build-bin.sh Executable file
View 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

View File

@@ -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)

View File

@@ -3,20 +3,24 @@ set -e
# You may consider running: "pytest -m smoke1" instead of this test. # You may consider running: "pytest -m smoke1" instead of this test.
echo "Running (crude) prerelease tests to verify sanity" 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 echo running hello
python3 tests/hello_world.py python3 tests/hello_world.py
# bin/run.sh --help # meshtastic --help
echo toggling router echo toggling router
bin/run.sh --set is_router true meshtastic --set is_router true
bin/run.sh --set is_router false meshtastic --set is_router false
# TODO: This does not seem to work. # TODO: This does not seem to work.
echo setting channel 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 echo setting owner
bin/run.sh --set-owner "Test Build" meshtastic --set-owner "Test Build"
echo setting position 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 echo dumping info
bin/run.sh --info meshtastic run meshtastic --info
echo sending closing message echo sending closing message
bin/run.sh --sendtext "Sanity complete" meshtastic --sendtext "Sanity complete"

View File

@@ -4,8 +4,11 @@
#gsed -i 's/import "\//import ".\//g' ./protobufs/meshtastic/* #gsed -i 's/import "\//import ".\//g' ./protobufs/meshtastic/*
#gsed -i 's/package meshtastic;//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 # protoc looks for mypy plugin in the python path
./nanopb-0.4.7/generator-bin/protoc -I=protobufs --python_out ./meshtastic/ --mypy_out ./meshtastic/ ./protobufs/nanopb.proto 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 # workaround for import bug in protoc https://github.com/protocolbuffers/protobuf/issues/1491#issuecomment-690618628

11
bin/run-ci-local.sh Executable file
View 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" "$@"

View File

@@ -1,2 +0,0 @@
rm log_*
python3 -m meshtastic "$@"

View File

@@ -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()}")

View File

@@ -3,8 +3,6 @@ set -e
bin/regen-docs.sh bin/regen-docs.sh
pandoc --from=markdown --to=rst --output=README README.md pandoc --from=markdown --to=rst --output=README README.md
python3 setup.py sdist bdist_wheel
python3 -m twine check dist/* poetry publish -r test-pypi --build
# test the upload
python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
echo "view the upload at https://test.pypi.org/ it it looks good upload for real" echo "view the upload at https://test.pypi.org/ it it looks good upload for real"

View File

@@ -1,5 +1,8 @@
rm dist/* rm dist/*
set -e set -e
python3 setup.py sdist bdist_wheel poetry build
python3 -m twine upload dist/* poetry run pytest
poetry publish
#python3 setup.py sdist bdist_wheel
#python3 -m twine upload dist/*

View File

@@ -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/)" Install with pip: "[pip3 install meshtastic](https://pypi.org/project/meshtastic/)"
Source code on [github](https://github.com/meshtastic/python) 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 - 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. node in the mesh. This is a read-only datastructure.
- nodesByNum - like "nodes" but keyed by nodeNum instead of nodeId - 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 # Published PubSub topics
@@ -72,7 +76,6 @@ from typing import *
import google.protobuf.json_format import google.protobuf.json_format
import serial # type: ignore[import-untyped] import serial # type: ignore[import-untyped]
import timeago # type: ignore[import-untyped]
from dotmap import DotMap # type: ignore[import-untyped] from dotmap import DotMap # type: ignore[import-untyped]
from google.protobuf.json_format import MessageToJson from google.protobuf.json_format import MessageToJson
from pubsub import pub # type: ignore[import-untyped] 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 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") publishingThread = DeferredExecution("publishing")
@@ -121,6 +127,7 @@ class ResponseHandler(NamedTuple):
# requestId: int - used only as a key # requestId: int - used only as a key
callback: Callable callback: Callable
ackPermitted: bool = False
# FIXME, add timestamp and age out old requests # FIXME, add timestamp and age out old requests

View File

@@ -226,13 +226,14 @@ def setPref(config, comp_name, valStr) -> bool:
config_values = getattr(config_part, config_type.name) config_values = getattr(config_part, config_type.name)
setattr(config_values, pref.name, valStr) setattr(config_values, pref.name, valStr)
else: else:
config_values = getattr(config, config_type.name)
if val == 0: if val == 0:
# clear values # clear values
print("Clearing ignore_incoming list") print("Clearing ignore_incoming list")
del config_type.message_type.ignore_incoming[:] del config_values.ignore_incoming[:]
else: else:
print(f"Adding '{val}' to the ignore_incoming list") 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"{'.'.join(name[0:-1])}." if config_type.message_type is not None else "" prefix = f"{'.'.join(name[0:-1])}." if config_type.message_type is not None else ""
if mt_config.camel_case: if mt_config.camel_case:
@@ -256,33 +257,41 @@ def onConnected(interface):
if not args.export_config: if not args.export_config:
print("Connected to radio") 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: if args.dest != BROADCAST_ADDR:
print("Setting latitude, longitude, and altitude of remote nodes is not supported.") print("Setting latitude, longitude, and altitude of remote nodes is not supported.")
return return
closeNow = True closeNow = True
alt = 0 alt = 0
lat = 0.0 lat = 0
lon = 0.0 lon = 0
localConfig = interface.localNode.localConfig
if args.setalt: if args.setalt:
alt = int(args.setalt) alt = int(args.setalt)
localConfig.position.fixed_position = True
print(f"Fixing altitude at {alt} meters") print(f"Fixing altitude at {alt} meters")
if args.setlat: if args.setlat:
lat = float(args.setlat) try:
localConfig.position.fixed_position = True lat = int(args.setlat)
except ValueError:
lat = float(args.setlat)
print(f"Fixing latitude at {lat} degrees") print(f"Fixing latitude at {lat} degrees")
if args.setlon: if args.setlon:
lon = float(args.setlon) try:
localConfig.position.fixed_position = True lon = int(args.setlon)
except ValueError:
lon = float(args.setlon)
print(f"Fixing longitude at {lon} degrees") 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 # can include lat/long/alt etc: latitude = 37.5, longitude = -122.1
interface.sendPosition(lat, lon, alt) interface.localNode.setFixedPosition(lat, lon, alt)
interface.localNode.writeConfig("position")
elif not args.no_time: elif not args.no_time:
# We normally provide a current time to the mesh when we connect # We normally provide a current time to the mesh when we connect
if interface.localNode.nodeNum in interface.nodesByNum and "position" in interface.nodesByNum[interface.localNode.nodeNum]: if interface.localNode.nodeNum in interface.nodesByNum and "position" in interface.nodesByNum[interface.localNode.nodeNum]:
@@ -861,6 +870,10 @@ def onConnected(interface):
) )
interface.getNode(args.dest, False).iface.waitForAckNak() 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 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: if (not args.seriallog) and closeNow:
interface.close() # after running command then exit interface.close() # after running command then exit
@@ -1037,11 +1050,11 @@ def common():
meshtastic.util.our_exit("BLE scan finished", 0) meshtastic.util.our_exit("BLE scan finished", 0)
return return
elif args.ble: 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: elif args.host:
try: try:
client = meshtastic.tcp_interface.TCPInterface( 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: except Exception as ex:
meshtastic.util.our_exit( meshtastic.util.our_exit(
@@ -1050,7 +1063,7 @@ def common():
else: else:
try: try:
client = meshtastic.serial_interface.SerialInterface( 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: except PermissionError as ex:
username = os.getlogin() username = os.getlogin()
@@ -1065,7 +1078,7 @@ def common():
if client.devPath is None: if client.devPath is None:
try: try:
client = meshtastic.tcp_interface.TCPInterface( 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: except Exception as ex:
meshtastic.util.our_exit( meshtastic.util.our_exit(
@@ -1440,12 +1453,32 @@ def initParser():
action="store_true", action="store_true",
) )
group.add_argument("--setalt", help="Set device altitude in meters (allows use without GPS)") group.add_argument(
"--no-nodes",
group.add_argument("--setlat", help="Set device latitude (allows use without GPS)") 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( 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( group.add_argument(
@@ -1473,6 +1506,14 @@ def initParser():
action="store_true", 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( group.add_argument(
"--noproto", "--noproto",
help="Don't start the API, just function as a dumb serial terminal.", help="Don't start the API, just function as a dumb serial terminal.",

View File

@@ -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.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.atak_pb2', globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.atak_pb2', globals())
@@ -21,20 +21,20 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nATAKProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' 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_start=622
_TEAM._serialized_end=772 _TEAM._serialized_end=814
_MEMBERROLE._serialized_start=774 _MEMBERROLE._serialized_start=816
_MEMBERROLE._serialized_end=901 _MEMBERROLE._serialized_end=943
_TAKPACKET._serialized_start=38 _TAKPACKET._serialized_start=38
_TAKPACKET._serialized_end=268 _TAKPACKET._serialized_end=268
_GEOCHAT._serialized_start=270 _GEOCHAT._serialized_start=270
_GEOCHAT._serialized_end=320 _GEOCHAT._serialized_end=362
_GROUP._serialized_start=322 _GROUP._serialized_start=364
_GROUP._serialized_end=399 _GROUP._serialized_end=441
_STATUS._serialized_start=401 _STATUS._serialized_start=443
_STATUS._serialized_end=426 _STATUS._serialized_end=468
_CONTACT._serialized_start=428 _CONTACT._serialized_start=470
_CONTACT._serialized_end=480 _CONTACT._serialized_end=522
_PLI._serialized_start=482 _PLI._serialized_start=524
_PLI._serialized_end=577 _PLI._serialized_end=619
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -302,6 +302,7 @@ class GeoChat(google.protobuf.message.Message):
MESSAGE_FIELD_NUMBER: builtins.int MESSAGE_FIELD_NUMBER: builtins.int
TO_FIELD_NUMBER: builtins.int TO_FIELD_NUMBER: builtins.int
TO_CALLSIGN_FIELD_NUMBER: builtins.int
message: builtins.str message: builtins.str
""" """
The text message The text message
@@ -310,15 +311,23 @@ class GeoChat(google.protobuf.message.Message):
""" """
Uid recipient of the message Uid recipient of the message
""" """
to_callsign: builtins.str
"""
Callsign of the recipient for the message
"""
def __init__( def __init__(
self, self,
*, *,
message: builtins.str = ..., message: builtins.str = ...,
to: builtins.str | None = ..., to: builtins.str | None = ...,
to_callsign: builtins.str | None = ...,
) -> None: ... ) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["_to", b"_to", "to", b"to"]) -> builtins.bool: ... 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", "message", b"message", "to", b"to"]) -> None: ... 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: ... 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 global___GeoChat = GeoChat

View File

@@ -32,7 +32,7 @@ class BLEInterface(MeshInterface):
MESH = False MESH = False
def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None): def __init__(self, address: Optional[str], noProto: bool = False, debugOut = None, noNodes: bool = False):
self.state = BLEInterface.BLEState() self.state = BLEInterface.BLEState()
if not address: if not address:
@@ -60,7 +60,7 @@ class BLEInterface(MeshInterface):
return return
logging.debug("Mesh init starting") logging.debug("Mesh init starting")
MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto) MeshInterface.__init__(self, debugOut = debugOut, noProto = noProto, noNodes = noNodes)
self._startConfig() self._startConfig()
if not self.noProto: if not self.noProto:
self._waitConnected(timeout = 60.0) self._waitConnected(timeout = 60.0)

View File

@@ -9,11 +9,11 @@ import sys
import threading import threading
import time import time
from datetime import datetime from datetime import datetime
from decimal import Decimal
from typing import Any, Callable, Dict, List, Optional, Union from typing import Any, Callable, Dict, List, Optional, Union
import google.protobuf.json_format import google.protobuf.json_format
import timeago # type: ignore[import-untyped]
from pubsub import pub # type: ignore[import-untyped] from pubsub import pub # type: ignore[import-untyped]
from tabulate import tabulate from tabulate import tabulate
@@ -25,6 +25,7 @@ from meshtastic import (
BROADCAST_ADDR, BROADCAST_ADDR,
BROADCAST_NUM, BROADCAST_NUM,
LOCAL_ADDR, LOCAL_ADDR,
NODELESS_WANT_CONFIG_ID,
ResponseHandler, ResponseHandler,
protocols, protocols,
publishingThread, publishingThread,
@@ -40,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 """Interface class for meshtastic devices
Properties: Properties:
@@ -56,12 +80,14 @@ class MeshInterface:
self.message = message self.message = message
super().__init__(self.message) super().__init__(self.message)
def __init__(self, debugOut=None, noProto: bool=False) -> None: def __init__(self, debugOut=None, noProto: bool=False, noNodes: bool=False) -> None:
"""Constructor """Constructor
Keyword Arguments: Keyword Arguments:
noProto -- If True, don't try to run our protocol on the noProto -- If True, don't try to run our protocol on the
link - just be a dumb serial client. 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.debugOut = debugOut
self.nodes: Optional[Dict[str,Dict]] = None # FIXME self.nodes: Optional[Dict[str,Dict]] = None # FIXME
@@ -80,7 +106,8 @@ class MeshInterface:
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
self.currentPacketId: int = random.randint(0, 0xFFFFFFFF) self.currentPacketId: int = random.randint(0, 0xFFFFFFFF)
self.nodesByNum: Optional[Dict[int, Dict]] = None self.nodesByNum: Optional[Dict[int, Dict]] = None
self.configId: Optional[int] = 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.gotResponse: bool = False # used in gpio read
self.mask: Optional[int] = None # used in gpio read and gpio watch self.mask: Optional[int] = None # used in gpio read and gpio watch
self.queueStatus: Optional[mesh_pb2.QueueStatus] = None self.queueStatus: Optional[mesh_pb2.QueueStatus] = None
@@ -153,11 +180,13 @@ class MeshInterface:
def getTimeAgo(ts) -> Optional[str]: def getTimeAgo(ts) -> Optional[str]:
"""Format how long ago have we heard from this node (aka timeago).""" """Format how long ago have we heard from this node (aka timeago)."""
return ( if ts is None:
timeago.format(datetime.fromtimestamp(ts), datetime.now()) return None
if ts delta = datetime.now() - datetime.fromtimestamp(ts)
else None delta_secs = int(delta.total_seconds())
) if delta_secs < 0:
return None # not handling a timestamp from the future
return _timeago(delta_secs)
rows: List[Dict[str, Any]] = [] rows: List[Dict[str, Any]] = []
if self.nodesByNum: if self.nodesByNum:
@@ -166,7 +195,8 @@ class MeshInterface:
if not includeSelf and node["num"] == self.localNode.nodeNum: if not includeSelf and node["num"] == self.localNode.nodeNum:
continue 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") user = node.get("user")
if user: if user:
@@ -175,6 +205,7 @@ class MeshInterface:
"User": user.get("longName", "N/A"), "User": user.get("longName", "N/A"),
"AKA": user.get("shortName", "N/A"), "AKA": user.get("shortName", "N/A"),
"ID": user["id"], "ID": user["id"],
"Hardware": user.get("hwModel", "UNSET")
} }
) )
@@ -248,7 +279,7 @@ class MeshInterface:
destinationId: Union[int, str]=BROADCAST_ADDR, destinationId: Union[int, str]=BROADCAST_ADDR,
wantAck: bool=False, wantAck: bool=False,
wantResponse: bool=False, wantResponse: bool=False,
onResponse: Optional[Callable[[mesh_pb2.MeshPacket], Any]]=None, onResponse: Optional[Callable[[dict], Any]]=None,
channelIndex: int=0, channelIndex: int=0,
): ):
"""Send a utf8 string to some other node, if the node has a display it """Send a utf8 string to some other node, if the node has a display it
@@ -288,7 +319,8 @@ class MeshInterface:
portNum: portnums_pb2.PortNum.ValueType=portnums_pb2.PortNum.PRIVATE_APP, portNum: portnums_pb2.PortNum.ValueType=portnums_pb2.PortNum.PRIVATE_APP,
wantAck: bool=False, wantAck: bool=False,
wantResponse: bool=False, wantResponse: bool=False,
onResponse: Optional[Callable[[mesh_pb2.MeshPacket], Any]]=None, onResponse: Optional[Callable[[dict], Any]]=None,
onResponseAckPermitted: bool=False,
channelIndex: int=0, channelIndex: int=0,
): ):
"""Send a data packet to some other node """Send a data packet to some other node
@@ -308,6 +340,10 @@ class MeshInterface:
onResponse -- A closure of the form funct(packet), that will be onResponse -- A closure of the form funct(packet), that will be
called when a response packet arrives (or the transaction called when a response packet arrives (or the transaction
is NAKed due to non receipt) 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 channelIndex - channel number to use
Returns the sent packet. The id field will be populated in this packet Returns the sent packet. The id field will be populated in this packet
@@ -339,7 +375,7 @@ class MeshInterface:
if onResponse is not None: if onResponse is not None:
logging.debug(f"Setting a response handler for requestId {meshPacket.id}") logging.debug(f"Setting a response handler for requestId {meshPacket.id}")
self._addResponseHandler(meshPacket.id, onResponse) self._addResponseHandler(meshPacket.id, onResponse, ackPermitted=onResponseAckPermitted)
p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck) p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck)
return p return p
@@ -444,7 +480,7 @@ class MeshInterface:
waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit) waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit)
self.waitForTraceRoute(waitFactor) self.waitForTraceRoute(waitFactor)
def onResponseTraceRoute(self, p): def onResponseTraceRoute(self, p: dict):
"""on response for trace route""" """on response for trace route"""
routeDiscovery = mesh_pb2.RouteDiscovery() routeDiscovery = mesh_pb2.RouteDiscovery()
routeDiscovery.ParseFromString(p["decoded"]["payload"]) routeDiscovery.ParseFromString(p["decoded"]["payload"])
@@ -498,7 +534,7 @@ class MeshInterface:
if wantResponse: if wantResponse:
self.waitForTelemetry() self.waitForTelemetry()
def onResponseTelemetry(self, p): def onResponseTelemetry(self, p: dict):
"""on response for telemetry""" """on response for telemetry"""
if p["decoded"]["portnum"] == 'TELEMETRY_APP': if p["decoded"]["portnum"] == 'TELEMETRY_APP':
self._acknowledgment.receivedTelemetry = True self._acknowledgment.receivedTelemetry = True
@@ -521,8 +557,8 @@ class MeshInterface:
if p["decoded"]["routing"]["errorReason"] == 'NO_RESPONSE': 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.") our_exit("No response from node. At least firmware 2.1.22 is required on the destination node.")
def _addResponseHandler(self, requestId: int, callback: Callable): def _addResponseHandler(self, requestId: int, callback: Callable[[dict], Any], ackPermitted: bool=False):
self.responseHandlers[requestId] = ResponseHandler(callback) self.responseHandlers[requestId] = ResponseHandler(callback=callback, ackPermitted=ackPermitted)
def _sendPacket(self, meshPacket: mesh_pb2.MeshPacket, destinationId: Union[int,str]=BROADCAST_ADDR, wantAck: bool=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). """Send a MeshPacket to the specified node (or if unspecified, broadcast).
@@ -710,7 +746,8 @@ class MeshInterface:
self._localChannels = [] # empty until we start getting channels pushed from the device (during config) self._localChannels = [] # empty until we start getting channels pushed from the device (during config)
startConfig = mesh_pb2.ToRadio() 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 startConfig.want_config_id = self.configId
self._sendToRadio(startConfig) self._sendToRadio(startConfig)
@@ -844,16 +881,18 @@ class MeshInterface:
logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}") logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
elif fromRadio.HasField("node_info"): 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: try:
newpos = self._fixupPosition(node["position"]) newpos = self._fixupPosition(node["position"])
node["position"] = newpos node["position"] = newpos
except: except:
logging.debug("Node without position") logging.debug("Node without position")
logging.debug(f"Received nodeinfo: {node}") # no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum
#self.nodesByNum[node["num"]] = node
self.nodesByNum[node["num"]] = node
if "user" in node: # Some nodes might not have user/ids assigned yet if "user" in node: # Some nodes might not have user/ids assigned yet
if "id" in node["user"]: if "id" in node["user"]:
self.nodes[node["user"]["id"]] = node self.nodes[node["user"]["id"]] = node
@@ -974,9 +1013,9 @@ class MeshInterface:
Returns the position with the updated keys Returns the position with the updated keys
""" """
if "latitudeI" in position: if "latitudeI" in position:
position["latitude"] = position["latitudeI"] * 1e-7 position["latitude"] = float(position["latitudeI"] * Decimal("1e-7"))
if "longitudeI" in position: if "longitudeI" in position:
position["longitude"] = position["longitudeI"] * 1e-7 position["longitude"] = float(position["longitudeI"] * Decimal("1e-7"))
return position return position
def _nodeNumToId(self, num): def _nodeNumToId(self, num):
@@ -1005,7 +1044,16 @@ class MeshInterface:
if nodeNum in self.nodesByNum: if nodeNum in self.nodesByNum:
return self.nodesByNum[nodeNum] return self.nodesByNum[nodeNum]
else: 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 self.nodesByNum[nodeNum] = n
return n return n
@@ -1110,16 +1158,18 @@ class MeshInterface:
requestId = decoded.get("requestId") requestId = decoded.get("requestId")
if requestId is not None: if requestId is not None:
logging.debug(f"Got a response for requestId {requestId}") logging.debug(f"Got a response for requestId {requestId}")
# We ignore ACK packets, but send NAKs and data responses to the handlers # 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") routing = decoded.get("routing")
isAck = routing is not None and ("errorReason" not in routing or routing["errorReason"] == "NONE") isAck = routing is not None and ("errorReason" not in routing or routing["errorReason"] == "NONE")
if not isAck: # we keep the responseHandler in dict until we actually call it
# we keep the responseHandler in dict until we get a non ack handler = self.responseHandlers.get(requestId, None)
handler = self.responseHandlers.pop(requestId, None) if handler is not None:
if handler is not None: if (not isAck) or handler.callback.__name__ == "onAckNak" or handler.ackPermitted:
if not isAck or (isAck and handler.__name__ == "onAckNak"): handler = self.responseHandlers.pop(requestId, None)
logging.debug(f"Calling response handler for requestId {requestId}") logging.debug(f"Calling response handler for requestId {requestId}")
handler.callback(asDict) handler.callback(asDict)
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ") logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
publishingThread.queueWork( publishingThread.queueWork(

View File

File diff suppressed because one or more lines are too long

View File

@@ -116,6 +116,10 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
""" """
wiphone https://www.wiphone.io/ 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 STATION_G1: _HardwareModel.ValueType # 25
""" """
B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
@@ -285,6 +289,11 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
NRF52_PROMICRO_DIY NRF52_PROMICRO_DIY
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS 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 PRIVATE_HW: _HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------
@@ -386,6 +395,10 @@ WIPHONE: HardwareModel.ValueType # 20
""" """
wiphone https://www.wiphone.io/ 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 STATION_G1: HardwareModel.ValueType # 25
""" """
B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
@@ -555,6 +568,11 @@ NRF52_PROMICRO_DIY: HardwareModel.ValueType # 63
NRF52_PROMICRO_DIY NRF52_PROMICRO_DIY
Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS 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 PRIVATE_HW: HardwareModel.ValueType # 255
""" """
------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------
@@ -2407,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: ... def ClearField(self, field_name: typing_extensions.Literal["node_num", b"node_num", "pin", b"pin"]) -> None: ...
global___NodeRemoteHardwarePin = NodeRemoteHardwarePin 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

View File

File diff suppressed because one or more lines are too long

View File

@@ -405,6 +405,8 @@ class ModuleConfig(google.protobuf.message.Message):
ENABLED_FIELD_NUMBER: builtins.int ENABLED_FIELD_NUMBER: builtins.int
PAXCOUNTER_UPDATE_INTERVAL_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 enabled: builtins.bool
""" """
Enable the Paxcounter Module 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 Interval in seconds of how often we should try to send our
metrics to the mesh 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__( def __init__(
self, self,
*, *,
enabled: builtins.bool = ..., enabled: builtins.bool = ...,
paxcounter_update_interval: builtins.int = ..., paxcounter_update_interval: builtins.int = ...,
wifi_threshold: builtins.int = ...,
ble_threshold: builtins.int = ...,
) -> None: ... ) -> 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 @typing_extensions.final
class SerialConfig(google.protobuf.message.Message): class SerialConfig(google.protobuf.message.Message):

View File

@@ -7,7 +7,7 @@ import time
from typing import Union from typing import Union
from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, portnums_pb2 from meshtastic import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, mesh_pb2, portnums_pb2
from meshtastic.util import ( from meshtastic.util import (
Timeout, Timeout,
camel_to_snake, camel_to_snake,
@@ -608,9 +608,10 @@ class Node:
p.get_device_metadata_request = True p.get_device_metadata_request = True
logging.info(f"Requesting device metadata") logging.info(f"Requesting device metadata")
return self._sendAdmin( self._sendAdmin(
p, wantResponse=True, onResponse=self.onRequestGetMetadata p, wantResponse=True, onResponse=self.onRequestGetMetadata
) )
self.iface.waitForAckNak()
def factoryReset(self): def factoryReset(self):
"""Tell the node to factory reset.""" """Tell the node to factory reset."""
@@ -655,6 +656,38 @@ class Node:
onResponse = self.onAckNak onResponse = self.onAckNak
return self._sendAdmin(p, onResponse=onResponse) 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): def _fixupChannels(self):
"""Fixup indexes and add disabled channels as needed""" """Fixup indexes and add disabled channels as needed"""
@@ -681,24 +714,30 @@ class Node:
"""Handle the response packet for requesting device metadata getMetadata()""" """Handle the response packet for requesting device metadata getMetadata()"""
logging.debug(f"onRequestGetMetadata() p:{p}") logging.debug(f"onRequestGetMetadata() p:{p}")
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name( if "routing" in p["decoded"]:
portnums_pb2.PortNum.ROUTING_APP
):
if p["decoded"]["routing"]["errorReason"] != "NONE": if p["decoded"]["routing"]["errorReason"] != "NONE":
logging.warning( print(f'Error on response: {p["decoded"]["routing"]["errorReason"]}')
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}' self.iface._acknowledgment.receivedNak = True
) else:
self._timeout.expireTime = time.time() # Do not wait any longer self.iface._acknowledgment.receivedAck = True
return # Don't try to parse this routing message if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
logging.debug(f"Retrying metadata request.") portnums_pb2.PortNum.ROUTING_APP
self.getMetadata() ):
return 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 c = p["decoded"]["admin"]["raw"].get_device_metadata_response
self._timeout.reset() # We made forward progress self._timeout.reset() # We made forward progress
logging.debug(f"Received metadata {stripnl(c)}") logging.debug(f"Received metadata {stripnl(c)}")
print(f"\nfirmware_version: {c.firmware_version}") print(f"\nfirmware_version: {c.firmware_version}")
print(f"device_state_version: {c.device_state_version}") print(f"device_state_version: {c.device_state_version}")
def onResponseRequestChannel(self, p): def onResponseRequestChannel(self, p):
"""Handle the response packet for requesting a channel _requestChannel()""" """Handle the response packet for requesting a channel _requestChannel()"""

View File

@@ -18,7 +18,7 @@ if platform.system() != "Windows":
class SerialInterface(StreamInterface): class SerialInterface(StreamInterface):
"""Interface class for meshtastic devices over a serial link""" """Interface class for meshtastic devices over a serial link"""
def __init__(self, devPath: Optional[str]=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 """Constructor, opens a connection to a specified serial port, or if unspecified try to
find one Meshtastic device by probing find one Meshtastic device by probing
@@ -62,7 +62,7 @@ class SerialInterface(StreamInterface):
time.sleep(0.1) time.sleep(0.1)
StreamInterface.__init__( StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
) )
def close(self): def close(self):

View File

@@ -19,7 +19,7 @@ MAX_TO_FROM_RADIO_SIZE = 512
class StreamInterface(MeshInterface): class StreamInterface(MeshInterface):
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)""" """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 """Constructor, opens a connection to self.stream
Keyword Arguments: Keyword Arguments:
@@ -43,7 +43,7 @@ class StreamInterface(MeshInterface):
# FIXME, figure out why daemon=True causes reader thread to exit too early # FIXME, figure out why daemon=True causes reader thread to exit too early
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True) 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 # Start the reader thread after superclass constructor completes init
if connectNow: if connectNow:

View File

@@ -17,6 +17,7 @@ class TCPInterface(StreamInterface):
noProto=False, noProto=False,
connectNow=True, connectNow=True,
portNumber=4403, portNumber=4403,
noNodes:bool=False,
): ):
"""Constructor, opens a connection to a specified IP address/hostname """Constructor, opens a connection to a specified IP address/hostname
@@ -38,7 +39,7 @@ class TCPInterface(StreamInterface):
self.socket = None self.socket = None
StreamInterface.__init__( StreamInterface.__init__(
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
) )
def _socket_shutdown(self): def _socket_shutdown(self):

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
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\"\xba\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\x12\x0b\n\x03iaq\x18\x07 \x01(\r\x12\x10\n\x08\x64istance\x18\x08 \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*\xf9\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\x12\x0c\n\x08RCWL9620\x10\x10\x12\t\n\x05SHT4X\x10\x11\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.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.telemetry_pb2', globals())
@@ -21,16 +21,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017TelemetryProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_TELEMETRYSENSORTYPE._serialized_start=1097 _TELEMETRYSENSORTYPE._serialized_start=1205
_TELEMETRYSENSORTYPE._serialized_end=1346 _TELEMETRYSENSORTYPE._serialized_end=1554
_DEVICEMETRICS._serialized_start=43 _DEVICEMETRICS._serialized_start=43
_DEVICEMETRICS._serialized_end=172 _DEVICEMETRICS._serialized_end=172
_ENVIRONMENTMETRICS._serialized_start=175 _ENVIRONMENTMETRICS._serialized_start=175
_ENVIRONMENTMETRICS._serialized_end=361 _ENVIRONMENTMETRICS._serialized_end=469
_POWERMETRICS._serialized_start=364 _POWERMETRICS._serialized_start=472
_POWERMETRICS._serialized_end=504 _POWERMETRICS._serialized_end=612
_AIRQUALITYMETRICS._serialized_start=507 _AIRQUALITYMETRICS._serialized_start=615
_AIRQUALITYMETRICS._serialized_end=826 _AIRQUALITYMETRICS._serialized_end=934
_TELEMETRY._serialized_start=829 _TELEMETRY._serialized_start=937
_TELEMETRY._serialized_end=1094 _TELEMETRY._serialized_end=1202
# @@protoc_insertion_point(module_scope) # @@protoc_insertion_point(module_scope)

View File

@@ -94,6 +94,34 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
""" """
Sensirion High accuracy temperature and humidity 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): class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
""" """
@@ -172,6 +200,34 @@ SHT4X: TelemetrySensorType.ValueType # 17
""" """
Sensirion High accuracy temperature and humidity 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 global___TelemetrySensorType = TelemetrySensorType
@typing_extensions.final @typing_extensions.final
@@ -236,6 +292,12 @@ class EnvironmentMetrics(google.protobuf.message.Message):
CURRENT_FIELD_NUMBER: builtins.int CURRENT_FIELD_NUMBER: builtins.int
IAQ_FIELD_NUMBER: builtins.int IAQ_FIELD_NUMBER: builtins.int
DISTANCE_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: builtins.float
""" """
Temperature measured Temperature measured
@@ -269,6 +331,31 @@ class EnvironmentMetrics(google.protobuf.message.Message):
""" """
RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. 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__( def __init__(
self, self,
*, *,
@@ -280,8 +367,14 @@ class EnvironmentMetrics(google.protobuf.message.Message):
current: builtins.float = ..., current: builtins.float = ...,
iaq: builtins.int = ..., iaq: builtins.int = ...,
distance: builtins.float = ..., 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: ... ) -> 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", "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 global___EnvironmentMetrics = EnvironmentMetrics

View File

@@ -44,7 +44,7 @@ def test_init_onNodeInfoReceive(caplog, iface_with_nodes):
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
packet = { packet = {
"from": "foo", "from": 4808675309,
"decoded": { "decoded": {
"user": { "user": {
"id": "bar", "id": "bar",

View File

@@ -724,29 +724,79 @@ def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_
assert re.search(r"Warning: There were no self.nodes.", caplog.text, re.MULTILINE) assert re.search(r"Warning: There were no self.nodes.", caplog.text, re.MULTILINE)
assert err == "" assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_removeposition_invalid(capsys):
"""Test --remove-position with an invalid dest"""
sys.argv = ["", "--remove-position", "--dest", "!12345678"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"remote nodes is not supported", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_setlat_invalid(capsys):
"""Test --setlat with an invalid dest"""
sys.argv = ["", "--setlat", "37.5", "--dest", "!12345678"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"remote nodes is not supported", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_removeposition(capsys):
"""Test --remove-position"""
sys.argv = ["", "--remove-position"]
mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node)
def mock_removeFixedPosition():
print("inside mocked removeFixedPosition")
mocked_node.removeFixedPosition.side_effect = mock_removeFixedPosition
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main()
out, err = capsys.readouterr()
assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Removing fixed position", out, re.MULTILINE)
assert re.search(r"inside mocked removeFixedPosition", out, re.MULTILINE)
assert err == ""
mo.assert_called()
@pytest.mark.unit @pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config") @pytest.mark.usefixtures("reset_mt_config")
def test_main_setlat(capsys): def test_main_setlat(capsys):
"""Test --sendlat""" """Test --setlat"""
sys.argv = ["", "--setlat", "37.5"] sys.argv = ["", "--setlat", "37.5"]
mt_config.args = sys.argv mt_config.args = sys.argv
mocked_node = MagicMock(autospec=Node) mocked_node = MagicMock(autospec=Node)
def mock_writeConfig(): def mock_setFixedPosition(lat, lon, alt):
print("inside mocked writeConfig") print("inside mocked setFixedPosition")
mocked_node.writeConfig.side_effect = mock_writeConfig
iface = MagicMock(autospec=SerialInterface)
def mock_sendPosition(lat, lon, alt):
print("inside mocked sendPosition")
print(f"{lat} {lon} {alt}") print(f"{lat} {lon} {alt}")
iface.sendPosition.side_effect = mock_sendPosition mocked_node.setFixedPosition.side_effect = mock_setFixedPosition
iface.localNode.return_value = mocked_node
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main() main()
@@ -754,8 +804,7 @@ def test_main_setlat(capsys):
assert re.search(r"Connected to radio", out, re.MULTILINE) assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Fixing latitude", out, re.MULTILINE) assert re.search(r"Fixing latitude", out, re.MULTILINE)
assert re.search(r"Setting device position", out, re.MULTILINE) assert re.search(r"Setting device position", out, re.MULTILINE)
assert re.search(r"inside mocked sendPosition", out, re.MULTILINE) assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE)
# TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
assert err == "" assert err == ""
mo.assert_called() mo.assert_called()
@@ -769,19 +818,14 @@ def test_main_setlon(capsys):
mocked_node = MagicMock(autospec=Node) mocked_node = MagicMock(autospec=Node)
def mock_writeConfig(): def mock_setFixedPosition(lat, lon, alt):
print("inside mocked writeConfig") print("inside mocked setFixedPosition")
mocked_node.writeConfig.side_effect = mock_writeConfig
iface = MagicMock(autospec=SerialInterface)
def mock_sendPosition(lat, lon, alt):
print("inside mocked sendPosition")
print(f"{lat} {lon} {alt}") print(f"{lat} {lon} {alt}")
iface.sendPosition.side_effect = mock_sendPosition mocked_node.setFixedPosition.side_effect = mock_setFixedPosition
iface.localNode.return_value = mocked_node
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main() main()
@@ -789,8 +833,7 @@ def test_main_setlon(capsys):
assert re.search(r"Connected to radio", out, re.MULTILINE) assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Fixing longitude", out, re.MULTILINE) assert re.search(r"Fixing longitude", out, re.MULTILINE)
assert re.search(r"Setting device position", out, re.MULTILINE) assert re.search(r"Setting device position", out, re.MULTILINE)
assert re.search(r"inside mocked sendPosition", out, re.MULTILINE) assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE)
# TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
assert err == "" assert err == ""
mo.assert_called() mo.assert_called()
@@ -804,19 +847,14 @@ def test_main_setalt(capsys):
mocked_node = MagicMock(autospec=Node) mocked_node = MagicMock(autospec=Node)
def mock_writeConfig(): def mock_setFixedPosition(lat, lon, alt):
print("inside mocked writeConfig") print("inside mocked setFixedPosition")
mocked_node.writeConfig.side_effect = mock_writeConfig
iface = MagicMock(autospec=SerialInterface)
def mock_sendPosition(lat, lon, alt):
print("inside mocked sendPosition")
print(f"{lat} {lon} {alt}") print(f"{lat} {lon} {alt}")
iface.sendPosition.side_effect = mock_sendPosition mocked_node.setFixedPosition.side_effect = mock_setFixedPosition
iface.localNode.return_value = mocked_node
iface = MagicMock(autospec=SerialInterface)
iface.localNode = mocked_node
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo: with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
main() main()
@@ -824,8 +862,7 @@ def test_main_setalt(capsys):
assert re.search(r"Connected to radio", out, re.MULTILINE) assert re.search(r"Connected to radio", out, re.MULTILINE)
assert re.search(r"Fixing altitude", out, re.MULTILINE) assert re.search(r"Fixing altitude", out, re.MULTILINE)
assert re.search(r"Setting device position", out, re.MULTILINE) assert re.search(r"Setting device position", out, re.MULTILINE)
assert re.search(r"inside mocked sendPosition", out, re.MULTILINE) assert re.search(r"inside mocked setFixedPosition", out, re.MULTILINE)
# TODO: Why does this not work? assert re.search(r'inside mocked writeConfig', out, re.MULTILINE)
assert err == "" assert err == ""
mo.assert_called() mo.assert_called()

View File

@@ -5,9 +5,10 @@ import re
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
from hypothesis import given, strategies as st
from .. import mesh_pb2, config_pb2, BROADCAST_ADDR, LOCAL_ADDR from .. import mesh_pb2, config_pb2, BROADCAST_ADDR, LOCAL_ADDR
from ..mesh_interface import MeshInterface from ..mesh_interface import MeshInterface, _timeago
from ..node import Node from ..node import Node
# TODO # TODO
@@ -588,7 +589,7 @@ def test_getOrCreateByNum_minimal(iface_with_nodes):
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
tmp = iface._getOrCreateByNum(123) 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.unit
@@ -684,3 +685,21 @@ def test_waitConnected_isConnected_timeout(capsys):
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert re.search(r"warn about something", err, re.MULTILINE) assert re.search(r"warn about something", err, re.MULTILINE)
assert out == "" 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)

View File

@@ -6,6 +6,7 @@ import re
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from hypothesis import given, strategies as st
from meshtastic.supported_device import SupportedDevice from meshtastic.supported_device import SupportedDevice
from meshtastic.mesh_pb2 import MyNodeInfo from meshtastic.mesh_pb2 import MyNodeInfo
@@ -33,6 +34,7 @@ from meshtastic.util import (
stripnl, stripnl,
support_info, support_info,
message_to_json, message_to_json,
Acknowledgment
) )
@@ -63,6 +65,7 @@ def test_fromStr():
assert fromStr("123") == 123 assert fromStr("123") == 123
assert fromStr("abc") == "abc" assert fromStr("abc") == "abc"
assert fromStr("123456789") == 123456789 assert fromStr("123456789") == 123456789
assert fromStr("base64:Zm9vIGJhciBiYXo=") == b"foo bar baz"
@pytest.mark.unitslow @pytest.mark.unitslow
@@ -555,3 +558,39 @@ def test_message_to_json_shows_all():
actual = json.loads(message_to_json(MyNodeInfo())) actual = json.loads(message_to_json(MyNodeInfo()))
expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0 } expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0 }
assert actual == expected 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)

View File

@@ -14,6 +14,7 @@ from queue import Queue
from typing import List, NoReturn, Union from typing import List, NoReturn, Union
from google.protobuf.json_format import MessageToJson from google.protobuf.json_format import MessageToJson
from google.protobuf.message import Message
import packaging.version as pkg_version import packaging.version as pkg_version
import requests import requests
@@ -23,8 +24,16 @@ import serial.tools.list_ports # type: ignore[import-untyped]
from meshtastic.supported_device import supported_devices from meshtastic.supported_device import supported_devices
from meshtastic.version import get_active_version from meshtastic.version import get_active_version
"""Some devices such as a seger jlink we never want to accidentally open""" """Some devices such as a seger jlink or st-link we never want to accidentally open
blacklistVids = dict.fromkeys([0x1366]) 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): def quoteBooleans(a_string):
"""Quote booleans """Quote booleans
@@ -103,7 +112,7 @@ def pskToString(psk: bytes):
return "secret" return "secret"
def stripnl(s): def stripnl(s) -> str:
"""Remove newlines from a string (and remove extra whitespace)""" """Remove newlines from a string (and remove extra whitespace)"""
s = str(s).replace("\n", " ") s = str(s).replace("\n", " ")
return " ".join(s.split()) return " ".join(s.split())
@@ -129,19 +138,35 @@ def findPorts(eliminate_duplicates: bool=False) -> List[str]:
Returns: Returns:
list -- a list of device paths list -- a list of device paths
""" """
l = list( all_ports = serial.tools.list_ports.comports()
# look for 'likely' meshtastic devices
ports = list(
map( map(
lambda port: port.device, lambda port: port.device,
filter( filter(
lambda port: port.vid is not None and port.vid not in blacklistVids, lambda port: port.vid is not None and port.vid in whitelistVids,
serial.tools.list_ports.comports(), 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: if eliminate_duplicates:
l = eliminate_duplicate_port(l) ports = eliminate_duplicate_port(ports)
return l return ports
class dotdict(dict): class dotdict(dict):
@@ -628,7 +653,7 @@ def check_if_newer_version():
return pypi_version return pypi_version
def message_to_json(message, multiline=False): 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.""" """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) json = MessageToJson(message, always_print_fields_with_no_presence=True)
return stripnl(json) if not multiline else json return stripnl(json) if not multiline else json

1555
poetry.lock generated Normal file
View File

File diff suppressed because it is too large Load Diff

51
pyproject.toml Normal file
View 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"

View File

@@ -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

View File

@@ -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.8",
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]",
]
},
)

View File

@@ -1,9 +1,7 @@
import time import meshtastic.serial_interface
import meshtastic
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 ) # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
interface.sendText("hello mesh") interface.sendText("hello mesh")
interface.close() interface.close()