Compare commits

..

151 Commits

Author SHA1 Message Date
mkinney
968027a439 Update setup.py 2022-01-15 10:38:40 -08:00
mkinney
cc24b6ebc5 Merge pull request #231 from mkinney/camelCase
handle snake_case or camelCase
2022-01-15 10:32:08 -08:00
Mike Kinney
db09b4718d add two more lines to code coverage 2022-01-15 10:21:24 -08:00
Jm Casler
e0edbc6288 updating proto submodule to latest 2022-01-15 09:34:14 -08:00
Mike Kinney
ae9ae91af5 remove dead code 2022-01-15 00:24:41 -08:00
Mike Kinney
7921db007b add some coverage to getPref() and setPref() 2022-01-15 00:01:44 -08:00
Mike Kinney
e85af2f9e9 Merge branch 'camelCase' of github.com:mkinney/Meshtastic-python into camelCase 2022-01-14 20:16:26 -08:00
Mike Kinney
8ccc64f92e Merge remote-tracking branch 'upstream/master' into camelCase 2022-01-14 20:16:10 -08:00
Mike Kinney
afb21c6dc3 remove code not needed 2022-01-14 20:15:25 -08:00
mkinney
3291bc7097 Merge branch 'meshtastic:master' into camelCase 2022-01-14 20:06:40 -08:00
Mike Kinney
a7d56504be handle snake_case or camelCase 2022-01-14 16:36:53 -08:00
Jm Casler
90e5b473d9 updating proto submodule to latest 2022-01-13 16:55:22 -08:00
mkinney
52834e9966 Update setup.py 2022-01-12 17:25:00 -08:00
mkinney
63c60d4cea Merge pull request #229 from mkinney/check_for_multiple_mac_conversions
suggested fix from MitchConner912 for not converting mac address more…
2022-01-12 17:13:14 -08:00
Mike Kinney
6a2a9d2093 suggested fix from MitchConner912 for not converting mac address more than once 2022-01-12 17:10:51 -08:00
mkinney
1410448808 Merge pull request #228 from mkinney/more_unit_tests
add more unit tests
2022-01-12 17:06:24 -08:00
Mike Kinney
ad8f2222db cover a few more lines 2022-01-12 16:50:29 -08:00
Mike Kinney
48265e73b1 no need to import pygatt here 2022-01-12 15:54:07 -08:00
Mike Kinney
f3139a8aa0 add more unit tests 2022-01-12 15:50:16 -08:00
mkinney
8d68e36703 Merge pull request #227 from mkinney/pylint_fixes
fix the consider-using-f-string warnings
2022-01-12 14:57:50 -08:00
Mike Kinney
d2d93fbe80 no longer need to disable this pylint warning 2022-01-12 14:50:10 -08:00
Mike Kinney
ed8510468d no need to disable these pylint warnings anymore 2022-01-12 14:47:49 -08:00
Mike Kinney
e7680e07c2 fix pylint global-statement warnings 2022-01-12 14:46:21 -08:00
Mike Kinney
0f89baa36e fix the pylint unused-argument warnings 2022-01-12 14:41:49 -08:00
Mike Kinney
48ed7690af fix the consider-using-f-string warnings 2022-01-12 13:46:01 -08:00
mkinney
59b94ea650 Merge pull request #226 from mkinney/add_get_hw_example
add example how you can get the hwModel using api
2022-01-12 10:59:43 -08:00
Mike Kinney
5b992734fb add example how you can get the hwModel using api 2022-01-12 10:57:03 -08:00
mkinney
3b74b911f8 Merge pull request #224 from mkinney/add_set_owner_example
simple example showing how you can set the long and short name
2022-01-12 10:26:22 -08:00
Mike Kinney
b6570e3c27 simple example showing how you can set the long and short name 2022-01-12 10:23:51 -08:00
mkinney
e1e1664b96 Merge pull request #223 from mkinney/make_pygatt_linux_only
make pygatt linux only in requirements.txt
2022-01-12 10:23:06 -08:00
Mike Kinney
cb1913dfc3 make pygatt linux only in requirements.txt 2022-01-12 10:15:06 -08:00
Sacha Weatherstone
b813a6f8c5 Update requirements.txt 2022-01-12 14:39:20 +11:00
mkinney
0b662318e1 Merge pull request #222 from mkinney/testing_on_mac_air
fixes for working on mac air
2022-01-11 16:48:01 -08:00
Mike Kinney
a6ccc1a246 add conditional lib if linux 2022-01-11 16:42:25 -08:00
Mike Kinney
bc17e9b389 fixes for working on mac air 2022-01-11 16:36:39 -08:00
mkinney
b08e96b66a Update setup.py
bump version for release
2022-01-11 11:02:26 -08:00
mkinney
9e74ead54e Merge pull request #219 from mkinney/smoke_virt
add smoke test for virtual device
2022-01-11 10:56:33 -08:00
Mike Kinney
2cf52f3df6 add a convenience target in Makefile for running the smokevirt test 2022-01-11 18:50:18 +00:00
Mike Kinney
50e9a0a9f7 exclude smokevirt from ci 2022-01-11 18:44:39 +00:00
Mike Kinney
841c44e05c add smoke test for virtual device 2022-01-11 18:39:49 +00:00
mkinney
465bafeb30 Merge pull request #218 from mkinney/fix_remote_admin_message
refactor code to only call local node when necessary; fix tests
2022-01-10 16:51:04 -08:00
Mike Kinney
cb8dafbd31 add more coverage 2022-01-10 16:40:47 -08:00
Mike Kinney
52db617b06 refactor code to only call local node when necessary; fix tests 2022-01-10 16:20:11 -08:00
Jm Casler
ec622590da updating proto submodule to latest 2022-01-09 22:23:07 -08:00
mkinney
345cb1bdc4 Merge pull request #217 from mkinney/fix_remote_admin_message
Fix remote admin message
2022-01-06 12:36:03 -08:00
Mike Kinney
8ca692a26e cannot call os.getlogin() on github instances 2022-01-06 12:33:30 -08:00
Mike Kinney
d1ea68d7dc add code coverage to recently added code 2022-01-06 12:17:42 -08:00
Mike Kinney
b56440a4e8 should not need to talk with remote node if just doing sendtext 2022-01-06 11:55:36 -08:00
github-actions
1401b949a3 Update protobuf submodule 2022-01-06 17:09:45 +00:00
mkinney
97f3ce6198 Merge pull request #215 from mkinney/combine_build_single_executables
combine the building of single executables into one action
2022-01-06 00:25:46 -08:00
Mike Kinney
8019391914 combine the building of single executables into one action 2022-01-06 00:24:59 -08:00
mkinney
8d10010ab1 Merge pull request #214 from mkinney/more_testing_stuff
move slower unit tests to unitslow
2022-01-06 00:00:42 -08:00
mkinney
a2b4d2a96a Update build_mac.yml 2022-01-05 23:47:02 -08:00
mkinney
4de558bdf6 Update build_mac.yml 2022-01-05 23:40:49 -08:00
mkinney
8ff06a0de1 Update build_mac.yml 2022-01-05 23:37:35 -08:00
mkinney
f8ad6061c1 Update build_mac.yml
revert
2022-01-05 23:06:49 -08:00
mkinney
aa4cdb6aea Update build_mac.yml 2022-01-05 23:01:13 -08:00
Mike Kinney
cba424fde0 move slower unit tests to unitslow 2022-01-05 21:12:50 -08:00
mkinney
6c7a870645 Merge pull request #213 from mkinney/add_python_310
check python v3.10 as well
2022-01-05 18:20:55 -08:00
Mike Kinney
f42b1ad4e0 check python v3.10 as well 2022-01-05 18:18:31 -08:00
mkinney
30a51952e0 Merge pull request #212 from mkinney/serial_perms
improve the permission error on linux
2022-01-05 15:08:24 -08:00
Mike Kinney
5de754c5ab improve the permission error on linux 2022-01-05 23:03:06 +00:00
mkinney
bda446c7b5 Update build_mac.yml 2022-01-05 14:13:42 -08:00
mkinney
f79540f197 Update build_mac.yml 2022-01-05 14:09:17 -08:00
mkinney
d507697e56 Merge pull request #210 from mkinney/work_on_single_executable
install meshtastic before building single executable
2022-01-05 13:58:47 -08:00
Mike Kinney
acbc1f2e30 install meshtastic before building single executable 2022-01-05 13:56:07 -08:00
mkinney
7b3c68119c Merge pull request #209 from mkinney/add_modules_for_building_single_executables
collect the meshtastic libs
2022-01-05 13:36:25 -08:00
Mike Kinney
39f97166b0 collect the meshtastic libs 2022-01-05 13:34:28 -08:00
mkinney
541d19cafb Merge pull request #208 from mkinney/create_actions_for_building_single_executables
Create actions for building single executables
2022-01-05 13:26:47 -08:00
Mike Kinney
969a81b779 Merge remote-tracking branch 'upstream/master' into create_actions_for_building_single_executables 2022-01-05 13:24:33 -08:00
Mike Kinney
3f76c1efb0 refactor version info so pyinstaller will work; add build mac and ubuntu standalone executables 2022-01-05 13:22:37 -08:00
mkinney
774849189f Merge pull request #207 from mkinney/create_build_windows_action
add a build windows action
2022-01-05 12:46:10 -08:00
Mike Kinney
53d626aa72 add a build windows action 2022-01-05 12:44:05 -08:00
mkinney
1a3a840269 Merge pull request #206 from mkinney/fully_qualify_imports
need to fully qualify imports so projects consuming the library will …
2022-01-05 11:24:13 -08:00
Mike Kinney
fe69f05e75 add python 3.6, 3.7, 3.8, and 3.9 for ci and validation 2022-01-05 11:20:04 -08:00
Mike Kinney
5c662822b9 need to fully qualify imports so projects consuming the library will work 2022-01-05 11:16:08 -08:00
mkinney
c049d3424a Merge pull request #205 from mkinney/remove_nested_keys
remove nested keys from nodes so we do not display garbage
2022-01-02 11:28:07 -08:00
Mike Kinney
471535853b bump version 2022-01-02 11:20:15 -08:00
Mike Kinney
676148cc14 meant to use decoded not decode 2022-01-02 11:19:17 -08:00
Mike Kinney
a915b05240 remove nested keys from nodes so we do not display garbage 2022-01-02 11:15:19 -08:00
Jm Casler
a1668e8c66 updating proto submodule to latest 2022-01-01 23:25:22 -08:00
mkinney
e7664cb40b Merge pull request #204 from mkinney/add_more_unit_tests
get last two lines covered in node
2022-01-01 15:51:21 -08:00
Mike Kinney
83c18f4008 working on more unit tests 2022-01-01 15:48:33 -08:00
Mike Kinney
8b6321ce7f add a few more tests 2022-01-01 15:21:53 -08:00
Mike Kinney
9fac981ba6 test heartbeatTimer 2022-01-01 14:53:57 -08:00
Mike Kinney
ccc71930f7 get last two lines covered in node 2022-01-01 13:25:36 -08:00
mkinney
9380f048fa Update setup.py
bump version
2022-01-01 11:11:54 -08:00
mkinney
0a655ac8df Merge pull request #203 from mkinney/minor_changes
do not print line for export; comment out ble test; do not send decoded
2022-01-01 09:51:34 -08:00
Mike Kinney
0b6676c5b3 do not print line for export; comment out ble test; do not send decoded 2022-01-01 09:49:21 -08:00
mkinney
e5ecba7ec0 Merge pull request #202 from mkinney/format_mac_address
if mac address is in nodes, format it like a valid mac address
2021-12-31 20:04:19 -08:00
Mike Kinney
a1809f5b84 if mac address is in nodes, format it like a valid mac address 2021-12-31 20:01:14 -08:00
mkinney
9c66447913 Merge pull request #201 from mkinney/more_testing
added tests for _getOrCreateByNum(), nodeNumToId(), and _fixupPositio…
2021-12-31 14:26:37 -08:00
Mike Kinney
65960fb982 added tests for _getOrCreateByNum(), nodeNumToId(), and _fixupPosition(); found/fixed bug on _fixupPosition 2021-12-31 13:43:37 -08:00
mkinney
9d0bc09e0f Merge pull request #200 from mkinney/tuning_tests
Tuning tests
2021-12-31 12:30:00 -08:00
Mike Kinney
475ddcc8dd add tests for _ipToNodeId() 2021-12-31 12:28:14 -08:00
Mike Kinney
105276f98e add unit tests for _shouldFilterPacket() 2021-12-31 12:17:04 -08:00
Mike Kinney
4ee647403b fix output on tests using pytest -s option; fixed some tests 2021-12-31 10:55:13 -08:00
Mike Kinney
10f48f130f move some unit tests to unitslow 2021-12-31 09:59:22 -08:00
mkinney
bd697864e4 Merge pull request #199 from mkinney/unit_testing_continues
revert the stream interface change; fix tunnel tests
2021-12-31 09:40:58 -08:00
Mike Kinney
ab876c9efd add unit test for findPorts() 2021-12-31 09:38:44 -08:00
Mike Kinney
aba303c677 figured out issue; had device connected to serial port; needed to patch; fixed tunnel test in main 2021-12-31 09:28:17 -08:00
Mike Kinney
43d59ca8d8 temp comment out tests that pass locally but not when run from CI 2021-12-31 08:53:17 -08:00
Mike Kinney
177705aeff revert the stream interface change; fix tunnel tests 2021-12-31 08:49:13 -08:00
Sacha Weatherstone
b92fff0da6 Create vercel.json 2021-12-31 20:46:21 +11:00
mkinney
6a6b72a2ae Merge pull request #198 from mkinney/keep_working_on_unit_tests
start to add unit tests for tunnel
2021-12-30 23:01:05 -08:00
Mike Kinney
614a90c0eb unit test a few more lines 2021-12-30 22:59:01 -08:00
Mike Kinney
9adbed4be6 add unit tests for onTunnelReceive() 2021-12-30 22:52:49 -08:00
Mike Kinney
809f005f61 add unit tests for ipstr(), hexstr(), and readnet_u16() 2021-12-30 22:26:26 -08:00
Mike Kinney
d366e74e86 refactor of Tunnel() for unit testing; create unit tests for Tunnel() 2021-12-30 21:24:32 -08:00
Mike Kinney
3f307880f9 add unit tests for tunnel and subnet 2021-12-30 20:04:32 -08:00
Mike Kinney
50523ec1b1 start to add unit tests for tunnel 2021-12-30 19:37:38 -08:00
mkinney
684b2885aa Merge pull request #197 from mkinney/work_on_unit_tests
add more tests; do not need the old --reply test
2021-12-30 12:28:36 -08:00
Mike Kinney
f5eb8738fb added unit tests for --ch-set and onNode() 2021-12-30 12:20:24 -08:00
Mike Kinney
14941c742a add more tests; do not need the old --reply test 2021-12-30 11:28:03 -08:00
mkinney
217add3b00 Update README.md
add code coverage badge
2021-12-30 09:32:18 -08:00
mkinney
4bac85b6a9 Update ci.yml 2021-12-30 09:21:17 -08:00
mkinney
36bed11959 Update publish_to_pypi.yml 2021-12-30 09:12:53 -08:00
mkinney
6f9bcfaaff Merge pull request #196 from mkinney/bump_version
bump version for testing pub to pypi
2021-12-30 09:06:54 -08:00
Mike Kinney
f17b66c872 bump version for testing pub to pypi 2021-12-30 09:05:26 -08:00
Sacha Weatherstone
040cb9bf34 Update and rename publish_to_pypi.py to publish_to_pypi.yml 2021-12-31 04:00:03 +11:00
mkinney
3269b3018f Merge pull request #195 from mkinney/publish_to_pypi
add workflow to publish to pypi
2021-12-30 08:49:26 -08:00
Mike Kinney
aab10b0912 add workflow to publish to pypi 2021-12-30 08:46:34 -08:00
mkinney
e2e9b7d55e Merge pull request #194 from mkinney/remove_raw_from_nodes_display
remove the raw key from the nodes dict
2021-12-30 08:28:36 -08:00
Mike Kinney
cecc5c3b25 remove the raw key from the nodes dict 2021-12-30 08:26:13 -08:00
mkinney
54bb846d00 Merge pull request #193 from mkinney/temp_disable_reply_unittest
need to comment out unittest with change to --reply
2021-12-30 07:51:15 -08:00
Mike Kinney
1a5f525632 need to comment out unittest with change to --reply 2021-12-30 07:49:34 -08:00
mkinney
8ba3d26d63 Merge pull request #191 from Beiri22/patch-1
Update __main__.py
2021-12-30 07:46:40 -08:00
Beiri22
b341b6cfdb Update __main__.py
Main loop also in reply mode.
2021-12-30 10:31:27 +01:00
mkinney
38e7972191 Merge pull request #189 from mkinney/master
bump version; remove docs dir; no need to regen docs on release
2021-12-29 22:02:01 -08:00
mkinney
5be70328fe Merge branch 'meshtastic:master' into master 2021-12-29 21:58:15 -08:00
Mike Kinney
dfe798dbdf no longer gen docs 2021-12-29 21:57:16 -08:00
Mike Kinney
d98b23dba0 remove docs dir 2021-12-29 21:56:03 -08:00
Mike Kinney
4fbe5c7863 bump version 2021-12-29 21:55:41 -08:00
Sacha Weatherstone
69bb8bcca2 Fx protobuf path 2021-12-30 16:52:25 +11:00
Sacha Weatherstone
bb2ea17371 Fix action name 2021-12-30 16:49:55 +11:00
Sacha Weatherstone
b51af8a070 Create update_protobufs 2021-12-30 16:48:20 +11:00
mkinney
aede26694c Merge pull request #188 from mkinney/work_on_serial_issue
revamp the serial connection to avoid Tbeam reboots
2021-12-29 21:41:59 -08:00
Mike Kinney
8e1010e9f2 ignore two checks for Windows smoke1 testing 2021-12-29 21:21:24 -08:00
Mike Kinney
ce2d9f5728 yet another attempt 2021-12-29 21:13:20 -08:00
Mike Kinney
9879e9f2df try yet another way 2021-12-29 21:00:22 -08:00
Mike Kinney
e79faf93d0 try a different way 2021-12-29 20:57:57 -08:00
Mike Kinney
181c04716a accidentally dropped an arg 2021-12-29 20:55:31 -08:00
Mike Kinney
c7d981ec35 fix typo 2021-12-29 20:51:46 -08:00
Mike Kinney
75fe7622a4 deal with windows on the serial issue 2021-12-29 20:49:19 -08:00
Mike Kinney
5dc800f9a3 deal with windows on smoke1 test 2021-12-29 20:39:34 -08:00
Mike Kinney
c7d3f9f787 close seriallog if we have one 2021-12-29 20:35:50 -08:00
Mike Kinney
c70d36d2cd revamp the serial connection to avoid Tbeam reboots 2021-12-29 19:24:26 -08:00
Jm Casler
f239c23d9a Merge branch 'master' of https://github.com/meshtastic/Meshtastic-python 2021-12-29 14:18:35 -08:00
Jm Casler
ac5d729cdf Bumped to 1.2.47. 2021-12-29 14:18:34 -08:00
mkinney
047f43534f Merge pull request #187 from mkinney/bump_version
bump version
2021-12-29 13:29:28 -08:00
Mike Kinney
c26e030f1f bump version 2021-12-29 13:28:08 -08:00
50 changed files with 3311 additions and 654 deletions

View File

@@ -1,2 +1,6 @@
[run] [run]
omit = meshtastic/*_pb2.py,meshtastic/tests/*.py,meshtastic/test.py omit = meshtastic/*_pb2.py,meshtastic/tests/*.py,meshtastic/test.py
[report]
exclude_lines =
if __name__ == .__main__.:

90
.github/workflows/build_executables.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: Build and publish standalone executables
on: workflow_dispatch
jobs:
build-and-publish-mac:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Setup code signing
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
run: |
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$MACOS_KEYCHAIN_PASSWORD" meshtastic.keychain
security default-keychain -s meshtastic.keychain
security unlock-keychain -p "$MACOS_KEYCHAIN_PASSWORD" meshtastic.keychain
security import certificate.p12 -k meshtastic.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_KEYCHAIN_PASSWORD" meshtastic.keychain
- name: Build
env:
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
run: |
pip install pyinstaller
pip install -r requirements.txt
pip install .
pyinstaller -F -n meshtastic --collect-all meshtastic --codesign-identity "$MACOS_SIGNING_IDENTITY" meshtastic/__main__.py
- uses: actions/upload-artifact@v2
with:
name: meshtastic_mac
path: dist
build-and-publish-ubuntu:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Build
run: |
pip install pyinstaller
pip install -r requirements.txt
pip install .
pyinstaller -F -n meshtastic --collect-all meshtastic meshtastic/__main__.py
- uses: actions/upload-artifact@v2
with:
name: meshtastic_ubuntu
path: dist
build-and-publish-windows:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Build
run: |
pip install pyinstaller
pip install -r requirements.txt
pip install .
pyinstaller -F -n meshtastic --collect-all meshtastic meshtastic/__main__.py
- uses: actions/upload-artifact@v2
with:
name: meshtastic_windows
path: dist

View File

@@ -10,12 +10,18 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Python 3 - name: Install Python 3
uses: actions/setup-python@v1 uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Uninstall meshtastic - name: Uninstall meshtastic
run: | run: |
pip3 uninstall meshtastic pip3 uninstall meshtastic
@@ -32,14 +38,32 @@ jobs:
run: pylint meshtastic run: pylint meshtastic
- name: Run tests with pytest - name: Run tests with pytest
run: pytest --cov=meshtastic run: pytest --cov=meshtastic
- name: Generate coverage report
run: |
pytest --cov=meshtastic --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
yml: ./codecov.yml
fail_ci_if_error: true
validate: validate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10"
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install Python 3 - name: Install Python 3
uses: actions/setup-python@v1 uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install meshtastic from local - name: Install meshtastic from local
run: | run: |
pip3 install . pip3 install .

35
.github/workflows/publish_to_pypi.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Publish PyPI
on: workflow_dispatch
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
username: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

24
.github/workflows/update_protobufs.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: "Update protobufs"
on: workflow_dispatch
jobs:
update-protobufs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
submodules: true
- name: Update Submodule
run: |
git pull --recurse-submodules
git submodule update --remote --recursive
- name: Commit update
run: |
git config --global user.name 'github-actions'
git config --global user.email 'bot@noreply.github.com'
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git add proto
git commit -m "Update protobuf submodule" && git push || echo "No changes to commit"

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ venv/
.DS_Store .DS_Store
__pycache__ __pycache__
examples/__pycache__ examples/__pycache__
meshtastic.spec

View File

@@ -23,7 +23,7 @@ ignore-patterns=mqtt_pb2.py,channel_pb2.py,environmental_measurement_pb2.py,admi
# no Warning level messages displayed, use"--disable=all --enable=classes # no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W" # --disable=W"
# #
disable=invalid-name,fixme,logging-fstring-interpolation,too-many-statements,too-many-branches,too-many-locals,no-member,f-string-without-interpolation,protected-access,no-self-use,pointless-string-statement,too-few-public-methods,consider-using-f-string,broad-except,no-else-return,unused-argument,global-statement,global-variable-not-assigned,too-many-boolean-expressions,no-else-raise,bare-except disable=invalid-name,fixme,logging-fstring-interpolation,too-many-statements,too-many-branches,too-many-locals,no-member,f-string-without-interpolation,protected-access,no-self-use,pointless-string-statement,too-few-public-methods,broad-except,no-else-return,no-else-raise,bare-except
[BASIC] [BASIC]

View File

@@ -2,6 +2,10 @@
test: test:
pytest -m unit pytest -m unit
# only run the smoke tests against the virtual device
virt:
pytest -m smokevirt
# local install # local install
install: install:
pip install . pip install .
@@ -16,7 +20,7 @@ lint:
# show the slowest unit tests # show the slowest unit tests
slow: slow:
pytest --durations=0 pytest -m unit --durations=5
# run the coverage report and open results in a browser # run the coverage report and open results in a browser
cov: cov:

View File

@@ -2,6 +2,7 @@
[![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/meshtastic/Meshtastic-python) [![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/meshtastic/Meshtastic-python)
![Unit Tests](https://github.com/meshtastic/Meshtastic-python/actions/workflows/ci.yml/badge.svg) ![Unit Tests](https://github.com/meshtastic/Meshtastic-python/actions/workflows/ci.yml/badge.svg)
[![codecov](https://codecov.io/gh/meshtastic/Meshtastic-python/branch/master/graph/badge.svg?token=TIWPJL73KV)](https://codecov.io/gh/meshtastic/Meshtastic-python)
A python client for using [Meshtastic](https://www.meshtastic.org) devices. This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. It also provides access to any of the operations/data available in the device user interface or the Android application. Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in. A python client for using [Meshtastic](https://www.meshtastic.org) devices. This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. It also provides access to any of the operations/data available in the device user interface or the Android application. Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in.

View File

@@ -1,7 +1,5 @@
rm dist/* rm dist/*
set -e set -e
bin/regen-docs.sh
pandoc --from=markdown --to=rst --output=README README.md
python3 setup.py sdist bdist_wheel python3 setup.py sdist bdist_wheel
python3 -m twine upload dist/* python3 -m twine upload dist/*

16
exampleConfig.yaml Normal file
View File

@@ -0,0 +1,16 @@
# example config using camelCase keys
owner: Bob TBeam
channelUrl: https://www.meshtastic.org/d/#CgUYAyIBAQ
location:
lat: 35.88888
lon: -93.88888
alt: 304
userPrefs:
region: 1
isAlwaysPowered: 'true'
sendOwnerInterval: 2
screenOnSecs: 31536000
waitBluetoothSecs: 31536000

View File

@@ -1,3 +1,4 @@
# example configuration file with snake_case keys
owner: Bob TBeam owner: Bob TBeam
channel_url: https://www.meshtastic.org/d/#CgUYAyIBAQ channel_url: https://www.meshtastic.org/d/#CgUYAyIBAQ

20
examples/get_hw.py Normal file
View File

@@ -0,0 +1,20 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/get_hw.py
"""
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) != 1:
print(f"usage: {sys.argv[0]}")
print("Print the hardware model for the local node.")
sys.exit(3)
iface = meshtastic.serial_interface.SerialInterface()
if iface.nodes:
for n in iface.nodes.values():
if n['num'] == iface.myInfo.my_node_num:
print(n['user']['hwModel'])
iface.close()

20
examples/set_owner.py Normal file
View File

@@ -0,0 +1,20 @@
"""Simple program to demo how to use meshtastic library.
To run: python examples/set_owner.py Bobby 333
"""
import sys
import meshtastic
import meshtastic.serial_interface
# simple arg check
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} long_name [short_name]")
sys.exit(3)
iface = meshtastic.serial_interface.SerialInterface()
long_name = sys.argv[1]
short_name = None
if len(sys.argv) > 2:
short_name = sys.argv[2]
iface.localNode.setOwner(long_name, short_name)
iface.close()

View File

@@ -72,14 +72,16 @@ from typing import *
import serial import serial
import timeago import timeago
import google.protobuf.json_format import google.protobuf.json_format
import pygatt
from pubsub import pub from pubsub import pub
from dotmap import DotMap from dotmap import DotMap
from tabulate import tabulate from tabulate import tabulate
from google.protobuf.json_format import MessageToJson from google.protobuf.json_format import MessageToJson
from .util import fixme, catchAndIgnore, stripnl, DeferredExecution, Timeout from meshtastic.util import fixme, catchAndIgnore, stripnl, DeferredExecution, Timeout
from .node import Node from meshtastic.node import Node
from . import mesh_pb2, portnums_pb2, apponly_pb2, admin_pb2, environmental_measurement_pb2, remote_hardware_pb2, channel_pb2, radioconfig_pb2, util from meshtastic import (mesh_pb2, portnums_pb2, apponly_pb2, admin_pb2,
environmental_measurement_pb2, remote_hardware_pb2,
channel_pb2, radioconfig_pb2, util)
# Note: To follow PEP224, comments should be after the module variable. # Note: To follow PEP224, comments should be after the module variable.
@@ -127,6 +129,7 @@ def _onTextReceive(iface, asDict):
# #
# Usually btw this problem is caused by apps sending binary data but setting the payload type to # Usually btw this problem is caused by apps sending binary data but setting the payload type to
# text. # text.
logging.debug(f'in _onTextReceive() asDict:{asDict}')
try: try:
asBytes = asDict["decoded"]["payload"] asBytes = asDict["decoded"]["payload"]
asDict["decoded"]["text"] = asBytes.decode("utf-8") asDict["decoded"]["text"] = asBytes.decode("utf-8")
@@ -137,22 +140,30 @@ def _onTextReceive(iface, asDict):
def _onPositionReceive(iface, asDict): def _onPositionReceive(iface, asDict):
"""Special auto parsing for received messages""" """Special auto parsing for received messages"""
p = asDict["decoded"]["position"] logging.debug(f'in _onPositionReceive() asDict:{asDict}')
iface._fixupPosition(p) if 'decoded' in asDict:
# update node DB as needed if 'position' in asDict['decoded'] and 'from' in asDict:
iface._getOrCreateByNum(asDict["from"])["position"] = p p = asDict["decoded"]["position"]
logging.debug(f'p:{p}')
p = iface._fixupPosition(p)
logging.debug(f'after fixup p:{p}')
# update node DB as needed
iface._getOrCreateByNum(asDict["from"])["position"] = p
def _onNodeInfoReceive(iface, asDict): def _onNodeInfoReceive(iface, asDict):
"""Special auto parsing for received messages""" """Special auto parsing for received messages"""
p = asDict["decoded"]["user"] logging.debug(f'in _onNodeInfoReceive() asDict:{asDict}')
# decode user protobufs and update nodedb, provide decoded version as "position" in the published msg if 'decoded' in asDict:
# update node DB as needed if 'user' in asDict['decoded'] and 'from' in asDict:
n = iface._getOrCreateByNum(asDict["from"]) p = asDict["decoded"]["user"]
n["user"] = p # decode user protobufs and update nodedb, provide decoded version as "position" in the published msg
# We now have a node ID, make sure it is uptodate in that table # update node DB as needed
iface.nodes[p["id"]] = n n = iface._getOrCreateByNum(asDict["from"])
_receiveInfoUpdate(iface, asDict) n["user"] = p
# We now have a node ID, make sure it is uptodate in that table
iface.nodes[p["id"]] = n
_receiveInfoUpdate(iface, asDict)
def _receiveInfoUpdate(iface, asDict): def _receiveInfoUpdate(iface, asDict):

View File

@@ -5,6 +5,7 @@
import argparse import argparse
import platform import platform
import logging import logging
import os
import sys import sys
import time import time
import yaml import yaml
@@ -13,13 +14,11 @@ import pyqrcode
import pkg_resources import pkg_resources
import meshtastic.util import meshtastic.util
import meshtastic.test import meshtastic.test
from . import remote_hardware from meshtastic import remote_hardware
from . import portnums_pb2, channel_pb2, radioconfig_pb2 from meshtastic.ble_interface import BLEInterface
from .globals import Globals from meshtastic import portnums_pb2, channel_pb2, radioconfig_pb2
from meshtastic.globals import Globals
from meshtastic.__init__ import BROADCAST_ADDR
have_tunnel = platform.system() == 'Linux'
"""We only import the tunnel code if we are on a platform that can run it. """
def onReceive(packet, interface): def onReceive(packet, interface):
"""Callback invoked when a packet arrives""" """Callback invoked when a packet arrives"""
@@ -40,15 +39,15 @@ def onReceive(packet, interface):
rxSnr = packet['rxSnr'] rxSnr = packet['rxSnr']
hopLimit = packet['hopLimit'] hopLimit = packet['hopLimit']
print(f"message: {msg}") print(f"message: {msg}")
reply = "got msg \'{}\' with rxSnr: {} and hopLimit: {}".format(msg, rxSnr, hopLimit) reply = f"got msg \'{msg}\' with rxSnr: {rxSnr} and hopLimit: {hopLimit}"
print("Sending reply: ", reply) print("Sending reply: ", reply)
interface.sendText(reply) interface.sendText(reply)
except Exception as ex: except Exception as ex:
print(ex) print(f'Warning: There is no field {ex} in the packet.')
def onConnection(interface, topic=pub.AUTO_TOPIC): def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=W0613
"""Callback invoked when we connect/disconnect from a radio""" """Callback invoked when we connect/disconnect from a radio"""
print(f"Connection changed: {topic.getName()}") print(f"Connection changed: {topic.getName()}")
@@ -56,43 +55,63 @@ def onConnection(interface, topic=pub.AUTO_TOPIC):
def getPref(attributes, name): def getPref(attributes, name):
"""Get a channel or preferences value""" """Get a channel or preferences value"""
camel_name = meshtastic.util.snake_to_camel(name)
# Note: protobufs has the keys in snake_case, so snake internally
snake_name = meshtastic.util.camel_to_snake(name)
logging.debug(f'snake_name:{snake_name} camel_name:{camel_name}')
logging.debug(f'use camel:{Globals.getInstance().get_camel_case()}')
objDesc = attributes.DESCRIPTOR objDesc = attributes.DESCRIPTOR
field = objDesc.fields_by_name.get(name) field = objDesc.fields_by_name.get(snake_name)
if not field: if not field:
print(f"{attributes.__class__.__name__} does not have an attribute called {name}, so you can not get it.") if Globals.getInstance().get_camel_case():
print(f"{attributes.__class__.__name__} does not have an attribute called {camel_name}, so you can not get it.")
else:
print(f"{attributes.__class__.__name__} does not have an attribute called {snake_name}, so you can not get it.")
print(f"Choices in sorted order are:") print(f"Choices in sorted order are:")
names = [] names = []
for f in objDesc.fields: for f in objDesc.fields:
names.append(f'{f.name}') tmp_name = f'{f.name}'
if Globals.getInstance().get_camel_case():
tmp_name = meshtastic.util.snake_to_camel(tmp_name)
names.append(tmp_name)
for temp_name in sorted(names): for temp_name in sorted(names):
print(f" {temp_name}") print(f" {temp_name}")
return return
# okay - try to read the value # read the value
try: val = getattr(attributes, snake_name)
try:
val = getattr(attributes, name)
except TypeError:
# The getter didn't like our arg type guess try again as a string
val = getattr(attributes, name)
# succeeded! if Globals.getInstance().get_camel_case():
print(f"{name}: {str(val)}") print(f"{camel_name}: {str(val)}")
except Exception as ex: logging.debug(f"{camel_name}: {str(val)}")
print(f"Can't get {name} due to {ex}") else:
print(f"{snake_name}: {str(val)}")
logging.debug(f"{snake_name}: {str(val)}")
def setPref(attributes, name, valStr): def setPref(attributes, name, valStr):
"""Set a channel or preferences value""" """Set a channel or preferences value"""
snake_name = meshtastic.util.camel_to_snake(name)
camel_name = meshtastic.util.snake_to_camel(name)
logging.debug(f'snake_name:{snake_name}')
logging.debug(f'camel_name:{camel_name}')
objDesc = attributes.DESCRIPTOR objDesc = attributes.DESCRIPTOR
field = objDesc.fields_by_name.get(name) field = objDesc.fields_by_name.get(snake_name)
if not field: if not field:
print(f"{attributes.__class__.__name__} does not have an attribute called {name}, so you can not set it.") if Globals.getInstance().get_camel_case():
print(f"{attributes.__class__.__name__} does not have an attribute called {camel_name}, so you can not set it.")
else:
print(f"{attributes.__class__.__name__} does not have an attribute called {snake_name}, so you can not set it.")
print(f"Choices in sorted order are:") print(f"Choices in sorted order are:")
names = [] names = []
for f in objDesc.fields: for f in objDesc.fields:
names.append(f'{f.name}') tmp_name = f'{f.name}'
if Globals.getInstance().get_camel_case():
tmp_name = meshtastic.util.snake_to_camel(tmp_name)
names.append(tmp_name)
for temp_name in sorted(names): for temp_name in sorted(names):
print(f" {temp_name}") print(f" {temp_name}")
return return
@@ -107,27 +126,27 @@ def setPref(attributes, name, valStr):
if e: if e:
val = e.number val = e.number
else: else:
print(f"{name} does not have an enum called {val}, so you can not set it.") if Globals.getInstance().get_camel_case():
print(f"{camel_name} does not have an enum called {val}, so you can not set it.")
else:
print(f"{snake_name} does not have an enum called {val}, so you can not set it.")
print(f"Choices in sorted order are:") print(f"Choices in sorted order are:")
names = [] names = []
for f in enumType.values: for f in enumType.values:
names.append(f'{f.name}') tmp_name = f'{f.name}'
if Globals.getInstance().get_camel_case():
tmp_name = meshtastic.util.snake_to_camel(tmp_name)
names.append(name)
for temp_name in sorted(names): for temp_name in sorted(names):
print(f" {temp_name}") print(f" {temp_name}")
return return
# okay - try to read the value setattr(attributes, snake_name, val)
try:
try:
setattr(attributes, name, val)
except TypeError:
# The setter didn't like our arg type guess try again as a string
setattr(attributes, name, valStr)
# succeeded! if Globals.getInstance().get_camel_case():
print(f"Set {name} to {valStr}") print(f"Set {camel_name} to {valStr}")
except Exception as ex: else:
print(f"Can't set {name} due to {ex}") print(f"Set {snake_name} to {valStr}")
def onConnected(interface): def onConnected(interface):
@@ -137,15 +156,9 @@ def onConnected(interface):
our_globals = Globals.getInstance() our_globals = Globals.getInstance()
args = our_globals.get_args() args = our_globals.get_args()
print("Connected to radio") # do not print this line if we are exporting the config
if not args.export_config:
def getNode(): print("Connected to radio")
"""This operation could be expensive, so we try to cache the results"""
targetNode = our_globals.get_target_node()
if not targetNode:
targetNode = interface.getNode(args.destOrLocal)
our_globals.set_target_node(targetNode)
return targetNode
if args.setlat or args.setlon or args.setalt: if args.setlat or args.setlon or args.setalt:
closeNow = True closeNow = True
@@ -178,12 +191,12 @@ def onConnected(interface):
if args.set_owner: if args.set_owner:
closeNow = True closeNow = True
print(f"Setting device owner to {args.set_owner}") print(f"Setting device owner to {args.set_owner}")
getNode().setOwner(args.set_owner) interface.getNode(args.dest).setOwner(args.set_owner)
if args.pos_fields: if args.pos_fields:
# If --pos-fields invoked with args, set position fields # If --pos-fields invoked with args, set position fields
closeNow = True closeNow = True
prefs = getNode().radioConfig.preferences prefs = interface.getNode(args.dest).radioConfig.preferences
allFields = 0 allFields = 0
try: try:
@@ -198,14 +211,14 @@ def onConnected(interface):
else: else:
print(f"Setting position fields to {allFields}") print(f"Setting position fields to {allFields}")
setPref(prefs, 'position_flags', ('%d' % allFields)) setPref(prefs, 'position_flags', f'{allFields:d}')
print("Writing modified preferences to device") print("Writing modified preferences to device")
getNode().writeConfig() interface.getNode(args.dest).writeConfig()
elif args.pos_fields is not None: elif args.pos_fields is not None:
# If --pos-fields invoked without args, read and display current value # If --pos-fields invoked without args, read and display current value
closeNow = True closeNow = True
prefs = getNode().radioConfig.preferences prefs = interface.getNode(args.dest).radioConfig.preferences
fieldNames = [] fieldNames = []
for bit in radioconfig_pb2.PositionFlags.values(): for bit in radioconfig_pb2.PositionFlags.values():
@@ -224,81 +237,85 @@ def onConnected(interface):
print(sorted(meshtastic.mesh_pb2.Team.keys())) print(sorted(meshtastic.mesh_pb2.Team.keys()))
else: else:
print(f"Setting team to {meshtastic.mesh_pb2.Team.Name(v_team)}") print(f"Setting team to {meshtastic.mesh_pb2.Team.Name(v_team)}")
getNode().setOwner(team=v_team) interface.getNode(args.dest).setOwner(team=v_team)
if args.set_ham: if args.set_ham:
closeNow = True closeNow = True
print(f"Setting Ham ID to {args.set_ham} and turning off encryption") print(f"Setting Ham ID to {args.set_ham} and turning off encryption")
getNode().setOwner(args.set_ham, is_licensed=True) interface.getNode(args.dest).setOwner(args.set_ham, is_licensed=True)
# Must turn off encryption on primary channel # Must turn off encryption on primary channel
getNode().turnOffEncryptionOnPrimaryChannel() interface.getNode(args.dest).turnOffEncryptionOnPrimaryChannel()
if args.reboot: if args.reboot:
closeNow = True closeNow = True
getNode().reboot() interface.getNode(args.dest).reboot()
if args.sendtext: if args.sendtext:
closeNow = True closeNow = True
channelIndex = 0 channelIndex = 0
if args.ch_index is not None: if args.ch_index is not None:
channelIndex = int(args.ch_index) channelIndex = int(args.ch_index)
ch = getNode().getChannelByChannelIndex(channelIndex) ch = interface.localNode.getChannelByChannelIndex(channelIndex)
logging.debug(f'ch:{ch}')
if ch and ch.role != channel_pb2.Channel.Role.DISABLED: if ch and ch.role != channel_pb2.Channel.Role.DISABLED:
print(f"Sending text message {args.sendtext} to {args.destOrAll} on channelIndex:{channelIndex}") print(f"Sending text message {args.sendtext} to {args.dest} on channelIndex:{channelIndex}")
interface.sendText(args.sendtext, args.destOrAll, wantAck=True, channelIndex=channelIndex) interface.sendText(args.sendtext, args.dest, wantAck=True, channelIndex=channelIndex)
else: else:
meshtastic.util.our_exit(f"Warning: {channelIndex} is not a valid channel. Channel must not be DISABLED.") meshtastic.util.our_exit(f"Warning: {channelIndex} is not a valid channel. Channel must not be DISABLED.")
if args.sendping: if args.sendping:
payload = str.encode("test string") payload = str.encode("test string")
print(f"Sending ping message to {args.destOrAll}") print(f"Sending ping message to {args.dest}")
interface.sendData(payload, args.destOrAll, portNum=portnums_pb2.PortNum.REPLY_APP, interface.sendData(payload, args.dest, portNum=portnums_pb2.PortNum.REPLY_APP,
wantAck=True, wantResponse=True) wantAck=True, wantResponse=True)
if args.gpio_wrb or args.gpio_rd or args.gpio_watch: if args.gpio_wrb or args.gpio_rd or args.gpio_watch:
rhc = remote_hardware.RemoteHardwareClient(interface) if args.dest == BROADCAST_ADDR:
meshtastic.util.our_exit("Warning: Must use a destination node ID.")
else:
rhc = remote_hardware.RemoteHardwareClient(interface)
if args.gpio_wrb: if args.gpio_wrb:
bitmask = 0 bitmask = 0
bitval = 0 bitval = 0
for wrpair in (args.gpio_wrb or []): for wrpair in (args.gpio_wrb or []):
bitmask |= 1 << int(wrpair[0]) bitmask |= 1 << int(wrpair[0])
bitval |= int(wrpair[1]) << int(wrpair[0]) bitval |= int(wrpair[1]) << int(wrpair[0])
print(f"Writing GPIO mask 0x{bitmask:x} with value 0x{bitval:x} to {args.dest}") print(f"Writing GPIO mask 0x{bitmask:x} with value 0x{bitval:x} to {args.dest}")
rhc.writeGPIOs(args.dest, bitmask, bitval) rhc.writeGPIOs(args.dest, bitmask, bitval)
closeNow = True closeNow = True
if args.gpio_rd: if args.gpio_rd:
bitmask = int(args.gpio_rd, 16) bitmask = int(args.gpio_rd, 16)
print(f"Reading GPIO mask 0x{bitmask:x} from {args.dest}") print(f"Reading GPIO mask 0x{bitmask:x} from {args.dest}")
interface.mask = bitmask interface.mask = bitmask
rhc.readGPIOs(args.dest, bitmask, None) rhc.readGPIOs(args.dest, bitmask, None)
if not interface.noProto: if not interface.noProto:
# wait up to X seconds for a response # wait up to X seconds for a response
for _ in range(10): for _ in range(10):
time.sleep(1)
if interface.gotResponse:
break
logging.debug(f'end of gpio_rd')
if args.gpio_watch:
bitmask = int(args.gpio_watch, 16)
print(f"Watching GPIO mask 0x{bitmask:x} from {args.dest}. Press ctrl-c to exit")
while True:
rhc.watchGPIOs(args.dest, bitmask)
time.sleep(1) time.sleep(1)
if interface.gotResponse:
break
logging.debug(f'end of gpio_rd')
if args.gpio_watch:
bitmask = int(args.gpio_watch, 16)
print(f"Watching GPIO mask 0x{bitmask:x} from {args.dest}. Press ctrl-c to exit")
while True:
rhc.watchGPIOs(args.dest, bitmask)
time.sleep(1)
# handle settings # handle settings
if args.set: if args.set:
closeNow = True closeNow = True
prefs = getNode().radioConfig.preferences prefs = interface.getNode(args.dest).radioConfig.preferences
# Handle the int/float/bool arguments # Handle the int/float/bool arguments
for pref in args.set: for pref in args.set:
setPref(prefs, pref[0], pref[1]) setPref(prefs, pref[0], pref[1])
print("Writing modified preferences to device") print("Writing modified preferences to device")
getNode().writeConfig() interface.getNode(args.dest).writeConfig()
if args.configure: if args.configure:
with open(args.configure[0], encoding='utf8') as file: with open(args.configure[0], encoding='utf8') as file:
@@ -307,11 +324,15 @@ def onConnected(interface):
if 'owner' in configuration: if 'owner' in configuration:
print(f"Setting device owner to {configuration['owner']}") print(f"Setting device owner to {configuration['owner']}")
getNode().setOwner(configuration['owner']) interface.getNode(args.dest).setOwner(configuration['owner'])
if 'channel_url' in configuration: if 'channel_url' in configuration:
print("Setting channel url to", configuration['channel_url']) print("Setting channel url to", configuration['channel_url'])
getNode().setURL(configuration['channel_url']) interface.getNode(args.dest).setURL(configuration['channel_url'])
if 'channelUrl' in configuration:
print("Setting channel url to", configuration['channelUrl'])
interface.getNode(args.dest).setURL(configuration['channelUrl'])
if 'location' in configuration: if 'location' in configuration:
alt = 0 alt = 0
@@ -336,11 +357,18 @@ def onConnected(interface):
interface.localNode.writeConfig() interface.localNode.writeConfig()
if 'user_prefs' in configuration: if 'user_prefs' in configuration:
prefs = getNode().radioConfig.preferences prefs = interface.getNode(args.dest).radioConfig.preferences
for pref in configuration['user_prefs']: for pref in configuration['user_prefs']:
setPref(prefs, pref, str(configuration['user_prefs'][pref])) setPref(prefs, pref, str(configuration['user_prefs'][pref]))
print("Writing modified preferences to device") print("Writing modified preferences to device")
getNode().writeConfig() interface.getNode(args.dest).writeConfig()
if 'userPrefs' in configuration:
prefs = interface.getNode(args.dest).radioConfig.preferences
for pref in configuration['userPrefs']:
setPref(prefs, pref, str(configuration['userPrefs'][pref]))
print("Writing modified preferences to device")
interface.getNode(args.dest).writeConfig()
if args.export_config: if args.export_config:
# export the configuration (the opposite of '--configure') # export the configuration (the opposite of '--configure')
@@ -349,7 +377,7 @@ def onConnected(interface):
if args.seturl: if args.seturl:
closeNow = True closeNow = True
getNode().setURL(args.seturl) interface.getNode(args.dest).setURL(args.seturl)
# handle changing channels # handle changing channels
@@ -357,7 +385,7 @@ def onConnected(interface):
closeNow = True closeNow = True
if len(args.ch_add) > 10: if len(args.ch_add) > 10:
meshtastic.util.our_exit("Warning: Channel name must be shorter. Channel not added.") meshtastic.util.our_exit("Warning: Channel name must be shorter. Channel not added.")
n = getNode() n = interface.getNode(args.dest)
ch = n.getChannelByName(args.ch_add) ch = n.getChannelByName(args.ch_add)
if ch: if ch:
meshtastic.util.our_exit(f"Warning: This node already has a '{args.ch_add}' channel. No changes were made.") meshtastic.util.our_exit(f"Warning: This node already has a '{args.ch_add}' channel. No changes were made.")
@@ -385,7 +413,7 @@ def onConnected(interface):
meshtastic.util.our_exit("Warning: Cannot delete primary channel.", 1) meshtastic.util.our_exit("Warning: Cannot delete primary channel.", 1)
else: else:
print(f"Deleting channel {channelIndex}") print(f"Deleting channel {channelIndex}")
ch = getNode().deleteChannel(channelIndex) ch = interface.getNode(args.dest).deleteChannel(channelIndex)
ch_changes = [args.ch_longslow, args.ch_longfast, ch_changes = [args.ch_longslow, args.ch_longfast,
args.ch_mediumslow, args.ch_mediumfast, args.ch_mediumslow, args.ch_mediumfast,
@@ -401,7 +429,7 @@ def onConnected(interface):
channelIndex = 0 channelIndex = 0
else: else:
meshtastic.util.our_exit("Warning: Need to specify '--ch-index'.", 1) meshtastic.util.our_exit("Warning: Need to specify '--ch-index'.", 1)
ch = getNode().channels[channelIndex] ch = interface.getNode(args.dest).channels[channelIndex]
if any_primary_channel_changes or args.ch_enable or args.ch_disable: if any_primary_channel_changes or args.ch_enable or args.ch_disable:
@@ -462,21 +490,22 @@ def onConnected(interface):
ch.role = channel_pb2.Channel.Role.DISABLED ch.role = channel_pb2.Channel.Role.DISABLED
print(f"Writing modified channels to device") print(f"Writing modified channels to device")
getNode().writeChannel(channelIndex) interface.getNode(args.dest).writeChannel(channelIndex)
if args.info: if args.info:
print("") print("")
if not args.dest: # If we aren't trying to talk to our local node, don't show it # If we aren't trying to talk to our local node, don't show it
if args.dest == BROADCAST_ADDR:
interface.showInfo() interface.showInfo()
print("") print("")
getNode().showInfo() interface.getNode(args.dest).showInfo()
closeNow = True # FIXME, for now we leave the link up while talking to remote nodes closeNow = True # FIXME, for now we leave the link up while talking to remote nodes
print("") print("")
if args.get: if args.get:
closeNow = True closeNow = True
prefs = getNode().radioConfig.preferences prefs = interface.getNode(args.dest).radioConfig.preferences
# Handle the int/float/bool arguments # Handle the int/float/bool arguments
for pref in args.get: for pref in args.get:
@@ -495,12 +524,16 @@ def onConnected(interface):
qr = pyqrcode.create(url) qr = pyqrcode.create(url)
print(qr.terminal()) print(qr.terminal())
have_tunnel = platform.system() == 'Linux'
if have_tunnel and args.tunnel: if have_tunnel and args.tunnel:
# pylint: disable=C0415 # pylint: disable=C0415
from . import tunnel from . import tunnel
# Even if others said we could close, stay open if the user asked for a tunnel # Even if others said we could close, stay open if the user asked for a tunnel
closeNow = False closeNow = False
tunnel.Tunnel(interface, subnet=args.tunnel_net) if interface.noProto:
logging.warning(f"Not starting Tunnel - disabled by noProto")
else:
tunnel.Tunnel(interface, subnet=args.tunnel_net)
# if the user didn't ask for serial debugging output, we might want to exit after we've done our operation # if 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:
@@ -545,7 +578,10 @@ def export_config(interface):
if owner: if owner:
config += f"owner: {owner}\n\n" config += f"owner: {owner}\n\n"
if channel_url: if channel_url:
config += f"channel_url: {channel_url}\n\n" if Globals.getInstance().get_camel_case():
config += f"channelUrl: {channel_url}\n\n"
else:
config += f"channel_url: {channel_url}\n\n"
if lat or lon or alt: if lat or lon or alt:
config += "location:\n" config += "location:\n"
if lat: if lat:
@@ -558,15 +594,23 @@ def export_config(interface):
preferences = f'{interface.localNode.radioConfig.preferences}' preferences = f'{interface.localNode.radioConfig.preferences}'
prefs = preferences.splitlines() prefs = preferences.splitlines()
if prefs: if prefs:
config += "user_prefs:\n" if Globals.getInstance().get_camel_case():
config += "userPrefs:\n"
else:
config += "user_prefs:\n"
for pref in prefs: for pref in prefs:
config += f" {meshtastic.util.quoteBooleans(pref)}\n" if Globals.getInstance().get_camel_case():
# Note: This may not work if the value has '_'
config += f" {meshtastic.util.snake_to_camel(meshtastic.util.quoteBooleans(pref))}\n"
else:
config += f" {meshtastic.util.quoteBooleans(pref)}\n"
print(config) print(config)
return config return config
def common(): def common():
"""Shared code for all of our command line wrappers""" """Shared code for all of our command line wrappers"""
logfile = None
our_globals = Globals.getInstance() our_globals = Globals.getInstance()
args = our_globals.get_args() args = our_globals.get_args()
parser = our_globals.get_parser() parser = our_globals.get_parser()
@@ -585,13 +629,8 @@ def common():
channelIndex = int(args.ch_index) channelIndex = int(args.ch_index)
our_globals.set_channel_index(channelIndex) our_globals.set_channel_index(channelIndex)
# Some commands require dest to be set, so we now use destOrAll/destOrLocal for more lenient commands
if not args.dest: if not args.dest:
args.destOrAll = "^all" args.dest = BROADCAST_ADDR
args.destOrLocal = "^local"
else:
args.destOrAll = args.dest
args.destOrLocal = args.dest # FIXME, temp hack for debugging remove
if not args.seriallog: if not args.seriallog:
if args.noproto: if args.noproto:
@@ -621,23 +660,31 @@ def common():
logging.info(f"Logging serial output to {args.seriallog}") logging.info(f"Logging serial output to {args.seriallog}")
# Note: using "line buffering" # Note: using "line buffering"
# pylint: disable=R1732 # pylint: disable=R1732
logfile = open(args.seriallog, 'w+', logfile = open(args.seriallog, 'w+', buffering=1, encoding='utf8')
buffering=1, encoding='utf8') our_globals.set_logfile(logfile)
subscribe() subscribe()
if args.ble: if args.ble:
client = meshtastic.ble_interface.BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto) client = BLEInterface(args.ble, debugOut=logfile, noProto=args.noproto)
elif args.host: elif args.host:
client = meshtastic.tcp_interface.TCPInterface( client = meshtastic.tcp_interface.TCPInterface(args.host, debugOut=logfile, noProto=args.noproto)
args.host, debugOut=logfile, noProto=args.noproto)
else: else:
client = meshtastic.serial_interface.SerialInterface( try:
args.port, debugOut=logfile, noProto=args.noproto) client = meshtastic.serial_interface.SerialInterface(args.port, debugOut=logfile, noProto=args.noproto)
except PermissionError as ex:
username = os.getlogin()
message = "Permission Error:\n"
message += " Need to add yourself to the 'dialout' group by running:\n"
message += f" sudo usermod -a -G dialout {username}\n"
message += " After running that command, log out and re-login for it to take effect.\n"
message += f"Error was:{ex}"
meshtastic.util.our_exit(message)
# We assume client is fully connected now # We assume client is fully connected now
onConnected(client) onConnected(client)
if args.noproto or (have_tunnel and args.tunnel): # loop until someone presses ctrlc have_tunnel = platform.system() == 'Linux'
if args.noproto or args.reply or (have_tunnel and args.tunnel): # loop until someone presses ctrlc
while True: while True:
time.sleep(1000) time.sleep(1000)
@@ -685,10 +732,12 @@ def initParser():
action="store_true") action="store_true")
parser.add_argument( parser.add_argument(
"--get", help="Get a preferences field. Use an invalid field such as '0' to get a list of all fields.", nargs=1, action='append') "--get", help=("Get a preferences field. Use an invalid field such as '0' to get a list of all fields."
" Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')"),
nargs=1, action='append')
parser.add_argument( parser.add_argument(
"--set", help="Set a preferences field", nargs=2, action='append') "--set", help="Set a preferences field. Can use either snake_case or camelCase format. (ex: 'ls_secs' or 'lsSecs')", nargs=2, action='append')
parser.add_argument( parser.add_argument(
"--seturl", help="Set a channel URL", action="store") "--seturl", help="Set a channel URL", action="store")
@@ -802,16 +851,18 @@ def initParser():
parser.add_argument('--unset-router', dest='deprecated', parser.add_argument('--unset-router', dest='deprecated',
action='store_false', help='Deprecated, use "--set is_router false" instead') action='store_false', help='Deprecated, use "--set is_router false" instead')
have_tunnel = platform.system() == 'Linux'
if have_tunnel: if have_tunnel:
parser.add_argument('--tunnel', parser.add_argument('--tunnel', action='store_true',
action='store_true', help="Create a TUN tunnel device for forwarding IP packets over the mesh") help="Create a TUN tunnel device for forwarding IP packets over the mesh")
parser.add_argument( parser.add_argument("--subnet", dest='tunnel_net',
"--subnet", dest='tunnel_net', help="Sets the local-end subnet address for the TUN IP bridge", default=None) help="Sets the local-end subnet address for the TUN IP bridge. (ex: 10.115' which is the default)",
default=None)
parser.set_defaults(deprecated=None) parser.set_defaults(deprecated=None)
parser.add_argument('--version', action='version', the_version = pkg_resources.get_distribution("meshtastic").version
version=f"{pkg_resources.require('meshtastic')[0].version}") parser.add_argument('--version', action='version', version=f"{the_version}")
parser.add_argument( parser.add_argument(
"--support", action='store_true', help="Show support info (useful when troubleshooting an issue)") "--support", action='store_true', help="Show support info (useful when troubleshooting an issue)")
@@ -828,6 +879,10 @@ def main():
our_globals.set_parser(parser) our_globals.set_parser(parser)
initParser() initParser()
common() common()
logfile = our_globals.get_logfile()
if logfile:
logfile.close()
def tunnelMain(): def tunnelMain():

View File

@@ -12,8 +12,8 @@ _sym_db = _symbol_database.Default()
from . import channel_pb2 as channel__pb2 from . import channel_pb2 as channel__pb2
from . import mesh_pb2 as mesh__pb2
from . import radioconfig_pb2 as radioconfig__pb2 from . import radioconfig_pb2 as radioconfig__pb2
from . import mesh_pb2 as mesh__pb2
DESCRIPTOR = _descriptor.FileDescriptor( DESCRIPTOR = _descriptor.FileDescriptor(
@@ -21,9 +21,9 @@ DESCRIPTOR = _descriptor.FileDescriptor(
package='', package='',
syntax='proto3', syntax='proto3',
serialized_options=b'\n\023com.geeksville.meshB\013AdminProtosH\003Z!github.com/meshtastic/gomeshproto', serialized_options=b'\n\023com.geeksville.meshB\013AdminProtosH\003Z!github.com/meshtastic/gomeshproto',
serialized_pb=b'\n\x0b\x61\x64min.proto\x1a\rchannel.proto\x1a\nmesh.proto\x1a\x11radioconfig.proto\"\xfb\x02\n\x0c\x41\x64minMessage\x12!\n\tset_radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1a\n\tset_owner\x18\x02 \x01(\x0b\x32\x05.UserH\x00\x12\x1f\n\x0bset_channel\x18\x03 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_radio_request\x18\x04 \x01(\x08H\x00\x12*\n\x12get_radio_response\x18\x05 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1d\n\x13get_channel_request\x18\x06 \x01(\rH\x00\x12(\n\x14get_channel_response\x18\x07 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1d\n\x13\x63onfirm_set_channel\x18 \x01(\x08H\x00\x12\x1b\n\x11\x63onfirm_set_radio\x18! \x01(\x08H\x00\x12\x18\n\x0e\x65xit_simulator\x18\" \x01(\x08H\x00\x12\x18\n\x0ereboot_seconds\x18# \x01(\x05H\x00\x42\t\n\x07variantBG\n\x13\x63om.geeksville.meshB\x0b\x41\x64minProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3' serialized_pb=b'\n\x0b\x61\x64min.proto\x1a\rchannel.proto\x1a\x11radioconfig.proto\x1a\nmesh.proto\"\xbd\x03\n\x0c\x41\x64minMessage\x12!\n\tset_radio\x18\x01 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1a\n\tset_owner\x18\x02 \x01(\x0b\x32\x05.UserH\x00\x12\x1f\n\x0bset_channel\x18\x03 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_radio_request\x18\x04 \x01(\x08H\x00\x12*\n\x12get_radio_response\x18\x05 \x01(\x0b\x32\x0c.RadioConfigH\x00\x12\x1d\n\x13get_channel_request\x18\x06 \x01(\rH\x00\x12(\n\x14get_channel_response\x18\x07 \x01(\x0b\x32\x08.ChannelH\x00\x12\x1b\n\x11get_owner_request\x18\x08 \x01(\x08H\x00\x12#\n\x12get_owner_response\x18\t \x01(\x0b\x32\x05.UserH\x00\x12\x1d\n\x13\x63onfirm_set_channel\x18 \x01(\x08H\x00\x12\x1b\n\x11\x63onfirm_set_radio\x18! \x01(\x08H\x00\x12\x18\n\x0e\x65xit_simulator\x18\" \x01(\x08H\x00\x12\x18\n\x0ereboot_seconds\x18# \x01(\x05H\x00\x42\t\n\x07variantBG\n\x13\x63om.geeksville.meshB\x0b\x41\x64minProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3'
, ,
dependencies=[channel__pb2.DESCRIPTOR,mesh__pb2.DESCRIPTOR,radioconfig__pb2.DESCRIPTOR,]) dependencies=[channel__pb2.DESCRIPTOR,radioconfig__pb2.DESCRIPTOR,mesh__pb2.DESCRIPTOR,])
@@ -85,28 +85,42 @@ _ADMINMESSAGE = _descriptor.Descriptor(
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='confirm_set_channel', full_name='AdminMessage.confirm_set_channel', index=7, name='get_owner_request', full_name='AdminMessage.get_owner_request', index=7,
number=8, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='get_owner_response', full_name='AdminMessage.get_owner_response', index=8,
number=9, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='confirm_set_channel', full_name='AdminMessage.confirm_set_channel', index=9,
number=32, type=8, cpp_type=7, label=1, number=32, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False, has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='confirm_set_radio', full_name='AdminMessage.confirm_set_radio', index=8, name='confirm_set_radio', full_name='AdminMessage.confirm_set_radio', index=10,
number=33, type=8, cpp_type=7, label=1, number=33, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False, has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='exit_simulator', full_name='AdminMessage.exit_simulator', index=9, name='exit_simulator', full_name='AdminMessage.exit_simulator', index=11,
number=34, type=8, cpp_type=7, label=1, number=34, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False, has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='reboot_seconds', full_name='AdminMessage.reboot_seconds', index=10, name='reboot_seconds', full_name='AdminMessage.reboot_seconds', index=12,
number=35, type=5, cpp_type=1, label=1, number=35, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
@@ -128,7 +142,7 @@ _ADMINMESSAGE = _descriptor.Descriptor(
index=0, containing_type=None, fields=[]), index=0, containing_type=None, fields=[]),
], ],
serialized_start=62, serialized_start=62,
serialized_end=441, serialized_end=507,
) )
_ADMINMESSAGE.fields_by_name['set_radio'].message_type = radioconfig__pb2._RADIOCONFIG _ADMINMESSAGE.fields_by_name['set_radio'].message_type = radioconfig__pb2._RADIOCONFIG
@@ -136,6 +150,7 @@ _ADMINMESSAGE.fields_by_name['set_owner'].message_type = mesh__pb2._USER
_ADMINMESSAGE.fields_by_name['set_channel'].message_type = channel__pb2._CHANNEL _ADMINMESSAGE.fields_by_name['set_channel'].message_type = channel__pb2._CHANNEL
_ADMINMESSAGE.fields_by_name['get_radio_response'].message_type = radioconfig__pb2._RADIOCONFIG _ADMINMESSAGE.fields_by_name['get_radio_response'].message_type = radioconfig__pb2._RADIOCONFIG
_ADMINMESSAGE.fields_by_name['get_channel_response'].message_type = channel__pb2._CHANNEL _ADMINMESSAGE.fields_by_name['get_channel_response'].message_type = channel__pb2._CHANNEL
_ADMINMESSAGE.fields_by_name['get_owner_response'].message_type = mesh__pb2._USER
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append( _ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
_ADMINMESSAGE.fields_by_name['set_radio']) _ADMINMESSAGE.fields_by_name['set_radio'])
_ADMINMESSAGE.fields_by_name['set_radio'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant'] _ADMINMESSAGE.fields_by_name['set_radio'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
@@ -157,6 +172,12 @@ _ADMINMESSAGE.fields_by_name['get_channel_request'].containing_oneof = _ADMINMES
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append( _ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
_ADMINMESSAGE.fields_by_name['get_channel_response']) _ADMINMESSAGE.fields_by_name['get_channel_response'])
_ADMINMESSAGE.fields_by_name['get_channel_response'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant'] _ADMINMESSAGE.fields_by_name['get_channel_response'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
_ADMINMESSAGE.fields_by_name['get_owner_request'])
_ADMINMESSAGE.fields_by_name['get_owner_request'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
_ADMINMESSAGE.fields_by_name['get_owner_response'])
_ADMINMESSAGE.fields_by_name['get_owner_response'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']
_ADMINMESSAGE.oneofs_by_name['variant'].fields.append( _ADMINMESSAGE.oneofs_by_name['variant'].fields.append(
_ADMINMESSAGE.fields_by_name['confirm_set_channel']) _ADMINMESSAGE.fields_by_name['confirm_set_channel'])
_ADMINMESSAGE.fields_by_name['confirm_set_channel'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant'] _ADMINMESSAGE.fields_by_name['confirm_set_channel'].containing_oneof = _ADMINMESSAGE.oneofs_by_name['variant']

View File

@@ -1,10 +1,16 @@
"""Bluetooth interface """Bluetooth interface
""" """
import logging import logging
import pygatt import platform
from meshtastic.mesh_interface import MeshInterface
from meshtastic.util import our_exit
if platform.system() == 'Linux':
# pylint: disable=E0401
import pygatt
from .mesh_interface import MeshInterface
# Our standard BLE characteristics # Our standard BLE characteristics
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7" TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
@@ -16,6 +22,8 @@ class BLEInterface(MeshInterface):
"""A not quite ready - FIXME - BLE interface to devices""" """A not quite ready - FIXME - BLE interface to devices"""
def __init__(self, address, noProto=False, debugOut=None): def __init__(self, address, noProto=False, debugOut=None):
if platform.system() != 'Linux':
our_exit("Linux is the only platform with experimental BLE support.", 1)
self.address = address self.address = address
if not noProto: if not noProto:
self.adapter = pygatt.GATTToolBackend() # BGAPIBackend() self.adapter = pygatt.GATTToolBackend() # BGAPIBackend()
@@ -31,7 +39,7 @@ class BLEInterface(MeshInterface):
self._readFromRadio() # read the initial responses self._readFromRadio() # read the initial responses
def handle_data(handle, data): def handle_data(handle, data): # pylint: disable=W0613
self._handleFromRadio(data) self._handleFromRadio(data)
if self.device: if self.device:

View File

@@ -27,16 +27,23 @@ class Globals:
Globals.__instance = self Globals.__instance = self
self.args = None self.args = None
self.parser = None self.parser = None
self.target_node = None
self.channel_index = None self.channel_index = None
self.logfile = None
self.tunnelInstance = None
# TODO: to migrate to camel_case for v1.3 change this value to True
self.camel_case = False
def reset(self): def reset(self):
"""Reset all of our globals. If you add a member, add it to this method, too.""" """Reset all of our globals. If you add a member, add it to this method, too."""
self.args = None self.args = None
self.parser = None self.parser = None
self.target_node = None
self.channel_index = None self.channel_index = None
self.logfile = None
self.tunnelInstance = None
# TODO: to migrate to camel_case for v1.3 change this value to True
self.camel_case = False
# setters
def set_args(self, args): def set_args(self, args):
"""Set the args""" """Set the args"""
self.args = args self.args = args
@@ -45,14 +52,23 @@ class Globals:
"""Set the parser""" """Set the parser"""
self.parser = parser self.parser = parser
def set_target_node(self, target_node):
"""Set the target_node"""
self.target_node = target_node
def set_channel_index(self, channel_index): def set_channel_index(self, channel_index):
"""Set the channel_index""" """Set the channel_index"""
self.channel_index = channel_index self.channel_index = channel_index
def set_logfile(self, logfile):
"""Set the logfile"""
self.logfile = logfile
def set_tunnelInstance(self, tunnelInstance):
"""Set the tunnelInstance"""
self.tunnelInstance = tunnelInstance
def set_camel_case(self):
"""Force using camelCase for things like prefs/set/set"""
self.camel_case = True
# getters
def get_args(self): def get_args(self):
"""Get args""" """Get args"""
return self.args return self.args
@@ -61,10 +77,18 @@ class Globals:
"""Get parser""" """Get parser"""
return self.parser return self.parser
def get_target_node(self):
"""Get target_node"""
return self.target_node
def get_channel_index(self): def get_channel_index(self):
"""Get channel_index""" """Get channel_index"""
return self.channel_index return self.channel_index
def get_logfile(self):
"""Get logfile"""
return self.logfile
def get_tunnelInstance(self):
"""Get tunnelInstance"""
return self.tunnelInstance
def get_camel_case(self):
"""Get whether or not to use camelCase"""
return self.camel_case

View File

@@ -17,9 +17,9 @@ from google.protobuf.json_format import MessageToJson
import meshtastic.node import meshtastic.node
from . import portnums_pb2, mesh_pb2 from meshtastic import portnums_pb2, mesh_pb2
from .util import stripnl, Timeout, our_exit from meshtastic.util import stripnl, Timeout, our_exit, remove_keys_from_dict, convert_mac_addr
from .__init__ import LOCAL_ADDR, BROADCAST_NUM, BROADCAST_ADDR, ResponseHandler, publishingThread, OUR_APP_VERSION, protocols from meshtastic.__init__ import LOCAL_ADDR, BROADCAST_NUM, BROADCAST_ADDR, ResponseHandler, publishingThread, OUR_APP_VERSION, protocols
class MeshInterface: class MeshInterface:
"""Interface class for meshtastic devices """Interface class for meshtastic devices
@@ -68,13 +68,12 @@ class MeshInterface:
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None and exc_value is not None: if exc_type is not None and exc_value is not None:
logging.error( logging.error(f'An exception of type {exc_type} with value {exc_value} has occurred')
f'An exception of type {exc_type} with value {exc_value} has occurred')
if traceback is not None: if traceback is not None:
logging.error(f'Traceback: {traceback}') logging.error(f'Traceback: {traceback}')
self.close() self.close()
def showInfo(self, file=sys.stdout): def showInfo(self, file=sys.stdout): # pylint: disable=W0613
"""Show human readable summary about this object""" """Show human readable summary about this object"""
owner = f"Owner: {self.getLongName()} ({self.getShortName()})" owner = f"Owner: {self.getLongName()} ({self.getShortName()})"
myinfo = '' myinfo = ''
@@ -84,12 +83,24 @@ class MeshInterface:
nodes = "" nodes = ""
if self.nodes: if self.nodes:
for n in self.nodes.values(): for n in self.nodes.values():
nodes = nodes + f" {stripnl(n)}" # when the TBeam is first booted, it sometimes shows the raw data
# so, we will just remove any raw keys
keys_to_remove = ('raw', 'decoded', 'payload')
n2 = remove_keys_from_dict(keys_to_remove, n)
# if we have 'macaddr', re-format it
if 'macaddr' in n2['user']:
val = n2['user']['macaddr']
# decode the base64 value
addr = convert_mac_addr(val)
n2['user']['macaddr'] = addr
nodes = nodes + f" {stripnl(n2)}"
infos = owner + myinfo + mesh + nodes infos = owner + myinfo + mesh + nodes
print(infos) print(infos)
return infos return infos
def showNodes(self, includeSelf=True, file=sys.stdout): def showNodes(self, includeSelf=True, file=sys.stdout): # pylint: disable=W0613
"""Show table summary of nodes in mesh""" """Show table summary of nodes in mesh"""
def formatFloat(value, precision=2, unit=''): def formatFloat(value, precision=2, unit=''):
"""Format a float value with precsion.""" """Format a float value with precsion."""
@@ -148,7 +159,7 @@ class MeshInterface:
def getNode(self, nodeId): def getNode(self, nodeId):
"""Return a node object which contains device settings and channel info""" """Return a node object which contains device settings and channel info"""
if nodeId == LOCAL_ADDR: if nodeId in (LOCAL_ADDR, BROADCAST_ADDR):
return self.localNode return self.localNode
else: else:
n = meshtastic.node.Node(self, nodeId) n = meshtastic.node.Node(self, nodeId)
@@ -382,11 +393,11 @@ class MeshInterface:
return user.get('shortName', None) return user.get('shortName', None)
return None return None
def _waitConnected(self): def _waitConnected(self, timeout=15.0):
"""Block until the initial node db download is complete, or timeout """Block until the initial node db download is complete, or timeout
and raise an exception""" and raise an exception"""
if not self.noProto: if not self.noProto:
if not self.isConnected.wait(15.0): # timeout after x seconds if not self.isConnected.wait(timeout): # timeout after x seconds
raise Exception("Timed out waiting for connection completion") raise Exception("Timed out waiting for connection completion")
# If we failed while connecting, raise the connection to the client # If we failed while connecting, raise the connection to the client
@@ -404,8 +415,7 @@ class MeshInterface:
def _disconnected(self): def _disconnected(self):
"""Called by subclasses to tell clients this interface has disconnected""" """Called by subclasses to tell clients this interface has disconnected"""
self.isConnected.clear() self.isConnected.clear()
publishingThread.queueWork(lambda: pub.sendMessage( publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.connection.lost", interface=self))
"meshtastic.connection.lost", interface=self))
def _startHeartbeat(self): def _startHeartbeat(self):
"""We need to send a heartbeat message to the device every X seconds""" """We need to send a heartbeat message to the device every X seconds"""
@@ -431,8 +441,7 @@ class MeshInterface:
if not self.isConnected.is_set(): if not self.isConnected.is_set():
self.isConnected.set() self.isConnected.set()
self._startHeartbeat() self._startHeartbeat()
publishingThread.queueWork(lambda: pub.sendMessage( publishingThread.queueWork(lambda: pub.sendMessage("meshtastic.connection.established", interface=self))
"meshtastic.connection.established", interface=self))
def _startConfig(self): def _startConfig(self):
"""Start device packets flowing""" """Start device packets flowing"""
@@ -504,7 +513,8 @@ class MeshInterface:
elif fromRadio.HasField("node_info"): elif fromRadio.HasField("node_info"):
node = asDict["nodeInfo"] node = asDict["nodeInfo"]
try: try:
self._fixupPosition(node["position"]) newpos = self._fixupPosition(node["position"])
node["position"] = newpos
except: except:
logging.debug("Node without position") logging.debug("Node without position")
@@ -536,12 +546,14 @@ class MeshInterface:
"""Convert integer lat/lon into floats """Convert integer lat/lon into floats
Arguments: Arguments:
position {Position dictionary} -- object ot fix up position {Position dictionary} -- object to fix up
Returns the position with the updated keys
""" """
if "latitudeI" in position: if "latitudeI" in position:
position["latitude"] = position["latitudeI"] * 1e-7 position["latitude"] = position["latitudeI"] * 1e-7
if "longitudeI" in position: if "longitudeI" in position:
position["longitude"] = position["longitudeI"] * 1e-7 position["longitude"] = position["longitudeI"] * 1e-7
return position
def _nodeNumToId(self, num): def _nodeNumToId(self, num):
"""Map a node node number to a node ID """Map a node node number to a node ID

View File

File diff suppressed because one or more lines are too long

View File

@@ -4,10 +4,8 @@
import logging import logging
import base64 import base64
from google.protobuf.json_format import MessageToJson from google.protobuf.json_format import MessageToJson
from . import portnums_pb2, apponly_pb2, admin_pb2, channel_pb2 from meshtastic import portnums_pb2, apponly_pb2, admin_pb2, channel_pb2
from .util import pskToString, stripnl, Timeout, our_exit, fromPSK from meshtastic.util import pskToString, stripnl, Timeout, our_exit, fromPSK
class Node: class Node:
@@ -257,6 +255,7 @@ class Node:
p = admin_pb2.AdminMessage() p = admin_pb2.AdminMessage()
p.get_radio_request = True p.get_radio_request = True
# TODO: should we check that localNode has an 'admin' channel?
# Show progress message for super slow operations # Show progress message for super slow operations
if self != self.iface.localNode: if self != self.iface.localNode:
print("Requesting preferences from remote node.") print("Requesting preferences from remote node.")

View File

File diff suppressed because one or more lines are too long

View File

@@ -2,8 +2,8 @@
""" """
import logging import logging
from pubsub import pub from pubsub import pub
from . import portnums_pb2, remote_hardware_pb2 from meshtastic import portnums_pb2, remote_hardware_pb2
from .util import our_exit from meshtastic.util import our_exit
def onGPIOreceive(packet, interface): def onGPIOreceive(packet, interface):

View File

@@ -1,13 +1,15 @@
""" Serial interface class """ Serial interface class
""" """
import logging import logging
import time
import platform import platform
import os
import stat
import serial import serial
import meshtastic.util import meshtastic.util
from .stream_interface import StreamInterface from meshtastic.stream_interface import StreamInterface
if platform.system() != 'Windows':
import termios
class SerialInterface(StreamInterface): class SerialInterface(StreamInterface):
"""Interface class for meshtastic devices over a serial link""" """Interface class for meshtastic devices over a serial link"""
@@ -20,6 +22,7 @@ class SerialInterface(StreamInterface):
devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None}) devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None}) debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})
""" """
self.noProto = noProto
if devPath is None: if devPath is None:
ports = meshtastic.util.findPorts() ports = meshtastic.util.findPorts()
@@ -35,43 +38,27 @@ class SerialInterface(StreamInterface):
logging.debug(f"Connecting to {devPath}") logging.debug(f"Connecting to {devPath}")
# Note: we provide None for port here, because we will be opening it later # first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR
self.stream = serial.Serial( # see https://github.com/pyserial/pyserial/issues/124
None, 921600, exclusive=True, timeout=0.5, write_timeout=0) if platform.system() != 'Windows':
with open(devPath, encoding='utf8') as f:
attrs = termios.tcgetattr(f)
attrs[2] = attrs[2] & ~termios.HUPCL
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
f.close()
time.sleep(0.1)
# rts=False Needed to prevent TBEAMs resetting on OSX, because rts is connected to reset self.stream = serial.Serial(devPath, 921600, exclusive=True, timeout=0.5, write_timeout=0)
self.stream.port = devPath self.stream.flush()
time.sleep(0.1)
# HACK: If the platform driving the serial port is unable to leave the RTS pin in high-impedance StreamInterface.__init__(self, debugOut=debugOut, noProto=noProto, connectNow=connectNow)
# mode, set RTS to false so that the device platform won't be reset spuriously.
# Linux does this properly, so don't apply this hack on Linux (because it makes the reset button not work).
if self._hostPlatformAlwaysDrivesUartRts():
self.stream.rts = False
self.stream.open()
StreamInterface.__init__( def close(self):
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow) """Close a connection to the device"""
self.stream.flush()
"""true if platform driving the serial port is Windows Subsystem for Linux 1.""" time.sleep(0.1)
def _isWsl1(self): self.stream.flush()
# WSL1 identifies itself as Linux, but has a special char device at /dev/lxss for use with session control, time.sleep(0.1)
# e.g. /init. We should treat WSL1 as Windows for the RTS-driving hack because the underlying platfrom logging.debug("Closing Serial stream")
# serial driver for the CP21xx still exhibits the buggy behavior. StreamInterface.close(self)
# WSL2 is not covered here, as it does not (as of 2021-May-25) support the appropriate functionality to
# share or pass-through serial ports.
try:
# Claims to be Linux, but has /dev/lxss; must be WSL 1
return platform.system() == 'Linux' and stat.S_ISCHR(os.stat('/dev/lxss').st_mode)
except:
# Couldn't stat /dev/lxss special device; not WSL1
return False
def _hostPlatformAlwaysDrivesUartRts(self):
# OS-X/Windows seems to have a bug in its CP21xx serial drivers. It ignores that we asked for no RTSCTS
# control and will always drive RTS either high or low (rather than letting the CP102 leave
# it as an open-collector floating pin).
# TODO: When WSL2 supports USB passthrough, this will get messier. If/when WSL2 gets virtual serial
# ports that "share" the Windows serial port (and thus the Windows drivers), this code will need to be
# updated to reflect that as well -- or if T-Beams get made with an alternate USB to UART bridge that has
# a less buggy driver.
return platform.system() != 'Linux' or self._isWsl1()

View File

@@ -18,7 +18,7 @@ DESCRIPTOR = _descriptor.FileDescriptor(
package='', package='',
syntax='proto3', syntax='proto3',
serialized_options=b'\n\023com.geeksville.meshB\025StoreAndForwardProtosH\003Z!github.com/meshtastic/gomeshproto', serialized_options=b'\n\023com.geeksville.meshB\025StoreAndForwardProtosH\003Z!github.com/meshtastic/gomeshproto',
serialized_pb=b'\n\x12storeforward.proto\"\xe7\x04\n\x0fStoreAndForward\x12,\n\x02rr\x18\x01 \x01(\x0e\x32 .StoreAndForward.RequestResponse\x12*\n\x05stats\x18\x02 \x01(\x0b\x32\x1b.StoreAndForward.Statistics\x12)\n\x07history\x18\x03 \x01(\x0b\x32\x18.StoreAndForward.History\x1a\xc6\x01\n\nStatistics\x12\x15\n\rMessagesTotal\x18\x01 \x01(\r\x12\x15\n\rMessagesSaved\x18\x02 \x01(\r\x12\x13\n\x0bMessagesMax\x18\x03 \x01(\r\x12\x0e\n\x06UpTime\x18\x04 \x01(\r\x12\x10\n\x08Requests\x18\x05 \x01(\r\x12\x17\n\x0fRequestsHistory\x18\x06 \x01(\r\x12\x11\n\tHeartbeat\x18\x07 \x01(\x08\x12\x11\n\tReturnMax\x18\x08 \x01(\r\x12\x14\n\x0cReturnWindow\x18\t \x01(\r\x1a\x32\n\x07History\x12\x17\n\x0fHistoryMessages\x18\x01 \x01(\r\x12\x0e\n\x06Window\x18\x02 \x01(\r\"\xd1\x01\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x10\n\x0c\x43LIENT_ERROR\x10\x65\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x66\x12\x10\n\x0c\x43LIENT_STATS\x10g\x12\x0f\n\x0b\x43LIENT_PING\x10h\x12\x0f\n\x0b\x43LIENT_PONG\x10iBQ\n\x13\x63om.geeksville.meshB\x15StoreAndForwardProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3' serialized_pb=b'\n\x12storeforward.proto\"\x8a\x06\n\x0fStoreAndForward\x12,\n\x02rr\x18\x01 \x01(\x0e\x32 .StoreAndForward.RequestResponse\x12*\n\x05stats\x18\x02 \x01(\x0b\x32\x1b.StoreAndForward.Statistics\x12)\n\x07history\x18\x03 \x01(\x0b\x32\x18.StoreAndForward.History\x12-\n\theartbeat\x18\x04 \x01(\x0b\x32\x1a.StoreAndForward.Heartbeat\x1a\xcd\x01\n\nStatistics\x12\x16\n\x0emessages_total\x18\x01 \x01(\r\x12\x16\n\x0emessages_saved\x18\x02 \x01(\r\x12\x14\n\x0cmessages_max\x18\x03 \x01(\r\x12\x0f\n\x07up_time\x18\x04 \x01(\r\x12\x10\n\x08requests\x18\x05 \x01(\r\x12\x18\n\x10requests_history\x18\x06 \x01(\r\x12\x11\n\theartbeat\x18\x07 \x01(\x08\x12\x12\n\nreturn_max\x18\x08 \x01(\r\x12\x15\n\rreturn_window\x18\t \x01(\r\x1aI\n\x07History\x12\x18\n\x10history_messages\x18\x01 \x01(\r\x12\x0e\n\x06window\x18\x02 \x01(\r\x12\x14\n\x0clast_request\x18\x03 \x01(\r\x1a.\n\tHeartbeat\x12\x0e\n\x06period\x18\x01 \x01(\r\x12\x11\n\tsecondary\x18\x02 \x01(\r\"\xf7\x01\n\x0fRequestResponse\x12\t\n\x05UNSET\x10\x00\x12\x10\n\x0cROUTER_ERROR\x10\x01\x12\x14\n\x10ROUTER_HEARTBEAT\x10\x02\x12\x0f\n\x0bROUTER_PING\x10\x03\x12\x0f\n\x0bROUTER_PONG\x10\x04\x12\x0f\n\x0bROUTER_BUSY\x10\x05\x12\x12\n\x0eROUTER_HISTORY\x10\x06\x12\x10\n\x0c\x43LIENT_ERROR\x10\x65\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x66\x12\x10\n\x0c\x43LIENT_STATS\x10g\x12\x0f\n\x0b\x43LIENT_PING\x10h\x12\x0f\n\x0b\x43LIENT_PONG\x10i\x12\x10\n\x0c\x43LIENT_ABORT\x10jBQ\n\x13\x63om.geeksville.meshB\x15StoreAndForwardProtosH\x03Z!github.com/meshtastic/gomeshprotob\x06proto3'
) )
@@ -54,30 +54,38 @@ _STOREANDFORWARD_REQUESTRESPONSE = _descriptor.EnumDescriptor(
serialized_options=None, serialized_options=None,
type=None), type=None),
_descriptor.EnumValueDescriptor( _descriptor.EnumValueDescriptor(
name='CLIENT_ERROR', index=6, number=101, name='ROUTER_HISTORY', index=6, number=6,
serialized_options=None, serialized_options=None,
type=None), type=None),
_descriptor.EnumValueDescriptor( _descriptor.EnumValueDescriptor(
name='CLIENT_HISTORY', index=7, number=102, name='CLIENT_ERROR', index=7, number=101,
serialized_options=None, serialized_options=None,
type=None), type=None),
_descriptor.EnumValueDescriptor( _descriptor.EnumValueDescriptor(
name='CLIENT_STATS', index=8, number=103, name='CLIENT_HISTORY', index=8, number=102,
serialized_options=None, serialized_options=None,
type=None), type=None),
_descriptor.EnumValueDescriptor( _descriptor.EnumValueDescriptor(
name='CLIENT_PING', index=9, number=104, name='CLIENT_STATS', index=9, number=103,
serialized_options=None, serialized_options=None,
type=None), type=None),
_descriptor.EnumValueDescriptor( _descriptor.EnumValueDescriptor(
name='CLIENT_PONG', index=10, number=105, name='CLIENT_PING', index=10, number=104,
serialized_options=None,
type=None),
_descriptor.EnumValueDescriptor(
name='CLIENT_PONG', index=11, number=105,
serialized_options=None,
type=None),
_descriptor.EnumValueDescriptor(
name='CLIENT_ABORT', index=12, number=106,
serialized_options=None, serialized_options=None,
type=None), type=None),
], ],
containing_type=None, containing_type=None,
serialized_options=None, serialized_options=None,
serialized_start=429, serialized_start=554,
serialized_end=638, serialized_end=801,
) )
_sym_db.RegisterEnumDescriptor(_STOREANDFORWARD_REQUESTRESPONSE) _sym_db.RegisterEnumDescriptor(_STOREANDFORWARD_REQUESTRESPONSE)
@@ -90,63 +98,63 @@ _STOREANDFORWARD_STATISTICS = _descriptor.Descriptor(
containing_type=None, containing_type=None,
fields=[ fields=[
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='MessagesTotal', full_name='StoreAndForward.Statistics.MessagesTotal', index=0, name='messages_total', full_name='StoreAndForward.Statistics.messages_total', index=0,
number=1, type=13, cpp_type=3, label=1, number=1, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='MessagesSaved', full_name='StoreAndForward.Statistics.MessagesSaved', index=1, name='messages_saved', full_name='StoreAndForward.Statistics.messages_saved', index=1,
number=2, type=13, cpp_type=3, label=1, number=2, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='MessagesMax', full_name='StoreAndForward.Statistics.MessagesMax', index=2, name='messages_max', full_name='StoreAndForward.Statistics.messages_max', index=2,
number=3, type=13, cpp_type=3, label=1, number=3, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='UpTime', full_name='StoreAndForward.Statistics.UpTime', index=3, name='up_time', full_name='StoreAndForward.Statistics.up_time', index=3,
number=4, type=13, cpp_type=3, label=1, number=4, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='Requests', full_name='StoreAndForward.Statistics.Requests', index=4, name='requests', full_name='StoreAndForward.Statistics.requests', index=4,
number=5, type=13, cpp_type=3, label=1, number=5, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='RequestsHistory', full_name='StoreAndForward.Statistics.RequestsHistory', index=5, name='requests_history', full_name='StoreAndForward.Statistics.requests_history', index=5,
number=6, type=13, cpp_type=3, label=1, number=6, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='Heartbeat', full_name='StoreAndForward.Statistics.Heartbeat', index=6, name='heartbeat', full_name='StoreAndForward.Statistics.heartbeat', index=6,
number=7, type=8, cpp_type=7, label=1, number=7, type=8, cpp_type=7, label=1,
has_default_value=False, default_value=False, has_default_value=False, default_value=False,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='ReturnMax', full_name='StoreAndForward.Statistics.ReturnMax', index=7, name='return_max', full_name='StoreAndForward.Statistics.return_max', index=7,
number=8, type=13, cpp_type=3, label=1, number=8, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='ReturnWindow', full_name='StoreAndForward.Statistics.ReturnWindow', index=8, name='return_window', full_name='StoreAndForward.Statistics.return_window', index=8,
number=9, type=13, cpp_type=3, label=1, number=9, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
@@ -164,8 +172,8 @@ _STOREANDFORWARD_STATISTICS = _descriptor.Descriptor(
extension_ranges=[], extension_ranges=[],
oneofs=[ oneofs=[
], ],
serialized_start=176, serialized_start=223,
serialized_end=374, serialized_end=428,
) )
_STOREANDFORWARD_HISTORY = _descriptor.Descriptor( _STOREANDFORWARD_HISTORY = _descriptor.Descriptor(
@@ -176,14 +184,58 @@ _STOREANDFORWARD_HISTORY = _descriptor.Descriptor(
containing_type=None, containing_type=None,
fields=[ fields=[
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='HistoryMessages', full_name='StoreAndForward.History.HistoryMessages', index=0, name='history_messages', full_name='StoreAndForward.History.history_messages', index=0,
number=1, type=13, cpp_type=3, label=1, number=1, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor( _descriptor.FieldDescriptor(
name='Window', full_name='StoreAndForward.History.Window', index=1, name='window', full_name='StoreAndForward.History.window', index=1,
number=2, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='last_request', full_name='StoreAndForward.History.last_request', index=2,
number=3, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=430,
serialized_end=503,
)
_STOREANDFORWARD_HEARTBEAT = _descriptor.Descriptor(
name='Heartbeat',
full_name='StoreAndForward.Heartbeat',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='period', full_name='StoreAndForward.Heartbeat.period', index=0,
number=1, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='secondary', full_name='StoreAndForward.Heartbeat.secondary', index=1,
number=2, type=13, cpp_type=3, label=1, number=2, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0, has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
@@ -201,8 +253,8 @@ _STOREANDFORWARD_HISTORY = _descriptor.Descriptor(
extension_ranges=[], extension_ranges=[],
oneofs=[ oneofs=[
], ],
serialized_start=376, serialized_start=505,
serialized_end=426, serialized_end=551,
) )
_STOREANDFORWARD = _descriptor.Descriptor( _STOREANDFORWARD = _descriptor.Descriptor(
@@ -233,10 +285,17 @@ _STOREANDFORWARD = _descriptor.Descriptor(
message_type=None, enum_type=None, containing_type=None, message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None, is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR), serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='heartbeat', full_name='StoreAndForward.heartbeat', index=3,
number=4, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
], ],
extensions=[ extensions=[
], ],
nested_types=[_STOREANDFORWARD_STATISTICS, _STOREANDFORWARD_HISTORY, ], nested_types=[_STOREANDFORWARD_STATISTICS, _STOREANDFORWARD_HISTORY, _STOREANDFORWARD_HEARTBEAT, ],
enum_types=[ enum_types=[
_STOREANDFORWARD_REQUESTRESPONSE, _STOREANDFORWARD_REQUESTRESPONSE,
], ],
@@ -247,14 +306,16 @@ _STOREANDFORWARD = _descriptor.Descriptor(
oneofs=[ oneofs=[
], ],
serialized_start=23, serialized_start=23,
serialized_end=638, serialized_end=801,
) )
_STOREANDFORWARD_STATISTICS.containing_type = _STOREANDFORWARD _STOREANDFORWARD_STATISTICS.containing_type = _STOREANDFORWARD
_STOREANDFORWARD_HISTORY.containing_type = _STOREANDFORWARD _STOREANDFORWARD_HISTORY.containing_type = _STOREANDFORWARD
_STOREANDFORWARD_HEARTBEAT.containing_type = _STOREANDFORWARD
_STOREANDFORWARD.fields_by_name['rr'].enum_type = _STOREANDFORWARD_REQUESTRESPONSE _STOREANDFORWARD.fields_by_name['rr'].enum_type = _STOREANDFORWARD_REQUESTRESPONSE
_STOREANDFORWARD.fields_by_name['stats'].message_type = _STOREANDFORWARD_STATISTICS _STOREANDFORWARD.fields_by_name['stats'].message_type = _STOREANDFORWARD_STATISTICS
_STOREANDFORWARD.fields_by_name['history'].message_type = _STOREANDFORWARD_HISTORY _STOREANDFORWARD.fields_by_name['history'].message_type = _STOREANDFORWARD_HISTORY
_STOREANDFORWARD.fields_by_name['heartbeat'].message_type = _STOREANDFORWARD_HEARTBEAT
_STOREANDFORWARD_REQUESTRESPONSE.containing_type = _STOREANDFORWARD _STOREANDFORWARD_REQUESTRESPONSE.containing_type = _STOREANDFORWARD
DESCRIPTOR.message_types_by_name['StoreAndForward'] = _STOREANDFORWARD DESCRIPTOR.message_types_by_name['StoreAndForward'] = _STOREANDFORWARD
_sym_db.RegisterFileDescriptor(DESCRIPTOR) _sym_db.RegisterFileDescriptor(DESCRIPTOR)
@@ -274,6 +335,13 @@ StoreAndForward = _reflection.GeneratedProtocolMessageType('StoreAndForward', (_
# @@protoc_insertion_point(class_scope:StoreAndForward.History) # @@protoc_insertion_point(class_scope:StoreAndForward.History)
}) })
, ,
'Heartbeat' : _reflection.GeneratedProtocolMessageType('Heartbeat', (_message.Message,), {
'DESCRIPTOR' : _STOREANDFORWARD_HEARTBEAT,
'__module__' : 'storeforward_pb2'
# @@protoc_insertion_point(class_scope:StoreAndForward.Heartbeat)
})
,
'DESCRIPTOR' : _STOREANDFORWARD, 'DESCRIPTOR' : _STOREANDFORWARD,
'__module__' : 'storeforward_pb2' '__module__' : 'storeforward_pb2'
# @@protoc_insertion_point(class_scope:StoreAndForward) # @@protoc_insertion_point(class_scope:StoreAndForward)
@@ -281,6 +349,7 @@ StoreAndForward = _reflection.GeneratedProtocolMessageType('StoreAndForward', (_
_sym_db.RegisterMessage(StoreAndForward) _sym_db.RegisterMessage(StoreAndForward)
_sym_db.RegisterMessage(StoreAndForward.Statistics) _sym_db.RegisterMessage(StoreAndForward.Statistics)
_sym_db.RegisterMessage(StoreAndForward.History) _sym_db.RegisterMessage(StoreAndForward.History)
_sym_db.RegisterMessage(StoreAndForward.Heartbeat)
DESCRIPTOR._options = None DESCRIPTOR._options = None

View File

@@ -7,8 +7,8 @@ import traceback
import serial import serial
from .mesh_interface import MeshInterface from meshtastic.mesh_interface import MeshInterface
from .util import stripnl from meshtastic.util import stripnl
START1 = 0x94 START1 = 0x94
@@ -88,6 +88,8 @@ class StreamInterface(MeshInterface):
if self.stream: # ignore writes when stream is closed if self.stream: # ignore writes when stream is closed
self.stream.write(b) self.stream.write(b)
self.stream.flush() self.stream.flush()
# we sleep here to give the TBeam a chance to work
time.sleep(0.1)
def _readBytes(self, length): def _readBytes(self, length):
"""Read an array of bytes from our stream""" """Read an array of bytes from our stream"""

View File

@@ -4,7 +4,7 @@ import logging
import socket import socket
from typing import AnyStr from typing import AnyStr
from .stream_interface import StreamInterface from meshtastic.stream_interface import StreamInterface
class TCPInterface(StreamInterface): class TCPInterface(StreamInterface):
"""Interface class for meshtastic devices over a TCP link""" """Interface class for meshtastic devices over a TCP link"""
@@ -17,8 +17,6 @@ class TCPInterface(StreamInterface):
hostname {string} -- Hostname/IP address of the device to connect to hostname {string} -- Hostname/IP address of the device to connect to
""" """
# Instead of wrapping as a stream, we use the native socket API
# self.stream = sock.makefile('rw')
self.stream = None self.stream = None
self.hostname = hostname self.hostname = hostname
@@ -35,6 +33,12 @@ class TCPInterface(StreamInterface):
StreamInterface.__init__(self, debugOut=debugOut, noProto=noProto, StreamInterface.__init__(self, debugOut=debugOut, noProto=noProto,
connectNow=connectNow) connectNow=connectNow)
def _socket_shutdown(self):
"""Shutdown the socket.
Note: Broke out this line so the exception could be unit tested.
"""
self.socket.shutdown(socket.SHUT_RDWR)
def myConnect(self): def myConnect(self):
"""Connect to socket""" """Connect to socket"""
server_address = (self.hostname, self.portNumber) server_address = (self.hostname, self.portNumber)
@@ -50,7 +54,7 @@ class TCPInterface(StreamInterface):
self._wantExit = True self._wantExit = True
if not self.socket is None: if not self.socket is None:
try: try:
self.socket.shutdown(socket.SHUT_RDWR) self._socket_shutdown()
except: except:
pass # Ignore errors in shutdown, because we might have a race with the server pass # Ignore errors in shutdown, because we might have a race with the server
self.socket.close() self.socket.close()

View File

@@ -8,9 +8,9 @@ import traceback
from dotmap import DotMap from dotmap import DotMap
from pubsub import pub from pubsub import pub
import meshtastic.util import meshtastic.util
from .__init__ import BROADCAST_NUM from meshtastic.__init__ import BROADCAST_NUM
from .serial_interface import SerialInterface from meshtastic.serial_interface import SerialInterface
from .tcp_interface import TCPInterface from meshtastic.tcp_interface import TCPInterface
"""The interfaces we are using for our tests""" """The interfaces we are using for our tests"""
@@ -63,6 +63,7 @@ def testSend(fromInterface, toInterface, isBroadcast=False, asBinary=False, want
Returns: Returns:
boolean -- True for success boolean -- True for success
""" """
# pylint: disable=W0603
global receivedPackets global receivedPackets
receivedPackets = [] receivedPackets = []
fromNode = fromInterface.myInfo.my_node_num fromNode = fromInterface.myInfo.my_node_num
@@ -74,6 +75,7 @@ def testSend(fromInterface, toInterface, isBroadcast=False, asBinary=False, want
logging.debug( logging.debug(
f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}") f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}")
# pylint: disable=W0603
global sendingInterface global sendingInterface
sendingInterface = fromInterface sendingInterface = fromInterface
if not asBinary: if not asBinary:
@@ -94,6 +96,7 @@ def runTests(numTests=50, wantAck=False, maxFailures=0):
numFail = 0 numFail = 0
numSuccess = 0 numSuccess = 0
for _ in range(numTests): for _ in range(numTests):
# pylint: disable=W0603
global testNumber global testNumber
testNumber = testNumber + 1 testNumber = testNumber + 1
isBroadcast = True isBroadcast = True
@@ -152,6 +155,7 @@ def testAll(numTests=5):
pub.subscribe(onConnection, "meshtastic.connection") pub.subscribe(onConnection, "meshtastic.connection")
pub.subscribe(onReceive, "meshtastic.receive") pub.subscribe(onReceive, "meshtastic.receive")
# pylint: disable=W0603
global interfaces global interfaces
interfaces = list(map(lambda port: SerialInterface( interfaces = list(map(lambda port: SerialInterface(
port, debugOut=openDebugLog(port), connectNow=True), ports)) port, debugOut=openDebugLog(port), connectNow=True), ports))

View File

@@ -1,12 +1,15 @@
"""Meshtastic unit tests for ble_interface.py""" """Meshtastic unit tests for ble_interface.py"""
from unittest.mock import patch
import pytest import pytest
from ..ble_interface import BLEInterface from ..ble_interface import BLEInterface
@pytest.mark.unit @pytest.mark.unit
def test_BLEInterface(): @patch('platform.system', return_value='Linux')
def test_BLEInterface(mock_platform):
"""Test that we can instantiate a BLEInterface""" """Test that we can instantiate a BLEInterface"""
iface = BLEInterface('foo', debugOut=True, noProto=True) iface = BLEInterface('foo', debugOut=True, noProto=True)
iface.close() iface.close()
mock_platform.assert_called()

View File

@@ -0,0 +1,61 @@
"""Meshtastic unit tests for __init__.py"""
import re
import logging
from unittest.mock import MagicMock
import pytest
from meshtastic.__init__ import _onTextReceive, _onPositionReceive, _onNodeInfoReceive
from ..serial_interface import SerialInterface
from ..globals import Globals
@pytest.mark.unit
def test_init_onTextReceive_with_exception(caplog):
"""Test _onTextReceive"""
args = MagicMock()
Globals.getInstance().set_args(args)
iface = MagicMock(autospec=SerialInterface)
packet = {}
with caplog.at_level(logging.DEBUG):
_onTextReceive(iface, packet)
assert re.search(r'in _onTextReceive', caplog.text, re.MULTILINE)
assert re.search(r'Malformatted', caplog.text, re.MULTILINE)
@pytest.mark.unit
def test_init_onPositionReceive(caplog):
"""Test _onPositionReceive"""
args = MagicMock()
Globals.getInstance().set_args(args)
iface = MagicMock(autospec=SerialInterface)
packet = {
'from': 'foo',
'decoded': {
'position': {}
}
}
with caplog.at_level(logging.DEBUG):
_onPositionReceive(iface, packet)
assert re.search(r'in _onPositionReceive', caplog.text, re.MULTILINE)
@pytest.mark.unit
def test_init_onNodeInfoReceive(caplog, iface_with_nodes):
"""Test _onNodeInfoReceive"""
args = MagicMock()
Globals.getInstance().set_args(args)
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
packet = {
'from': 'foo',
'decoded': {
'user': {
'id': 'bar',
},
}
}
with caplog.at_level(logging.DEBUG):
_onNodeInfoReceive(iface, packet)
assert re.search(r'in _onNodeInfoReceive', caplog.text, re.MULTILINE)

View File

@@ -6,13 +6,21 @@ import pytest
@pytest.mark.int @pytest.mark.int
def test_int_no_args(): def test_int_meshtastic_no_args():
"""Test without any args""" """Test meshtastic without any args"""
return_value, out = subprocess.getstatusoutput('meshtastic') return_value, out = subprocess.getstatusoutput('meshtastic')
assert re.match(r'usage: meshtastic', out) assert re.match(r'usage: meshtastic', out)
assert return_value == 1 assert return_value == 1
@pytest.mark.int
def test_int_mesh_tunnel_no_args():
"""Test mesh-tunnel without any args"""
return_value, out = subprocess.getstatusoutput('mesh-tunnel')
assert re.match(r'usage: mesh-tunnel', out)
assert return_value == 1
@pytest.mark.int @pytest.mark.int
def test_int_version(): def test_int_version():
"""Test '--version'.""" """Test '--version'."""

View File

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,13 @@ from ..mesh_interface import MeshInterface
from ..node import Node from ..node import Node
from .. import mesh_pb2 from .. import mesh_pb2
from ..__init__ import LOCAL_ADDR, BROADCAST_ADDR from ..__init__ import LOCAL_ADDR, BROADCAST_ADDR
from ..radioconfig_pb2 import RadioConfig
from ..util import Timeout
@pytest.mark.unit @pytest.mark.unit
def test_MeshInterface(capsys, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_MeshInterface(capsys):
"""Test that we can instantiate a MeshInterface""" """Test that we can instantiate a MeshInterface"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
anode = Node('foo', 'bar') anode = Node('foo', 'bar')
@@ -54,19 +57,19 @@ def test_MeshInterface(capsys, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_getMyUser(reset_globals, iface_with_nodes): @pytest.mark.usefixtures("reset_globals")
def test_getMyUser(iface_with_nodes):
"""Test getMyUser()""" """Test getMyUser()"""
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
myuser = iface.getMyUser() myuser = iface.getMyUser()
print(f'myuser:{myuser}')
assert myuser is not None assert myuser is not None
assert myuser["id"] == '!9388f81c' assert myuser["id"] == '!9388f81c'
@pytest.mark.unit @pytest.mark.unit
def test_getLongName(reset_globals, iface_with_nodes): @pytest.mark.usefixtures("reset_globals")
def test_getLongName(iface_with_nodes):
"""Test getLongName()""" """Test getLongName()"""
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
@@ -75,7 +78,8 @@ def test_getLongName(reset_globals, iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
def test_getShortName(reset_globals, iface_with_nodes): @pytest.mark.usefixtures("reset_globals")
def test_getShortName(iface_with_nodes):
"""Test getShortName().""" """Test getShortName()."""
iface = iface_with_nodes iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164 iface.myInfo.my_node_num = 2475227164
@@ -84,7 +88,8 @@ def test_getShortName(reset_globals, iface_with_nodes):
@pytest.mark.unit @pytest.mark.unit
def test_handlePacketFromRadio_no_from(capsys, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_handlePacketFromRadio_no_from(capsys):
"""Test _handlePacketFromRadio with no 'from' in the mesh packet.""" """Test _handlePacketFromRadio with no 'from' in the mesh packet."""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
meshPacket = mesh_pb2.MeshPacket() meshPacket = mesh_pb2.MeshPacket()
@@ -95,7 +100,8 @@ def test_handlePacketFromRadio_no_from(capsys, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_handlePacketFromRadio_with_a_portnum(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_handlePacketFromRadio_with_a_portnum(caplog):
"""Test _handlePacketFromRadio with a portnum """Test _handlePacketFromRadio with a portnum
Since we have an attribute called 'from', we cannot simply 'set' it. Since we have an attribute called 'from', we cannot simply 'set' it.
Had to implement a hack just to be able to test some code. Had to implement a hack just to be able to test some code.
@@ -110,7 +116,8 @@ def test_handlePacketFromRadio_with_a_portnum(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_handlePacketFromRadio_no_portnum(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_handlePacketFromRadio_no_portnum(caplog):
"""Test _handlePacketFromRadio without a portnum""" """Test _handlePacketFromRadio without a portnum"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
meshPacket = mesh_pb2.MeshPacket() meshPacket = mesh_pb2.MeshPacket()
@@ -121,7 +128,8 @@ def test_handlePacketFromRadio_no_portnum(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_getNode_with_local(reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_getNode_with_local():
"""Test getNode""" """Test getNode"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
anode = iface.getNode(LOCAL_ADDR) anode = iface.getNode(LOCAL_ADDR)
@@ -129,7 +137,8 @@ def test_getNode_with_local(reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_getNode_not_local(reset_globals, caplog): @pytest.mark.usefixtures("reset_globals")
def test_getNode_not_local(caplog):
"""Test getNode not local""" """Test getNode not local"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
anode = MagicMock(autospec=Node) anode = MagicMock(autospec=Node)
@@ -141,7 +150,8 @@ def test_getNode_not_local(reset_globals, caplog):
@pytest.mark.unit @pytest.mark.unit
def test_getNode_not_local_timeout(reset_globals, capsys): @pytest.mark.usefixtures("reset_globals")
def test_getNode_not_local_timeout(capsys):
"""Test getNode not local, simulate timeout""" """Test getNode not local, simulate timeout"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
anode = MagicMock(autospec=Node) anode = MagicMock(autospec=Node)
@@ -157,7 +167,8 @@ def test_getNode_not_local_timeout(reset_globals, capsys):
@pytest.mark.unit @pytest.mark.unit
def test_sendPosition(reset_globals, caplog): @pytest.mark.usefixtures("reset_globals")
def test_sendPosition(caplog):
"""Test sendPosition""" """Test sendPosition"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -167,7 +178,25 @@ def test_sendPosition(reset_globals, caplog):
@pytest.mark.unit @pytest.mark.unit
def test_handleFromRadio_empty_payload(reset_globals, caplog): @pytest.mark.usefixtures("reset_globals")
def test_close_with_heartbeatTimer(caplog):
"""Test close() with heartbeatTimer"""
iface = MeshInterface(noProto=True)
anode = Node('foo', 'bar')
radioConfig = RadioConfig()
radioConfig.preferences.phone_timeout_secs = 10
anode.radioConfig = radioConfig
iface.localNode = anode
assert iface.heartbeatTimer is None
with caplog.at_level(logging.DEBUG):
iface._startHeartbeat()
assert iface.heartbeatTimer is not None
iface.close()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_handleFromRadio_empty_payload(caplog):
"""Test _handleFromRadio""" """Test _handleFromRadio"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -177,7 +206,8 @@ def test_handleFromRadio_empty_payload(reset_globals, caplog):
@pytest.mark.unit @pytest.mark.unit
def test_handleFromRadio_with_my_info(reset_globals, caplog): @pytest.mark.usefixtures("reset_globals")
def test_handleFromRadio_with_my_info(caplog):
"""Test _handleFromRadio with my_info""" """Test _handleFromRadio with my_info"""
# Note: I captured the '--debug --info' for the bytes below. # Note: I captured the '--debug --info' for the bytes below.
# It "translates" to this: # It "translates" to this:
@@ -202,7 +232,8 @@ def test_handleFromRadio_with_my_info(reset_globals, caplog):
@pytest.mark.unit @pytest.mark.unit
def test_handleFromRadio_with_node_info(reset_globals, caplog, capsys): @pytest.mark.usefixtures("reset_globals")
def test_handleFromRadio_with_node_info(caplog, capsys):
"""Test _handleFromRadio with node_info""" """Test _handleFromRadio with node_info"""
# Note: I captured the '--debug --info' for the bytes below. # Note: I captured the '--debug --info' for the bytes below.
# It "translates" to this: # It "translates" to this:
@@ -238,7 +269,8 @@ def test_handleFromRadio_with_node_info(reset_globals, caplog, capsys):
@pytest.mark.unit @pytest.mark.unit
def test_handleFromRadio_with_node_info_tbeam1(reset_globals, caplog, capsys): @pytest.mark.usefixtures("reset_globals")
def test_handleFromRadio_with_node_info_tbeam1(caplog, capsys):
"""Test _handleFromRadio with node_info""" """Test _handleFromRadio with node_info"""
# Note: Captured the '--debug --info' for the bytes below. # Note: Captured the '--debug --info' for the bytes below.
# pylint: disable=C0301 # pylint: disable=C0301
@@ -261,7 +293,8 @@ def test_handleFromRadio_with_node_info_tbeam1(reset_globals, caplog, capsys):
@pytest.mark.unit @pytest.mark.unit
def test_handleFromRadio_with_node_info_tbeam_with_bad_data(reset_globals, caplog, capsys): @pytest.mark.usefixtures("reset_globals")
def test_handleFromRadio_with_node_info_tbeam_with_bad_data(caplog):
"""Test _handleFromRadio with node_info with some bad data (issue#172) - ensure we do not throw exception""" """Test _handleFromRadio with node_info with some bad data (issue#172) - ensure we do not throw exception"""
# Note: Captured the '--debug --info' for the bytes below. # Note: Captured the '--debug --info' for the bytes below.
from_radio_bytes = b'"\x17\x08\xdc\x8a\x8a\xae\x02\x12\x08"\x06\x00\x00\x00\x00\x00\x00\x1a\x00=\x00\x00\xb8@' from_radio_bytes = b'"\x17\x08\xdc\x8a\x8a\xae\x02\x12\x08"\x06\x00\x00\x00\x00\x00\x00\x1a\x00=\x00\x00\xb8@'
@@ -272,7 +305,8 @@ def test_handleFromRadio_with_node_info_tbeam_with_bad_data(reset_globals, caplo
@pytest.mark.unit @pytest.mark.unit
def test_MeshInterface_sendToRadioImpl(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_MeshInterface_sendToRadioImpl(caplog):
"""Test _sendToRadioImp()""" """Test _sendToRadioImp()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -282,7 +316,8 @@ def test_MeshInterface_sendToRadioImpl(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_MeshInterface_sendToRadio_no_proto(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_MeshInterface_sendToRadio_no_proto(caplog):
"""Test sendToRadio()""" """Test sendToRadio()"""
iface = MeshInterface() iface = MeshInterface()
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -292,7 +327,8 @@ def test_MeshInterface_sendToRadio_no_proto(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_sendData_too_long(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendData_too_long(caplog):
"""Test when data payload is too big""" """Test when data payload is too big"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
some_large_text = b'This is a long text that will be too long for send text.' some_large_text = b'This is a long text that will be too long for send text.'
@@ -316,7 +352,8 @@ def test_sendData_too_long(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_sendData_unknown_app(capsys, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendData_unknown_app(capsys):
"""Test sendData when unknown app""" """Test sendData when unknown app"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with pytest.raises(SystemExit) as pytest_wrapped_e: with pytest.raises(SystemExit) as pytest_wrapped_e:
@@ -329,7 +366,8 @@ def test_sendData_unknown_app(capsys, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_sendPosition_with_a_position(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendPosition_with_a_position(caplog):
"""Test sendPosition when lat/long/alt""" """Test sendPosition when lat/long/alt"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -340,7 +378,8 @@ def test_sendPosition_with_a_position(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_no_destination(capsys, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_no_destination(capsys):
"""Test _sendPacket()""" """Test _sendPacket()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with pytest.raises(SystemExit) as pytest_wrapped_e: with pytest.raises(SystemExit) as pytest_wrapped_e:
@@ -353,7 +392,8 @@ def test_sendPacket_with_no_destination(capsys, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_destination_as_int(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_destination_as_int(caplog):
"""Test _sendPacket() with int as a destination""" """Test _sendPacket() with int as a destination"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -363,7 +403,8 @@ def test_sendPacket_with_destination_as_int(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_destination_starting_with_a_bang(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_destination_starting_with_a_bang(caplog):
"""Test _sendPacket() with int as a destination""" """Test _sendPacket() with int as a destination"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -373,7 +414,8 @@ def test_sendPacket_with_destination_starting_with_a_bang(caplog, reset_globals)
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog):
"""Test _sendPacket() with BROADCAST_ADDR as a destination""" """Test _sendPacket() with BROADCAST_ADDR as a destination"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
@@ -383,7 +425,8 @@ def test_sendPacket_with_destination_as_BROADCAST_ADDR(caplog, reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys):
"""Test _sendPacket() with LOCAL_ADDR as a destination with no myInfo""" """Test _sendPacket() with LOCAL_ADDR as a destination with no myInfo"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
with pytest.raises(SystemExit) as pytest_wrapped_e: with pytest.raises(SystemExit) as pytest_wrapped_e:
@@ -397,11 +440,13 @@ def test_sendPacket_with_destination_as_LOCAL_ADDR_no_myInfo(capsys, reset_globa
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog):
"""Test _sendPacket() with LOCAL_ADDR as a destination with myInfo""" """Test _sendPacket() with LOCAL_ADDR as a destination with myInfo"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
myInfo = MagicMock() myInfo = MagicMock()
iface.myInfo = myInfo iface.myInfo = myInfo
iface.myInfo.my_node_num = 1
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
meshPacket = mesh_pb2.MeshPacket() meshPacket = mesh_pb2.MeshPacket()
iface._sendPacket(meshPacket, destinationId=LOCAL_ADDR) iface._sendPacket(meshPacket, destinationId=LOCAL_ADDR)
@@ -409,7 +454,8 @@ def test_sendPacket_with_destination_as_LOCAL_ADDR_with_myInfo(caplog, reset_glo
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_destination_is_blank_with_nodes(capsys, reset_globals, iface_with_nodes): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_destination_is_blank_with_nodes(capsys, iface_with_nodes):
"""Test _sendPacket() with '' as a destination with myInfo""" """Test _sendPacket() with '' as a destination with myInfo"""
iface = iface_with_nodes iface = iface_with_nodes
meshPacket = mesh_pb2.MeshPacket() meshPacket = mesh_pb2.MeshPacket()
@@ -423,7 +469,8 @@ def test_sendPacket_with_destination_is_blank_with_nodes(capsys, reset_globals,
@pytest.mark.unit @pytest.mark.unit
def test_sendPacket_with_destination_is_blank_without_nodes(caplog, reset_globals, iface_with_nodes): @pytest.mark.usefixtures("reset_globals")
def test_sendPacket_with_destination_is_blank_without_nodes(caplog, iface_with_nodes):
"""Test _sendPacket() with '' as a destination with myInfo""" """Test _sendPacket() with '' as a destination with myInfo"""
iface = iface_with_nodes iface = iface_with_nodes
iface.nodes = None iface.nodes = None
@@ -434,7 +481,8 @@ def test_sendPacket_with_destination_is_blank_without_nodes(caplog, reset_global
@pytest.mark.unit @pytest.mark.unit
def test_getMyNodeInfo(reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_getMyNodeInfo():
"""Test getMyNodeInfo()""" """Test getMyNodeInfo()"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
anode = iface.getNode(LOCAL_ADDR) anode = iface.getNode(LOCAL_ADDR)
@@ -448,7 +496,8 @@ def test_getMyNodeInfo(reset_globals):
@pytest.mark.unit @pytest.mark.unit
def test_generatePacketId(capsys, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_generatePacketId(capsys):
"""Test _generatePacketId() when no currentPacketId (not connected)""" """Test _generatePacketId() when no currentPacketId (not connected)"""
iface = MeshInterface(noProto=True) iface = MeshInterface(noProto=True)
# not sure when this condition would ever happen... but we can simulate it # not sure when this condition would ever happen... but we can simulate it
@@ -460,3 +509,164 @@ def test_generatePacketId(capsys, reset_globals):
assert re.search(r'Not connected yet, can not generate packet', out, re.MULTILINE) assert re.search(r'Not connected yet, can not generate packet', out, re.MULTILINE)
assert err == '' assert err == ''
assert pytest_wrapped_e.type == Exception assert pytest_wrapped_e.type == Exception
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_fixupPosition_empty_pos():
"""Test _fixupPosition()"""
iface = MeshInterface(noProto=True)
pos = {}
newpos = iface._fixupPosition(pos)
assert newpos == pos
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_fixupPosition_no_changes_needed():
"""Test _fixupPosition()"""
iface = MeshInterface(noProto=True)
pos = {"latitude": 101, "longitude": 102}
newpos = iface._fixupPosition(pos)
assert newpos == pos
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_fixupPosition():
"""Test _fixupPosition()"""
iface = MeshInterface(noProto=True)
pos = {"latitudeI": 1010000000, "longitudeI": 1020000000}
newpos = iface._fixupPosition(pos)
assert newpos == {"latitude": 101.0,
"latitudeI": 1010000000,
"longitude": 102.0,
"longitudeI": 1020000000}
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_nodeNumToId(iface_with_nodes):
"""Test _nodeNumToId()"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
someid = iface._nodeNumToId(2475227164)
assert someid == '!9388f81c'
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_nodeNumToId_not_found(iface_with_nodes):
"""Test _nodeNumToId()"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
someid = iface._nodeNumToId(123)
assert someid is None
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_nodeNumToId_to_all(iface_with_nodes):
"""Test _nodeNumToId()"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
someid = iface._nodeNumToId(0xffffffff)
assert someid == '^all'
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_getOrCreateByNum_minimal(iface_with_nodes):
"""Test _getOrCreateByNum()"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
tmp = iface._getOrCreateByNum(123)
assert tmp == {'num': 123}
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_getOrCreateByNum_not_found(iface_with_nodes):
"""Test _getOrCreateByNum()"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
with pytest.raises(Exception) as pytest_wrapped_e:
iface._getOrCreateByNum(0xffffffff)
assert pytest_wrapped_e.type == Exception
@pytest.mark.unit
@pytest.mark.usefixtures("reset_globals")
def test_getOrCreateByNum(iface_with_nodes):
"""Test _getOrCreateByNum()"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
tmp = iface._getOrCreateByNum(2475227164)
assert tmp['num'] == 2475227164
@pytest.mark.unit
def test_enter():
"""Test __enter__()"""
iface = MeshInterface(noProto=True)
assert iface == iface.__enter__()
@pytest.mark.unit
def test_exit_with_exception(caplog):
"""Test __exit__()"""
iface = MeshInterface(noProto=True)
with caplog.at_level(logging.ERROR):
iface.__exit__('foo', 'bar', 'baz')
assert re.search(r'An exception of type foo with value bar has occurred', caplog.text, re.MULTILINE)
assert re.search(r'Traceback: baz', caplog.text, re.MULTILINE)
@pytest.mark.unit
def test_showNodes_exclude_self(capsys, caplog, iface_with_nodes):
"""Test that we hit that continue statement"""
with caplog.at_level(logging.DEBUG):
iface = iface_with_nodes
iface.localNode.nodeNum = 2475227164
iface.showNodes()
iface.showNodes(includeSelf=False)
capsys.readouterr()
@pytest.mark.unitslow
def test_waitForConfig(capsys):
"""Test waitForConfig()"""
iface = MeshInterface(noProto=True)
# override how long to wait
iface._timeout = Timeout(0.01)
with pytest.raises(Exception) as pytest_wrapped_e:
iface.waitForConfig()
assert pytest_wrapped_e.type == Exception
out, err = capsys.readouterr()
assert re.search(r'Exception: Timed out waiting for interface config', err, re.MULTILINE)
assert out == ''
@pytest.mark.unit
def test_waitConnected_raises_an_exception(capsys):
"""Test waitConnected()"""
iface = MeshInterface(noProto=True)
with pytest.raises(Exception) as pytest_wrapped_e:
iface.failure = "warn about something"
iface._waitConnected(0.01)
assert pytest_wrapped_e.type == Exception
out, err = capsys.readouterr()
assert re.search(r'warn about something', err, re.MULTILINE)
assert out == ''
@pytest.mark.unit
def test_waitConnected_isConnected_timeout(capsys):
"""Test waitConnected()"""
with pytest.raises(Exception) as pytest_wrapped_e:
iface = MeshInterface()
iface._waitConnected(0.01)
assert pytest_wrapped_e.type == Exception
out, err = capsys.readouterr()
assert re.search(r'warn about something', err, re.MULTILINE)
assert out == ''

View File

@@ -11,6 +11,7 @@ from ..serial_interface import SerialInterface
from ..admin_pb2 import AdminMessage from ..admin_pb2 import AdminMessage
from ..channel_pb2 import Channel from ..channel_pb2 import Channel
from ..radioconfig_pb2 import RadioConfig from ..radioconfig_pb2 import RadioConfig
from ..util import Timeout
@pytest.mark.unit @pytest.mark.unit
@@ -29,7 +30,7 @@ def test_node(capsys):
@pytest.mark.unit @pytest.mark.unit
def test_node_reqquestConfig(): def test_node_requestConfig(capsys):
"""Test run requestConfig""" """Test run requestConfig"""
iface = MagicMock(autospec=SerialInterface) iface = MagicMock(autospec=SerialInterface)
amesg = MagicMock(autospec=AdminMessage) amesg = MagicMock(autospec=AdminMessage)
@@ -37,6 +38,9 @@ def test_node_reqquestConfig():
with patch('meshtastic.admin_pb2.AdminMessage', return_value=amesg): with patch('meshtastic.admin_pb2.AdminMessage', return_value=amesg):
anode = Node(mo, 'bar') anode = Node(mo, 'bar')
anode.requestConfig() anode.requestConfig()
out, err = capsys.readouterr()
assert re.search(r'Requesting preferences from remote node', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit @pytest.mark.unit
@@ -87,6 +91,16 @@ def test_setOwner_no_short_name_and_long_name_has_words(caplog):
assert re.search(r'p.set_owner.team:0', caplog.text, re.MULTILINE) assert re.search(r'p.set_owner.team:0', caplog.text, re.MULTILINE)
@pytest.mark.unit
def test_setOwner_long_name_no_short(caplog):
"""Test setOwner"""
anode = Node('foo', 'bar', noProto=True)
with caplog.at_level(logging.DEBUG):
anode.setOwner(long_name ='Aabo', is_licensed=True)
assert re.search(r'p.set_owner.long_name:Aabo:', caplog.text, re.MULTILINE)
assert re.search(r'p.set_owner.short_name:Aab:', caplog.text, re.MULTILINE)
@pytest.mark.unit @pytest.mark.unit
def test_exitSimulator(caplog): def test_exitSimulator(caplog):
"""Test exitSimulator""" """Test exitSimulator"""
@@ -106,13 +120,16 @@ def test_reboot(caplog):
@pytest.mark.unit @pytest.mark.unit
def test_setURL_empty_url(): def test_setURL_empty_url(capsys):
"""Test reboot""" """Test reboot"""
anode = Node('foo', 'bar', noProto=True) anode = Node('foo', 'bar', noProto=True)
with pytest.raises(SystemExit) as pytest_wrapped_e: with pytest.raises(SystemExit) as pytest_wrapped_e:
anode.setURL('') anode.setURL('')
assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 1 assert pytest_wrapped_e.value.code == 1
out, err = capsys.readouterr()
assert re.search(r'Warning: No RadioConfig has been read', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit @pytest.mark.unit
@@ -133,7 +150,7 @@ def test_setURL_valid_URL(caplog):
@pytest.mark.unit @pytest.mark.unit
def test_setURL_valid_URL_but_no_settings(caplog): def test_setURL_valid_URL_but_no_settings(capsys):
"""Test setURL""" """Test setURL"""
iface = MagicMock(autospec=SerialInterface) iface = MagicMock(autospec=SerialInterface)
url = "https://www.meshtastic.org/d/#" url = "https://www.meshtastic.org/d/#"
@@ -143,6 +160,9 @@ def test_setURL_valid_URL_but_no_settings(caplog):
anode.setURL(url) anode.setURL(url)
assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 1 assert pytest_wrapped_e.value.code == 1
out, err = capsys.readouterr()
assert re.search(r'Warning: There were no settings', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit @pytest.mark.unit
@@ -410,7 +430,7 @@ def test_deleteChannel_secondary_with_admin_channel_before_testing():
@pytest.mark.unit @pytest.mark.unit
def test_getChannelByName(capsys): def test_getChannelByName():
"""Get a channel by the name.""" """Get a channel by the name."""
anode = Node('foo', 'bar') anode = Node('foo', 'bar')
@@ -437,7 +457,7 @@ def test_getChannelByName(capsys):
@pytest.mark.unit @pytest.mark.unit
def test_getChannelByName_invalid_name(capsys): def test_getChannelByName_invalid_name():
"""Get a channel by the name but one that is not present.""" """Get a channel by the name but one that is not present."""
anode = Node('foo', 'bar') anode = Node('foo', 'bar')
@@ -464,7 +484,7 @@ def test_getChannelByName_invalid_name(capsys):
@pytest.mark.unit @pytest.mark.unit
def test_getDisabledChannel(capsys): def test_getDisabledChannel():
"""Get the first disabled channel.""" """Get the first disabled channel."""
anode = Node('foo', 'bar') anode = Node('foo', 'bar')
@@ -494,7 +514,7 @@ def test_getDisabledChannel(capsys):
@pytest.mark.unit @pytest.mark.unit
def test_getDisabledChannel_where_all_channels_are_used(capsys): def test_getDisabledChannel_where_all_channels_are_used():
"""Get the first disabled channel.""" """Get the first disabled channel."""
anode = Node('foo', 'bar') anode = Node('foo', 'bar')
@@ -518,7 +538,7 @@ def test_getDisabledChannel_where_all_channels_are_used(capsys):
@pytest.mark.unit @pytest.mark.unit
def test_getAdminChannelIndex(capsys): def test_getAdminChannelIndex():
"""Get the 'admin' channel index.""" """Get the 'admin' channel index."""
anode = Node('foo', 'bar') anode = Node('foo', 'bar')
@@ -545,7 +565,7 @@ def test_getAdminChannelIndex(capsys):
@pytest.mark.unit @pytest.mark.unit
def test_getAdminChannelIndex_when_no_admin_named_channel(capsys): def test_getAdminChannelIndex_when_no_admin_named_channel():
"""Get the 'admin' channel when there is not one.""" """Get the 'admin' channel when there is not one."""
anode = Node('foo', 'bar') anode = Node('foo', 'bar')
@@ -623,7 +643,7 @@ def test_writeConfig(caplog):
@pytest.mark.unit @pytest.mark.unit
def test_requestChannel_not_localNode(caplog): def test_requestChannel_not_localNode(caplog, capsys):
"""Test _requestChannel()""" """Test _requestChannel()"""
iface = MagicMock(autospec=SerialInterface) iface = MagicMock(autospec=SerialInterface)
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
@@ -633,6 +653,9 @@ def test_requestChannel_not_localNode(caplog):
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
anode._requestChannel(0) anode._requestChannel(0)
assert re.search(r'Requesting channel 0 info from remote node', caplog.text, re.MULTILINE) assert re.search(r'Requesting channel 0 info from remote node', caplog.text, re.MULTILINE)
out, err = capsys.readouterr()
assert re.search(r'Requesting channel 0 info', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit @pytest.mark.unit
@@ -857,3 +880,14 @@ def test_onResponseRequestSetting_with_error(capsys):
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert re.search(r'Error on response', out) assert re.search(r'Error on response', out)
assert err == '' assert err == ''
@pytest.mark.unitslow
def test_waitForConfig():
"""Test waitForConfig()"""
anode = Node('foo', 'bar')
radioConfig = RadioConfig()
anode.radioConfig = radioConfig
anode._timeout = Timeout(0.01)
result = anode.waitForConfig()
assert not result

View File

@@ -79,7 +79,7 @@ def test_watchGPIOs(caplog):
@pytest.mark.unit @pytest.mark.unit
def test_sendHardware_no_nodeid(): def test_sendHardware_no_nodeid(capsys):
"""Test sending no nodeid to _sendHardware()""" """Test sending no nodeid to _sendHardware()"""
iface = MagicMock(autospec=SerialInterface) iface = MagicMock(autospec=SerialInterface)
with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo: with patch('meshtastic.serial_interface.SerialInterface', return_value=iface) as mo:
@@ -87,3 +87,6 @@ def test_sendHardware_no_nodeid():
rhw = RemoteHardwareClient(mo) rhw = RemoteHardwareClient(mo)
rhw._sendHardware(None, None) rhw._sendHardware(None, None)
assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.type == SystemExit
out, err = capsys.readouterr()
assert re.search(r'Warning: Must use a destination node ID', out)
assert err == ''

View File

@@ -3,15 +3,19 @@
import re import re
from unittest.mock import patch from unittest.mock import patch, mock_open
import pytest import pytest
from ..serial_interface import SerialInterface from ..serial_interface import SerialInterface
@pytest.mark.unit @pytest.mark.unit
@patch("time.sleep")
@patch("termios.tcsetattr")
@patch("termios.tcgetattr")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch('serial.Serial') @patch('serial.Serial')
@patch('meshtastic.util.findPorts', return_value=['/dev/ttyUSBfake']) @patch('meshtastic.util.findPorts', return_value=['/dev/ttyUSBfake'])
def test_SerialInterface_single_port(mocked_findPorts, mocked_serial): def test_SerialInterface_single_port(mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_sleep, capsys):
"""Test that we can instantiate a SerialInterface with a single port""" """Test that we can instantiate a SerialInterface with a single port"""
iface = SerialInterface(noProto=True) iface = SerialInterface(noProto=True)
iface.showInfo() iface.showInfo()
@@ -19,6 +23,16 @@ def test_SerialInterface_single_port(mocked_findPorts, mocked_serial):
iface.close() iface.close()
mocked_findPorts.assert_called() mocked_findPorts.assert_called()
mocked_serial.assert_called() mocked_serial.assert_called()
mocked_open.assert_called()
mock_get.assert_called()
mock_set.assert_called()
mock_sleep.assert_called()
out, err = capsys.readouterr()
assert re.search(r'Nodes in mesh', out, re.MULTILINE)
assert re.search(r'Preferences', out, re.MULTILINE)
assert re.search(r'Channels', out, re.MULTILINE)
assert re.search(r'Primary channel', out, re.MULTILINE)
assert err == ''
@pytest.mark.unit @pytest.mark.unit

View File

@@ -2,6 +2,7 @@
import re import re
import subprocess import subprocess
import time import time
import platform
import os import os
# Do not like using hard coded sleeps, but it probably makes # Do not like using hard coded sleeps, but it probably makes
@@ -140,8 +141,9 @@ def test_smoke1_nodes():
"""Test --nodes""" """Test --nodes"""
return_value, out = subprocess.getstatusoutput('meshtastic --nodes') return_value, out = subprocess.getstatusoutput('meshtastic --nodes')
assert re.match(r'Connected to radio', out) assert re.match(r'Connected to radio', out)
assert re.search(r'^│ N │ User', out, re.MULTILINE) if platform.system() != 'Windows':
assert re.search(r'^│ 1 │', out, re.MULTILINE) assert re.search(r' User ', out, re.MULTILINE)
assert re.search(r' 1 ', out, re.MULTILINE)
assert return_value == 0 assert return_value == 0

View File

@@ -0,0 +1,706 @@
"""Meshtastic smoke tests with a single virtual device via localhost.
During the CI build of the Meshtastic-device, a build.zip file is created.
Inside that build.zip is a standalone executable meshtasticd_linux_amd64.
That linux executable will simulate a Meshtastic device listening on localhost.
This smoke test runs against that localhost.
"""
import re
import subprocess
import time
import platform
import os
# Do not like using hard coded sleeps, but it probably makes
# sense to pause for the radio at apprpriate times
import pytest
from ..util import findPorts
# seconds to pause after running a meshtastic command
PAUSE_AFTER_COMMAND = 0.1
PAUSE_AFTER_REBOOT = 0.1
#TODO: need to fix the virtual device to have a reboot. When you issue the command
# below, you get "FIXME implement reboot for this platform"
#@pytest.mark.smokevirt
#def test_smokevirt_reboot():
# """Test reboot"""
# return_value, _ = subprocess.getstatusoutput('meshtastic --host localhost --reboot')
# assert return_value == 0
# # pause for the radio to reset
# time.sleep(8)
@pytest.mark.smokevirt
def test_smokevirt_info():
"""Test --info"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Owner', out, re.MULTILINE)
assert re.search(r'^My info', out, re.MULTILINE)
assert re.search(r'^Nodes in mesh', out, re.MULTILINE)
assert re.search(r'^Preferences', out, re.MULTILINE)
assert re.search(r'^Channels', out, re.MULTILINE)
assert re.search(r'^ PRIMARY', out, re.MULTILINE)
assert re.search(r'^Primary channel URL', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_sendping():
"""Test --sendping"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --sendping')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Sending ping message', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_get_with_invalid_setting():
"""Test '--get a_bad_setting'."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --get a_bad_setting')
assert re.search(r'Choices in sorted order', out)
assert return_value == 0
@pytest.mark.smokevirt
def test_set_with_invalid_setting():
"""Test '--set a_bad_setting'."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set a_bad_setting foo')
assert re.search(r'Choices in sorted order', out)
assert return_value == 0
@pytest.mark.smokevirt
def test_ch_set_with_invalid_settingpatch_find_ports():
"""Test '--ch-set with a_bad_setting'."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set invalid_setting foo --ch-index 0')
assert re.search(r'Choices in sorted order', out)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_pos_fields():
"""Test --pos-fields (with some values POS_ALTITUDE POS_ALT_MSL POS_BATTERY)"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --pos-fields POS_ALTITUDE POS_ALT_MSL POS_BATTERY')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Setting position fields to 35', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --pos-fields')
assert re.match(r'Connected to radio', out)
assert re.search(r'POS_ALTITUDE', out, re.MULTILINE)
assert re.search(r'POS_ALT_MSL', out, re.MULTILINE)
assert re.search(r'POS_BATTERY', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_test_with_arg_but_no_hardware():
"""Test --test
Note: Since only one device is connected, it will not do much.
"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --test')
assert re.search(r'^Warning: Must have at least two devices', out, re.MULTILINE)
assert return_value == 1
@pytest.mark.smokevirt
def test_smokevirt_debug():
"""Test --debug"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info --debug')
assert re.search(r'^Owner', out, re.MULTILINE)
assert re.search(r'^DEBUG file', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_seriallog_to_file():
"""Test --seriallog to a file creates a file"""
filename = 'tmpoutput.txt'
if os.path.exists(f"{filename}"):
os.remove(f"{filename}")
return_value, _ = subprocess.getstatusoutput(f'meshtastic --host localhost --info --seriallog {filename}')
assert os.path.exists(f"{filename}")
assert return_value == 0
os.remove(f"{filename}")
@pytest.mark.smokevirt
def test_smokevirt_qr():
"""Test --qr"""
filename = 'tmpqr'
if os.path.exists(f"{filename}"):
os.remove(f"{filename}")
return_value, _ = subprocess.getstatusoutput(f'meshtastic --host localhost --qr > {filename}')
assert os.path.exists(f"{filename}")
# not really testing that a valid qr code is created, just that the file size
# is reasonably big enough for a qr code
assert os.stat(f"{filename}").st_size > 20000
assert return_value == 0
os.remove(f"{filename}")
@pytest.mark.smokevirt
def test_smokevirt_nodes():
"""Test --nodes"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --nodes')
assert re.match(r'Connected to radio', out)
if platform.system() != 'Windows':
assert re.search(r' User ', out, re.MULTILINE)
assert re.search(r' 1 ', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_send_hello():
"""Test --sendtext hello"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --sendtext hello')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Sending text message hello to \^all', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_port():
"""Test --port"""
# first, get the ports
ports = findPorts()
# hopefully there is none
assert len(ports) == 0
@pytest.mark.smokevirt
def test_smokevirt_set_is_router_true():
"""Test --set is_router true"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set is_router true')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Set is_router to true', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --get is_router')
assert re.search(r'^is_router: True', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_set_location_info():
"""Test --setlat, --setlon and --setalt """
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --setlat 32.7767 --setlon -96.7970 --setalt 1337')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Fixing altitude', out, re.MULTILINE)
assert re.search(r'^Fixing latitude', out, re.MULTILINE)
assert re.search(r'^Fixing longitude', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out2 = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(r'1337', out2, re.MULTILINE)
assert re.search(r'32.7767', out2, re.MULTILINE)
assert re.search(r'-96.797', out2, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_set_is_router_false():
"""Test --set is_router false"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set is_router false')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Set is_router to false', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --get is_router')
assert re.search(r'^is_router: False', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_set_owner():
"""Test --set-owner name"""
# make sure the owner is not Joe
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set-owner Bob')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Setting device owner to Bob', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert not re.search(r'Owner: Joe', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set-owner Joe')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Setting device owner to Joe', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(r'Owner: Joe', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_set_team():
"""Test --set-team """
# unset the team
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set-team CLEAR')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Setting team to CLEAR', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_REBOOT)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set-team CYAN')
assert re.search(r'Setting team to CYAN', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_REBOOT)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(r'CYAN', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_ch_values():
"""Test --ch-longslow, --ch-longfast, --ch-mediumslow, --ch-mediumsfast,
--ch-shortslow, and --ch-shortfast arguments
"""
exp = {
'--ch-longslow': 'Bw125Cr48Sf4096',
'--ch-longfast': 'Bw31_25Cr48Sf512',
'--ch-mediumslow': 'Bw250Cr46Sf2048',
'--ch-mediumfast': 'Bw250Cr47Sf1024',
'--ch-shortslow': '{ "psk',
'--ch-shortfast': 'Bw500Cr45Sf128'
}
for key, val in exp.items():
return_value, out = subprocess.getstatusoutput(f'meshtastic --host localhost {key}')
assert re.match(r'Connected to radio', out)
assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
assert return_value == 0
# pause for the radio (might reboot)
time.sleep(PAUSE_AFTER_REBOOT)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(val, out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_ch_set_name():
"""Test --ch-set name"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert not re.search(r'MyChannel', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set name MyChannel')
assert re.match(r'Connected to radio', out)
assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set name MyChannel --ch-index 0')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Set name to MyChannel', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(r'MyChannel', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_ch_set_downlink_and_uplink():
"""Test -ch-set downlink_enabled X and --ch-set uplink_enabled X"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set downlink_enabled false --ch-set uplink_enabled false')
assert re.match(r'Connected to radio', out)
assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
# pylint: disable=C0301
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set downlink_enabled false --ch-set uplink_enabled false --ch-index 0')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert not re.search(r'uplinkEnabled', out, re.MULTILINE)
assert not re.search(r'downlinkEnabled', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
# pylint: disable=C0301
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set downlink_enabled true --ch-set uplink_enabled true --ch-index 0')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Set downlink_enabled to true', out, re.MULTILINE)
assert re.search(r'^Set uplink_enabled to true', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(r'uplinkEnabled', out, re.MULTILINE)
assert re.search(r'downlinkEnabled', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_ch_add_and_ch_del():
"""Test --ch-add"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-add testing')
assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'SECONDARY', out, re.MULTILINE)
assert re.search(r'testing', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-index 1 --ch-del')
assert re.search(r'Deleting channel 1', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_REBOOT)
# make sure the secondar channel is not there
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert not re.search(r'SECONDARY', out, re.MULTILINE)
assert not re.search(r'testing', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_ch_enable_and_disable():
"""Test --ch-enable and --ch-disable"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-add testing')
assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'SECONDARY', out, re.MULTILINE)
assert re.search(r'testing', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
# ensure they need to specify a --ch-index
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-disable')
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-disable --ch-index 1')
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'DISABLED', out, re.MULTILINE)
assert re.search(r'testing', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-enable --ch-index 1')
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'SECONDARY', out, re.MULTILINE)
assert re.search(r'testing', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-del --ch-index 1')
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_ch_del_a_disabled_non_primary_channel():
"""Test --ch-del will work on a disabled non-primary channel."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-add testing')
assert re.search(r'Writing modified channels to device', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'SECONDARY', out, re.MULTILINE)
assert re.search(r'testing', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
# ensure they need to specify a --ch-index
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-disable')
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-del --ch-index 1')
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert not re.search(r'DISABLED', out, re.MULTILINE)
assert not re.search(r'SECONDARY', out, re.MULTILINE)
assert not re.search(r'testing', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_attempt_to_delete_primary_channel():
"""Test that we cannot delete the PRIMARY channel."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-del --ch-index 0')
assert re.search(r'Warning: Cannot delete primary channel', out, re.MULTILINE)
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_attempt_to_disable_primary_channel():
"""Test that we cannot disable the PRIMARY channel."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-disable --ch-index 0')
assert re.search(r'Warning: Cannot enable', out, re.MULTILINE)
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_attempt_to_enable_primary_channel():
"""Test that we cannot enable the PRIMARY channel."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-enable --ch-index 0')
assert re.search(r'Warning: Cannot enable', out, re.MULTILINE)
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_ensure_ch_del_second_of_three_channels():
"""Test that when we delete the 2nd of 3 channels, that it deletes the correct channel."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-add testing1')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'SECONDARY', out, re.MULTILINE)
assert re.search(r'testing1', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-add testing2')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'testing2', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-del --ch-index 1')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'testing2', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-del --ch-index 1')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_ensure_ch_del_third_of_three_channels():
"""Test that when we delete the 3rd of 3 channels, that it deletes the correct channel."""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-add testing1')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'SECONDARY', out, re.MULTILINE)
assert re.search(r'testing1', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-add testing2')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'testing2', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-del --ch-index 2')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.match(r'Connected to radio', out)
assert re.search(r'testing1', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-del --ch-index 1')
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_ch_set_modem_config():
"""Test --ch-set modem_config"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set modem_config Bw31_25Cr48Sf512')
assert re.search(r'Warning: Need to specify', out, re.MULTILINE)
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert not re.search(r'Bw31_25Cr48Sf512', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set modem_config Bw31_25Cr48Sf512 --ch-index 0')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Set modem_config to Bw31_25Cr48Sf512', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(r'Bw31_25Cr48Sf512', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_seturl_default():
"""Test --seturl with default value"""
# set some channel value so we no longer have a default channel
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --ch-set name foo --ch-index 0')
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
# ensure we no longer have a default primary channel
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert not re.search('CgUYAyIBAQ', out, re.MULTILINE)
assert return_value == 0
url = "https://www.meshtastic.org/d/#CgUYAyIBAQ"
return_value, out = subprocess.getstatusoutput(f"meshtastic --host localhost --seturl {url}")
assert re.match(r'Connected to radio', out)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search('CgUYAyIBAQ', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_seturl_invalid_url():
"""Test --seturl with invalid url"""
# Note: This url is no longer a valid url.
url = "https://www.meshtastic.org/c/#GAMiENTxuzogKQdZ8Lz_q89Oab8qB0RlZmF1bHQ="
return_value, out = subprocess.getstatusoutput(f"meshtastic --host localhost --seturl {url}")
assert re.match(r'Connected to radio', out)
assert re.search('Warning: There were no settings', out, re.MULTILINE)
assert return_value == 1
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
@pytest.mark.smokevirt
def test_smokevirt_configure():
"""Test --configure"""
_ , out = subprocess.getstatusoutput(f"meshtastic --host localhost --configure example_config.yaml")
assert re.match(r'Connected to radio', out)
assert re.search('^Setting device owner to Bob TBeam', out, re.MULTILINE)
assert re.search('^Fixing altitude at 304 meters', out, re.MULTILINE)
assert re.search('^Fixing latitude at 35.8', out, re.MULTILINE)
assert re.search('^Fixing longitude at -93.8', out, re.MULTILINE)
assert re.search('^Setting device position', out, re.MULTILINE)
assert re.search('^Set region to 1', out, re.MULTILINE)
assert re.search('^Set is_always_powered to true', out, re.MULTILINE)
assert re.search('^Set send_owner_interval to 2', out, re.MULTILINE)
assert re.search('^Set screen_on_secs to 31536000', out, re.MULTILINE)
assert re.search('^Set wait_bluetooth_secs to 31536000', out, re.MULTILINE)
assert re.search('^Writing modified preferences to device', out, re.MULTILINE)
# pause for the radio
time.sleep(PAUSE_AFTER_REBOOT)
@pytest.mark.smokevirt
def test_smokevirt_set_ham():
"""Test --set-ham
Note: Do a factory reset after this setting so it is very short-lived.
"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set-ham KI1234')
assert re.search(r'Setting Ham ID', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_REBOOT)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --info')
assert re.search(r'Owner: KI1234', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_set_wifi_settings():
"""Test --set wifi_ssid and --set wifi_password"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set wifi_ssid "some_ssid" --set wifi_password "temp1234"')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Set wifi_ssid to some_ssid', out, re.MULTILINE)
assert re.search(r'^Set wifi_password to temp1234', out, re.MULTILINE)
assert return_value == 0
# pause for the radio
time.sleep(PAUSE_AFTER_COMMAND)
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --get wifi_ssid --get wifi_password')
assert re.search(r'^wifi_ssid: some_ssid', out, re.MULTILINE)
assert re.search(r'^wifi_password: sekrit', out, re.MULTILINE)
assert return_value == 0
@pytest.mark.smokevirt
def test_smokevirt_factory_reset():
"""Test factory reset"""
return_value, out = subprocess.getstatusoutput('meshtastic --host localhost --set factory_reset true')
assert re.match(r'Connected to radio', out)
assert re.search(r'^Set factory_reset to true', out, re.MULTILINE)
assert re.search(r'^Writing modified preferences to device', out, re.MULTILINE)
assert return_value == 0
# NOTE: The virtual radio will not respond well after this command. Need to re-start the virtual program at this point.
# TODO: fix?

View File

@@ -19,7 +19,8 @@ def test_StreamInterface():
# Note: This takes a bit, so moving from unit to slow # Note: This takes a bit, so moving from unit to slow
@pytest.mark.unitslow @pytest.mark.unitslow
def test_StreamInterface_with_noProto(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_StreamInterface_with_noProto(caplog):
"""Test that we can instantiate a StreamInterface based on nonProto """Test that we can instantiate a StreamInterface based on nonProto
and we can read/write bytes from a mocked stream and we can read/write bytes from a mocked stream
""" """
@@ -34,11 +35,12 @@ def test_StreamInterface_with_noProto(caplog, reset_globals):
assert data == test_data assert data == test_data
# Note: This takes a bit, so moving from unit to slow ## Note: This takes a bit, so moving from unit to slow
# Tip: If you want to see the print output, run with '-s' flag: ## Tip: If you want to see the print output, run with '-s' flag:
# pytest -s meshtastic/tests/test_stream_interface.py::test_sendToRadioImpl ## pytest -s meshtastic/tests/test_stream_interface.py::test_sendToRadioImpl
@pytest.mark.unitslow @pytest.mark.unitslow
def test_sendToRadioImpl(caplog, reset_globals): @pytest.mark.usefixtures("reset_globals")
def test_sendToRadioImpl(caplog):
"""Test _sendToRadioImpl()""" """Test _sendToRadioImpl()"""
# def add_header(b): # def add_header(b):
@@ -78,4 +80,3 @@ def test_sendToRadioImpl(caplog, reset_globals):
assert re.search(r'Sending: ', caplog.text, re.MULTILINE) assert re.search(r'Sending: ', caplog.text, re.MULTILINE)
assert re.search(r'reading character', caplog.text, re.MULTILINE) assert re.search(r'reading character', caplog.text, re.MULTILINE)
assert re.search(r'In reader loop', caplog.text, re.MULTILINE) assert re.search(r'In reader loop', caplog.text, re.MULTILINE)
print(caplog.text)

View File

@@ -13,6 +13,7 @@ def test_TCPInterface(capsys):
"""Test that we can instantiate a TCPInterface""" """Test that we can instantiate a TCPInterface"""
with patch('socket.socket') as mock_socket: with patch('socket.socket') as mock_socket:
iface = TCPInterface(hostname='localhost', noProto=True) iface = TCPInterface(hostname='localhost', noProto=True)
iface.myConnect()
iface.showInfo() iface.showInfo()
iface.localNode.showInfo() iface.localNode.showInfo()
out, err = capsys.readouterr() out, err = capsys.readouterr()
@@ -24,3 +25,28 @@ def test_TCPInterface(capsys):
assert err == '' assert err == ''
assert mock_socket.called assert mock_socket.called
iface.close() iface.close()
@pytest.mark.unit
def test_TCPInterface_exception():
"""Test that we can instantiate a TCPInterface"""
def throw_an_exception():
raise ValueError("Fake exception.")
with patch('meshtastic.tcp_interface.TCPInterface._socket_shutdown') as mock_shutdown:
mock_shutdown.side_effect = throw_an_exception
with patch('socket.socket') as mock_socket:
iface = TCPInterface(hostname='localhost', noProto=True)
iface.myConnect()
iface.close()
assert mock_socket.called
assert mock_shutdown.called
@pytest.mark.unit
def test_TCPInterface_without_connecting():
"""Test that we can instantiate a TCPInterface with connectNow as false"""
with patch('socket.socket'):
iface = TCPInterface(hostname='localhost', noProto=True, connectNow=False)
assert iface.socket is None

View File

@@ -0,0 +1,266 @@
"""Meshtastic unit tests for tunnel.py"""
import re
import sys
import logging
from unittest.mock import patch, MagicMock
import pytest
from ..tcp_interface import TCPInterface
from ..tunnel import Tunnel, onTunnelReceive
from ..globals import Globals
@pytest.mark.unit
@patch('platform.system')
def test_Tunnel_on_non_linux_system(mock_platform_system):
"""Test that we cannot instantiate a Tunnel on a non Linux system"""
a_mock = MagicMock()
a_mock.return_value = 'notLinux'
mock_platform_system.side_effect = a_mock
with patch('socket.socket') as mock_socket:
with pytest.raises(Exception) as pytest_wrapped_e:
iface = TCPInterface(hostname='localhost', noProto=True)
Tunnel(iface)
assert pytest_wrapped_e.type == Exception
assert mock_socket.called
@pytest.mark.unit
@patch('platform.system')
def test_Tunnel_without_interface(mock_platform_system):
"""Test that we can not instantiate a Tunnel without a valid interface"""
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with pytest.raises(Exception) as pytest_wrapped_e:
Tunnel(None)
assert pytest_wrapped_e.type == Exception
@pytest.mark.unitslow
@patch('platform.system')
def test_Tunnel_with_interface(mock_platform_system, caplog, iface_with_nodes):
"""Test that we can not instantiate a Tunnel without a valid interface"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.WARNING):
with patch('socket.socket'):
tun = Tunnel(iface)
assert tun == Globals.getInstance().get_tunnelInstance()
iface.close()
assert re.search(r'Not creating a TapDevice()', caplog.text, re.MULTILINE)
assert re.search(r'Not starting TUN reader', caplog.text, re.MULTILINE)
assert re.search(r'Not sending packet', caplog.text, re.MULTILINE)
@pytest.mark.unitslow
@patch('platform.system')
def test_onTunnelReceive_from_ourselves(mock_platform_system, caplog, iface_with_nodes):
"""Test onTunnelReceive"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
sys.argv = ['']
Globals.getInstance().set_args(sys.argv)
packet = {'decoded': { 'payload': 'foo'}, 'from': 2475227164}
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
Globals.getInstance().set_tunnelInstance(tun)
onTunnelReceive(packet, iface)
assert re.search(r'in onTunnelReceive', caplog.text, re.MULTILINE)
assert re.search(r'Ignoring message we sent', caplog.text, re.MULTILINE)
@pytest.mark.unit
@patch('platform.system')
def test_onTunnelReceive_from_someone_else(mock_platform_system, caplog, iface_with_nodes):
"""Test onTunnelReceive"""
iface = iface_with_nodes
iface.myInfo.my_node_num = 2475227164
sys.argv = ['']
Globals.getInstance().set_args(sys.argv)
packet = {'decoded': { 'payload': 'foo'}, 'from': 123}
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
Globals.getInstance().set_tunnelInstance(tun)
onTunnelReceive(packet, iface)
assert re.search(r'in onTunnelReceive', caplog.text, re.MULTILINE)
@pytest.mark.unitslow
@patch('platform.system')
def test_shouldFilterPacket_random(mock_platform_system, caplog, iface_with_nodes):
"""Test _shouldFilterPacket()"""
iface = iface_with_nodes
iface.noProto = True
# random packet
packet = b'1234567890123456789012345678901234567890'
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
ignore = tun._shouldFilterPacket(packet)
assert not ignore
@pytest.mark.unitslow
@patch('platform.system')
def test_shouldFilterPacket_in_blacklist(mock_platform_system, caplog, iface_with_nodes):
"""Test _shouldFilterPacket()"""
iface = iface_with_nodes
iface.noProto = True
# faked IGMP
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
ignore = tun._shouldFilterPacket(packet)
assert ignore
@pytest.mark.unitslow
@patch('platform.system')
def test_shouldFilterPacket_icmp(mock_platform_system, caplog, iface_with_nodes):
"""Test _shouldFilterPacket()"""
iface = iface_with_nodes
iface.noProto = True
# faked ICMP
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
ignore = tun._shouldFilterPacket(packet)
assert re.search(r'forwarding ICMP message', caplog.text, re.MULTILINE)
assert not ignore
@pytest.mark.unit
@patch('platform.system')
def test_shouldFilterPacket_udp(mock_platform_system, caplog, iface_with_nodes):
"""Test _shouldFilterPacket()"""
iface = iface_with_nodes
iface.noProto = True
# faked UDP
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
ignore = tun._shouldFilterPacket(packet)
assert re.search(r'forwarding udp', caplog.text, re.MULTILINE)
assert not ignore
@pytest.mark.unitslow
@patch('platform.system')
def test_shouldFilterPacket_udp_blacklisted(mock_platform_system, caplog, iface_with_nodes):
"""Test _shouldFilterPacket()"""
iface = iface_with_nodes
iface.noProto = True
# faked UDP
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x6c\x07\x6c\x00\x00\x00'
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
# Note: custom logging level
LOG_TRACE = 5
with caplog.at_level(LOG_TRACE):
with patch('socket.socket'):
tun = Tunnel(iface)
ignore = tun._shouldFilterPacket(packet)
assert re.search(r'ignoring blacklisted UDP', caplog.text, re.MULTILINE)
assert ignore
@pytest.mark.unit
@patch('platform.system')
def test_shouldFilterPacket_tcp(mock_platform_system, caplog, iface_with_nodes):
"""Test _shouldFilterPacket()"""
iface = iface_with_nodes
iface.noProto = True
# faked TCP
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
ignore = tun._shouldFilterPacket(packet)
assert re.search(r'forwarding tcp', caplog.text, re.MULTILINE)
assert not ignore
@pytest.mark.unitslow
@patch('platform.system')
def test_shouldFilterPacket_tcp_blacklisted(mock_platform_system, caplog, iface_with_nodes):
"""Test _shouldFilterPacket()"""
iface = iface_with_nodes
iface.noProto = True
# faked TCP
packet = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x0c\x17\x0c\x00\x00\x00'
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
# Note: custom logging level
LOG_TRACE = 5
with caplog.at_level(LOG_TRACE):
with patch('socket.socket'):
tun = Tunnel(iface)
ignore = tun._shouldFilterPacket(packet)
assert re.search(r'ignoring blacklisted TCP', caplog.text, re.MULTILINE)
assert ignore
@pytest.mark.unitslow
@patch('platform.system')
def test_ipToNodeId_none(mock_platform_system, caplog, iface_with_nodes):
"""Test _ipToNodeId()"""
iface = iface_with_nodes
iface.noProto = True
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
nodeid = tun._ipToNodeId('something not useful')
assert nodeid is None
@pytest.mark.unitslow
@patch('platform.system')
def test_ipToNodeId_all(mock_platform_system, caplog, iface_with_nodes):
"""Test _ipToNodeId()"""
iface = iface_with_nodes
iface.noProto = True
a_mock = MagicMock()
a_mock.return_value = 'Linux'
mock_platform_system.side_effect = a_mock
with caplog.at_level(logging.DEBUG):
with patch('socket.socket'):
tun = Tunnel(iface)
nodeid = tun._ipToNodeId(b'\x00\x00\xff\xff')
assert nodeid == '^all'

View File

@@ -3,11 +3,15 @@
import re import re
import logging import logging
from unittest.mock import patch
import pytest import pytest
from meshtastic.util import (fixme, stripnl, pskToString, our_exit, from meshtastic.util import (fixme, stripnl, pskToString, our_exit,
support_info, genPSK256, fromStr, fromPSK, support_info, genPSK256, fromStr, fromPSK,
quoteBooleans, catchAndIgnore) quoteBooleans, catchAndIgnore,
remove_keys_from_dict, Timeout, hexstr,
ipstr, readnet_u16, findPorts, convert_mac_addr,
snake_to_camel, camel_to_snake)
@pytest.mark.unit @pytest.mark.unit
@@ -38,7 +42,7 @@ def test_fromStr():
assert fromStr('abc') == 'abc' assert fromStr('abc') == 'abc'
@pytest.mark.unit @pytest.mark.unitslow
def test_quoteBooleans(): def test_quoteBooleans():
"""Test quoteBooleans""" """Test quoteBooleans"""
assert quoteBooleans('') == '' assert quoteBooleans('') == ''
@@ -85,13 +89,13 @@ def test_pskToString_one_byte_zero_value():
assert pskToString(bytes([0x00])) == 'unencrypted' assert pskToString(bytes([0x00])) == 'unencrypted'
@pytest.mark.unit @pytest.mark.unitslow
def test_pskToString_one_byte_non_zero_value(): def test_pskToString_one_byte_non_zero_value():
"""Test pskToString one byte that is non-zero""" """Test pskToString one byte that is non-zero"""
assert pskToString(bytes([0x01])) == 'default' assert pskToString(bytes([0x01])) == 'default'
@pytest.mark.unit @pytest.mark.unitslow
def test_pskToString_many_bytes(): def test_pskToString_many_bytes():
"""Test pskToString many bytes""" """Test pskToString many bytes"""
assert pskToString(bytes([0x02, 0x01])) == 'secret' assert pskToString(bytes([0x02, 0x01])) == 'secret'
@@ -103,25 +107,31 @@ def test_pskToString_simple():
assert pskToString(bytes([0x03])) == 'simple2' assert pskToString(bytes([0x03])) == 'simple2'
@pytest.mark.unit @pytest.mark.unitslow
def test_our_exit_zero_return_value(): def test_our_exit_zero_return_value(capsys):
"""Test our_exit with a zero return value""" """Test our_exit with a zero return value"""
with pytest.raises(SystemExit) as pytest_wrapped_e: with pytest.raises(SystemExit) as pytest_wrapped_e:
our_exit("Warning: Some message", 0) our_exit("Warning: Some message", 0)
out, err = capsys.readouterr()
assert re.search(r'Warning: Some message', out, re.MULTILINE)
assert err == ''
assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 0 assert pytest_wrapped_e.value.code == 0
@pytest.mark.unit @pytest.mark.unitslow
def test_our_exit_non_zero_return_value(): def test_our_exit_non_zero_return_value(capsys):
"""Test our_exit with a non-zero return value""" """Test our_exit with a non-zero return value"""
with pytest.raises(SystemExit) as pytest_wrapped_e: with pytest.raises(SystemExit) as pytest_wrapped_e:
our_exit("Error: Some message", 1) our_exit("Error: Some message", 1)
out, err = capsys.readouterr()
assert re.search(r'Error: Some message', out, re.MULTILINE)
assert err == ''
assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 1 assert pytest_wrapped_e.value.code == 1
@pytest.mark.unit @pytest.mark.unitslow
def test_fixme(): def test_fixme():
"""Test fixme()""" """Test fixme()"""
with pytest.raises(Exception) as pytest_wrapped_e: with pytest.raises(Exception) as pytest_wrapped_e:
@@ -149,3 +159,115 @@ def test_catchAndIgnore(caplog):
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):
catchAndIgnore("something", some_closure) catchAndIgnore("something", some_closure)
assert re.search(r'Exception thrown in something', caplog.text, re.MULTILINE) assert re.search(r'Exception thrown in something', caplog.text, re.MULTILINE)
@pytest.mark.unitslow
def test_remove_keys_from_dict_empty_keys_empty_dict():
"""Test when keys and dict both are empty"""
assert not remove_keys_from_dict((), {})
@pytest.mark.unitslow
def test_remove_keys_from_dict_empty_dict():
"""Test when dict is empty"""
assert not remove_keys_from_dict(('a'), {})
@pytest.mark.unit
def test_remove_keys_from_dict_empty_keys():
"""Test when keys is empty"""
assert remove_keys_from_dict((), {'a':1}) == {'a':1}
@pytest.mark.unitslow
def test_remove_keys_from_dict():
"""Test remove_keys_from_dict()"""
assert remove_keys_from_dict(('b'), {'a':1, 'b':2}) == {'a':1}
@pytest.mark.unitslow
def test_remove_keys_from_dict_multiple_keys():
"""Test remove_keys_from_dict()"""
keys = ('a', 'b')
adict = {'a': 1, 'b': 2, 'c': 3}
assert remove_keys_from_dict(keys, adict) == {'c':3}
@pytest.mark.unit
def test_remove_keys_from_dict_nested():
"""Test remove_keys_from_dict()"""
keys = ('b')
adict = {'a': {'b': 1}, 'b': 2, 'c': 3}
exp = {'a': {}, 'c': 3}
assert remove_keys_from_dict(keys, adict) == exp
@pytest.mark.unitslow
def test_Timeout_not_found():
"""Test Timeout()"""
to = Timeout(0.2)
attrs = ('foo')
to.waitForSet('bar', attrs)
@pytest.mark.unitslow
def test_Timeout_found():
"""Test Timeout()"""
to = Timeout(0.2)
attrs = ()
to.waitForSet('bar', attrs)
@pytest.mark.unitslow
def test_hexstr():
"""Test hexstr()"""
assert hexstr(b'123') == '31:32:33'
assert hexstr(b'') == ''
@pytest.mark.unitslow
def test_ipstr():
"""Test ipstr()"""
assert ipstr(b'1234') == '49.50.51.52'
assert ipstr(b'') == ''
@pytest.mark.unitslow
def test_readnet_u16():
"""Test readnet_u16()"""
assert readnet_u16(b'123456', 2) == 13108
@pytest.mark.unitslow
@patch('serial.tools.list_ports.comports', return_value=[])
def test_findPorts_when_none_found(patch_comports):
"""Test findPorts()"""
assert not findPorts()
patch_comports.assert_called()
@pytest.mark.unitslow
def test_convert_mac_addr():
"""Test convert_mac_addr()"""
assert convert_mac_addr('/c0gFyhb') == 'fd:cd:20:17:28:5b'
assert convert_mac_addr('fd:cd:20:17:28:5b') == 'fd:cd:20:17:28:5b'
assert convert_mac_addr('') == ''
@pytest.mark.unit
def test_snake_to_camel():
"""Test snake_to_camel"""
assert snake_to_camel('') == ''
assert snake_to_camel('foo') == 'foo'
assert snake_to_camel('foo_bar') == 'fooBar'
assert snake_to_camel('fooBar') == 'fooBar'
@pytest.mark.unit
def test_camel_to_snake():
"""Test camel_to_snake"""
assert camel_to_snake('') == ''
assert camel_to_snake('foo') == 'foo'
assert camel_to_snake('Foo') == 'foo'
assert camel_to_snake('fooBar') == 'foo_bar'
assert camel_to_snake('fooBarBaz') == 'foo_bar_baz'

View File

@@ -17,61 +17,28 @@
import logging import logging
import threading import threading
import platform
from pubsub import pub from pubsub import pub
from pytap2 import TapDevice from pytap2 import TapDevice
from . import portnums_pb2 from meshtastic import portnums_pb2
from meshtastic.util import ipstr, readnet_u16
# A new non standard log level that is lower level than DEBUG from meshtastic.globals import Globals
LOG_TRACE = 5
# fixme - find a way to move onTunnelReceive inside of the class
tunnelInstance = None
"""A list of chatty UDP services we should never accidentally
forward to our slow network"""
udpBlacklist = {
1900, # SSDP
5353, # multicast DNS
}
"""A list of TCP services to block"""
tcpBlacklist = {}
"""A list of protocols we ignore"""
protocolBlacklist = {
0x02, # IGMP
0x80, # Service-Specific Connection-Oriented Protocol in a Multilink and Connectionless Environment
}
def hexstr(barray): def onTunnelReceive(packet, interface): # pylint: disable=W0613
"""Print a string of hex digits""" """Callback for received tunneled messages from mesh."""
return ":".join('{:02x}'.format(x) for x in barray) logging.debug(f'in onTunnelReceive()')
our_globals = Globals.getInstance()
tunnelInstance = our_globals.get_tunnelInstance()
def ipstr(barray):
"""Print a string of ip digits"""
return ".".join('{}'.format(x) for x in barray)
def readnet_u16(p, offset):
"""Read big endian u16 (network byte order)"""
return p[offset] * 256 + p[offset + 1]
def onTunnelReceive(packet, interface):
"""Callback for received tunneled messages from mesh
FIXME figure out how to do closures with methods in python"""
tunnelInstance.onReceive(packet) tunnelInstance.onReceive(packet)
class Tunnel: class Tunnel:
"""A TUN based IP tunnel over meshtastic""" """A TUN based IP tunnel over meshtastic"""
def __init__(self, iface, subnet=None, netmask="255.255.0.0"): def __init__(self, iface, subnet='10.115', netmask="255.255.0.0"):
""" """
Constructor Constructor
@@ -79,35 +46,69 @@ class Tunnel:
subnet is used to construct our network number (normally 10.115.x.x) subnet is used to construct our network number (normally 10.115.x.x)
""" """
if subnet is None: if not iface:
subnet = "10.115" raise Exception("Tunnel() must have a interface")
self.iface = iface self.iface = iface
self.subnetPrefix = subnet self.subnetPrefix = subnet
global tunnelInstance if platform.system() != 'Linux':
tunnelInstance = self raise Exception("Tunnel() can only be run instantiated on a Linux system")
our_globals = Globals.getInstance()
our_globals.set_tunnelInstance(self)
"""A list of chatty UDP services we should never accidentally
forward to our slow network"""
self.udpBlacklist = {
1900, # SSDP
5353, # multicast DNS
}
"""A list of TCP services to block"""
self.tcpBlacklist = {
5900, # VNC (Note: Only adding for testing purposes.)
}
"""A list of protocols we ignore"""
self.protocolBlacklist = {
0x02, # IGMP
0x80, # Service-Specific Connection-Oriented Protocol in a Multilink and Connectionless Environment
}
# A new non standard log level that is lower level than DEBUG
self.LOG_TRACE = 5
# TODO: check if root?
logging.info("Starting IP to mesh tunnel (you must be root for this *pre-alpha* "\ logging.info("Starting IP to mesh tunnel (you must be root for this *pre-alpha* "\
"feature to work). Mesh members:") "feature to work). Mesh members:")
pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP") pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP")
myAddr = self._nodeNumToIp(self.iface.myInfo.my_node_num) myAddr = self._nodeNumToIp(self.iface.myInfo.my_node_num)
for node in self.iface.nodes.values(): if self.iface.nodes:
nodeId = node["user"]["id"] for node in self.iface.nodes.values():
ip = self._nodeNumToIp(node["num"]) nodeId = node["user"]["id"]
logging.info(f"Node { nodeId } has IP address { ip }") ip = self._nodeNumToIp(node["num"])
logging.info(f"Node { nodeId } has IP address { ip }")
logging.debug("creating TUN device with MTU=200") logging.debug("creating TUN device with MTU=200")
# FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data # FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data
self.tun = TapDevice(name="mesh") self.tun = None
self.tun.up() if self.iface.noProto:
self.tun.ifconfig(address=myAddr, netmask=netmask, mtu=200) logging.warning(f"Not creating a TapDevice() because it is disabled by noProto")
logging.debug(f"starting TUN reader, our IP address is {myAddr}") else:
self._rxThread = threading.Thread( self.tun = TapDevice(name="mesh")
target=self.__tunReader, args=(), daemon=True) self.tun.up()
self._rxThread.start() self.tun.ifconfig(address=myAddr, netmask=netmask, mtu=200)
self._rxThread = None
if self.iface.noProto:
logging.warning(f"Not starting TUN reader because it is disabled by noProto")
else:
logging.debug(f"starting TUN reader, our IP address is {myAddr}")
self._rxThread = threading.Thread(target=self.__tunReader, args=(), daemon=True)
self._rxThread.start()
def onReceive(self, packet): def onReceive(self, packet):
"""onReceive""" """onReceive"""
@@ -115,12 +116,12 @@ class Tunnel:
if packet["from"] == self.iface.myInfo.my_node_num: if packet["from"] == self.iface.myInfo.my_node_num:
logging.debug("Ignoring message we sent") logging.debug("Ignoring message we sent")
else: else:
logging.debug( logging.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
f"Received mesh tunnel message type={type(p)} len={len(p)}")
# we don't really need to check for filtering here (sender should have checked), # we don't really need to check for filtering here (sender should have checked),
# but this provides useful debug printing on types of packets received # but this provides useful debug printing on types of packets received
if not self._shouldFilterPacket(p): if not self.iface.noProto:
self.tun.write(p) if not self._shouldFilterPacket(p):
self.tun.write(p)
def _shouldFilterPacket(self, p): def _shouldFilterPacket(self, p):
"""Given a packet, decode it and return true if it should be ignored""" """Given a packet, decode it and return true if it should be ignored"""
@@ -129,10 +130,9 @@ class Tunnel:
destAddr = p[16:20] destAddr = p[16:20]
subheader = 20 subheader = 20
ignore = False # Assume we will be forwarding the packet ignore = False # Assume we will be forwarding the packet
if protocol in protocolBlacklist: if protocol in self.protocolBlacklist:
ignore = True ignore = True
logging.log( logging.log(self.LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
elif protocol == 0x01: # ICMP elif protocol == 0x01: # ICMP
icmpType = p[20] icmpType = p[20]
icmpCode = p[21] icmpCode = p[21]
@@ -145,19 +145,17 @@ class Tunnel:
elif protocol == 0x11: # UDP elif protocol == 0x11: # UDP
srcport = readnet_u16(p, subheader) srcport = readnet_u16(p, subheader)
destport = readnet_u16(p, subheader + 2) destport = readnet_u16(p, subheader + 2)
if destport in udpBlacklist: if destport in self.udpBlacklist:
ignore = True ignore = True
logging.log( logging.log(self.LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
else: else:
logging.debug( logging.debug(f"forwarding udp srcport={srcport}, destport={destport}")
f"forwarding udp srcport={srcport}, destport={destport}")
elif protocol == 0x06: # TCP elif protocol == 0x06: # TCP
srcport = readnet_u16(p, subheader) srcport = readnet_u16(p, subheader)
destport = readnet_u16(p, subheader + 2) destport = readnet_u16(p, subheader + 2)
if destport in tcpBlacklist: if destport in self.tcpBlacklist:
ignore = True ignore = True
logging.log(LOG_TRACE, f"ignoring blacklisted TCP port {destport}") logging.log(self.LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
else: else:
logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}") logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
else: else:

View File

@@ -3,7 +3,9 @@
import traceback import traceback
from queue import Queue from queue import Queue
import os import os
import re
import sys import sys
import base64
import time import time
import platform import platform
import logging import logging
@@ -169,8 +171,7 @@ class DeferredExecution():
o = self.queue.get() o = self.queue.get()
o() o()
except: except:
logging.error( logging.error(f"Unexpected error in deferred execution {sys.exc_info()[0]}")
f"Unexpected error in deferred execution {sys.exc_info()[0]}")
print(traceback.format_exc()) print(traceback.format_exc())
@@ -189,15 +190,70 @@ def support_info():
print('or wish to make feature requests, visit:') print('or wish to make feature requests, visit:')
print('https://github.com/meshtastic/Meshtastic-python/issues') print('https://github.com/meshtastic/Meshtastic-python/issues')
print('When adding an issue, be sure to include the following info:') print('When adding an issue, be sure to include the following info:')
print(' System: {0}'.format(platform.system())) print(f' System: {platform.system()}')
print(' Platform: {0}'.format(platform.platform())) print(f' Platform: {platform.platform()}')
print(' Release: {0}'.format(platform.uname().release)) print(f' Release: {platform.uname().release}')
print(' Machine: {0}'.format(platform.uname().machine)) print(f' Machine: {platform.uname().machine}')
print(' Encoding (stdin): {0}'.format(sys.stdin.encoding)) print(f' Encoding (stdin): {sys.stdin.encoding}')
print(' Encoding (stdout): {0}'.format(sys.stdout.encoding)) print(f' Encoding (stdout): {sys.stdout.encoding}')
print(' meshtastic: v{0}'.format(pkg_resources.require('meshtastic')[0].version)) the_version = pkg_resources.get_distribution("meshtastic").version
print(' Executable: {0}'.format(sys.argv[0])) print(f' meshtastic: v{the_version}')
print(' Python: {0} {1} {2}'.format(platform.python_version(), print(f' Executable: {sys.argv[0]}')
platform.python_implementation(), platform.python_compiler())) print(f' Python: {platform.python_version()} {platform.python_implementation()} {platform.python_compiler()}')
print('') print('')
print('Please add the output from the command: meshtastic --info') print('Please add the output from the command: meshtastic --info')
def remove_keys_from_dict(keys, adict):
"""Return a dictionary without some keys in it.
Will removed nested keys.
"""
for key in keys:
try:
del adict[key]
except:
pass
for val in adict.values():
if isinstance(val, dict):
remove_keys_from_dict(keys, val)
return adict
def hexstr(barray):
"""Print a string of hex digits"""
return ":".join(f'{x:02x}' for x in barray)
def ipstr(barray):
"""Print a string of ip digits"""
return ".".join(f'{x}' for x in barray)
def readnet_u16(p, offset):
"""Read big endian u16 (network byte order)"""
return p[offset] * 256 + p[offset + 1]
def convert_mac_addr(val):
"""Convert the base 64 encoded value to a mac address
val - base64 encoded value (ex: '/c0gFyhb'))
returns: a string formatted like a mac address (ex: 'fd:cd:20:17:28:5b')
"""
if not re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", val):
val_as_bytes = base64.b64decode(val)
return hexstr(val_as_bytes)
return val
def snake_to_camel(a_string):
"""convert snake_case to camelCase"""
# split underscore using split
temp = a_string.split('_')
# joining result
result = temp[0] + ''.join(ele.title() for ele in temp[1:])
return result
def camel_to_snake(a_string):
"""convert camelCase to snake_case"""
return ''.join(['_'+i.lower() if i.isupper() else i for i in a_string]).lstrip('_')

2
proto

Submodule proto updated: 1d3b4806ab...d7b2791b7c

View File

@@ -1,11 +1,15 @@
[pytest] [pytest]
addopts = -m "not int and not smoke1 and not smoke2 and not smokewifi and not examples" addopts = -m "not int and not smoke1 and not smoke2 and not smokewifi and not examples and not smokevirt"
filterwarnings =
ignore::DeprecationWarning
markers = markers =
unit: marks tests as unit tests unit: marks tests as unit tests
unitslow: marks slow unit tests unitslow: marks slow unit tests
int: marks tests as integration tests int: marks tests as integration tests
smokevirt: marks tests as smoke tests against virtual device
smoke1: runs smoke tests on a single device connected via USB smoke1: runs smoke tests on a single device connected via USB
smoke2: runs smoke tests on a two devices connected via USB smoke2: runs smoke tests on a two devices connected via USB
smokewifi: runs smoke test on an esp32 device setup with wifi smokewifi: runs smoke test on an esp32 device setup with wifi

View File

@@ -4,7 +4,6 @@ protobuf
dotmap dotmap
pexpect pexpect
pyqrcode pyqrcode
pygatt
tabulate tabulate
timeago timeago
webencodings webencodings
@@ -18,3 +17,4 @@ pyyaml
pytap2 pytap2
pdoc3 pdoc3
pypubsub pypubsub
pygatt; platform_system == "Linux"

View File

@@ -12,7 +12,7 @@ with open("README.md", "r") as fh:
# This call to setup() does all the work # This call to setup() does all the work
setup( setup(
name="meshtastic", name="meshtastic",
version="1.2.46", version="1.2.55",
description="Python API & client shell for talking to Meshtastic devices", description="Python API & client shell for talking to Meshtastic devices",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@@ -23,13 +23,18 @@ setup(
classifiers=[ classifiers=[
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
], ],
packages=["meshtastic"], packages=["meshtastic"],
include_package_data=True, include_package_data=True,
install_requires=["pyserial>=3.4", "protobuf>=3.13.0", install_requires=["pyserial>=3.4", "protobuf>=3.13.0",
"pypubsub>=4.0.3", "dotmap>=1.3.14", "pexpect>=4.6.0", "pyqrcode>=1.2.1", "pypubsub>=4.0.3", "dotmap>=1.3.14", "pexpect>=4.6.0", "pyqrcode>=1.2.1",
"pygatt>=4.0.5", "tabulate>=0.8.9", "timeago>=1.0.15", "pyyaml"], "tabulate>=0.8.9", "timeago>=1.0.15", "pyyaml",
"pygatt>=4.0.5 ; platform_system=='Linux'"],
extras_require={ extras_require={
'tunnel': ["pytap2>=2.0.0"] 'tunnel': ["pytap2>=2.0.0"]
}, },

5
vercel.json Normal file
View File

@@ -0,0 +1,5 @@
{
"github": {
"silent": true
}
}