mirror of
https://github.com/meshtastic/python.git
synced 2026-07-31 17:16:45 -04:00
Compare commits
260 Commits
revert-753
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f2ce3dfc2 | ||
|
|
307d8d81e7 | ||
|
|
405a6bbc5d | ||
|
|
db746a0981 | ||
|
|
13b8cdcb04 | ||
|
|
9d445098f4 | ||
|
|
8c84074c1d | ||
|
|
1cffae6add | ||
|
|
e4f0fb222b | ||
|
|
6eeb90066c | ||
|
|
e216848c91 | ||
|
|
dd1df473c7 | ||
|
|
953d01206b | ||
|
|
c71d3df332 | ||
|
|
0413d00825 | ||
|
|
8f0faf5c3f | ||
|
|
ab656d54a8 | ||
|
|
c575547fae | ||
|
|
0bef1370d7 | ||
|
|
f9a1357816 | ||
|
|
d1f3552f64 | ||
|
|
81ae8b6c87 | ||
|
|
9c1573f1b3 | ||
|
|
ff26f9769d | ||
|
|
e5449be38e | ||
|
|
e350c238c5 | ||
|
|
2fca36b3fe | ||
|
|
b16441378e | ||
|
|
6037944215 | ||
|
|
6909d3d21f | ||
|
|
dbf4cabee1 | ||
|
|
435e53eae2 | ||
|
|
12509bef30 | ||
|
|
02485a88fb | ||
|
|
4d3553b955 | ||
|
|
95bd3ea7af | ||
|
|
2c443c082d | ||
|
|
ce5bbcda68 | ||
|
|
c2a01f8294 | ||
|
|
14f53398d7 | ||
|
|
4d8091a7e3 | ||
|
|
15ce4968db | ||
|
|
9c0b253103 | ||
|
|
c7f43b3100 | ||
|
|
8c7eb118fc | ||
|
|
badf7279af | ||
|
|
13c9f34589 | ||
|
|
781764553e | ||
|
|
f5e7faea3b | ||
|
|
e86a495b50 | ||
|
|
5dde67ef82 | ||
|
|
a7d13eb2c1 | ||
|
|
3916009dbd | ||
|
|
280323da8b | ||
|
|
89d81c9c7a | ||
|
|
fddaad316a | ||
|
|
bfe38ac0c7 | ||
|
|
c5fc51ea37 | ||
|
|
cec79a7c1f | ||
|
|
6805fee667 | ||
|
|
3cb9ce2a56 | ||
|
|
b05a6a6017 | ||
|
|
8a842ed82b | ||
|
|
eb964d78bb | ||
|
|
80c40ef893 | ||
|
|
2494bb4c63 | ||
|
|
1e4822fe87 | ||
|
|
c7ee644ad2 | ||
|
|
9af5f22837 | ||
|
|
1d3bdf143a | ||
|
|
7129a9f963 | ||
|
|
d8e438b225 | ||
|
|
6511d06e2f | ||
|
|
94c531e7ee | ||
|
|
ad4f3f557e | ||
|
|
1c9cf37974 | ||
|
|
cd9199bc00 | ||
|
|
04a23ae882 | ||
|
|
b003214d1b | ||
|
|
414a621091 | ||
|
|
a87065a081 | ||
|
|
942ce115f3 | ||
|
|
d5eaecea07 | ||
|
|
545c3ab192 | ||
|
|
fee2b6ccbd | ||
|
|
d0ccb1a597 | ||
|
|
5721859314 | ||
|
|
07172f88f3 | ||
|
|
3ea1994cf5 | ||
|
|
4de19f50d9 | ||
|
|
4d8430d360 | ||
|
|
bf580c36ae | ||
|
|
1eb13c9953 | ||
|
|
1fc2407c5e | ||
|
|
25ffd25fda | ||
|
|
cdf893e618 | ||
|
|
9b9df9e585 | ||
|
|
b26d80f186 | ||
|
|
da559f0e37 | ||
|
|
a73f432f42 | ||
|
|
c3c5ce64dd | ||
|
|
683dd23d63 | ||
|
|
57d43b84e4 | ||
|
|
4f6d183ed1 | ||
|
|
9a72e36ca6 | ||
|
|
4d54ee7431 | ||
|
|
c8b1b8ea6f | ||
|
|
d9057c0aaf | ||
|
|
77db30125a | ||
|
|
5cc0dae394 | ||
|
|
1a50377d34 | ||
|
|
2a44be9269 | ||
|
|
776debcc86 | ||
|
|
1088cc607e | ||
|
|
aeec5447ed | ||
|
|
096fec95c8 | ||
|
|
dea5f788a2 | ||
|
|
f15a0bdc0b | ||
|
|
1214d5010b | ||
|
|
0906fc6bc0 | ||
|
|
ccb530574b | ||
|
|
76418b8e57 | ||
|
|
3be73b42e2 | ||
|
|
2245ac8d97 | ||
|
|
040f332078 | ||
|
|
f3f17a7d50 | ||
|
|
79334e83e6 | ||
|
|
dbc0101a7a | ||
|
|
debbb8caeb | ||
|
|
3c6dba78a3 | ||
|
|
8a3b114153 | ||
|
|
0bb04100e1 | ||
|
|
ad04c26d13 | ||
|
|
0e67ef37aa | ||
|
|
e924afd140 | ||
|
|
87682c153b | ||
|
|
e6c276fe96 | ||
|
|
b4662251ed | ||
|
|
a17cfe9d2b | ||
|
|
471e3ce145 | ||
|
|
39c9864682 | ||
|
|
1d3a7d39f7 | ||
|
|
2065598754 | ||
|
|
4cac15686b | ||
|
|
9285f7d13f | ||
|
|
f6f1b748dc | ||
|
|
d2d9c03bc8 | ||
|
|
c44f9b1bb4 | ||
|
|
6f35eb3923 | ||
|
|
dcd077d85e | ||
|
|
da416fcd20 | ||
|
|
93da1da386 | ||
|
|
2de7c30a27 | ||
|
|
43a685f012 | ||
|
|
7554c03a26 | ||
|
|
38a13f300f | ||
|
|
eef8a37703 | ||
|
|
63b940defb | ||
|
|
e5efa94264 | ||
|
|
efa841b08c | ||
|
|
6ff13eb95c | ||
|
|
49783d9108 | ||
|
|
3615135e97 | ||
|
|
cfb23788e1 | ||
|
|
14ff3eb9c4 | ||
|
|
47e5b04d3b | ||
|
|
e5159f1156 | ||
|
|
593b05dbcd | ||
|
|
f519d1f2d2 | ||
|
|
8b36561406 | ||
|
|
e2b4948d45 | ||
|
|
7e3d347b63 | ||
|
|
c6efccdbd2 | ||
|
|
fe093ac34b | ||
|
|
2b10459db0 | ||
|
|
dd238dcbe3 | ||
|
|
e330afc899 | ||
|
|
a63f3f6e94 | ||
|
|
52eb112b95 | ||
|
|
d53ced216c | ||
|
|
f5ecd28705 | ||
|
|
82ad9b2f51 | ||
|
|
cbf7b9befe | ||
|
|
f3ba660cf4 | ||
|
|
4b143030d3 | ||
|
|
03aaa4c98e | ||
|
|
a79e17a575 | ||
|
|
38b163fa89 | ||
|
|
af4947d020 | ||
|
|
db1891b651 | ||
|
|
d7d9c7219a | ||
|
|
c60b5d4b05 | ||
|
|
83d82c518a | ||
|
|
8a95ce4636 | ||
|
|
0261313fc5 | ||
|
|
c1a6234694 | ||
|
|
66e32f812a | ||
|
|
eb85439000 | ||
|
|
885eb4898d | ||
|
|
172c123990 | ||
|
|
fcdd83838b | ||
|
|
58967e1d91 | ||
|
|
17f7e8e20e | ||
|
|
9b5a889676 | ||
|
|
ce7c61861f | ||
|
|
4adcbb6787 | ||
|
|
125f63419e | ||
|
|
cad5d18aff | ||
|
|
706d0649c1 | ||
|
|
167044907d | ||
|
|
ab1669994f | ||
|
|
6f67f33378 | ||
|
|
e60c8ea105 | ||
|
|
d633f8c895 | ||
|
|
ca82e1ce2b | ||
|
|
0ae23eec7e | ||
|
|
2fa85bac1f | ||
|
|
58fc614fb7 | ||
|
|
795b652069 | ||
|
|
213faa0cae | ||
|
|
68a2009e0e | ||
|
|
c76e4dac87 | ||
|
|
428be9fbce | ||
|
|
d83f7b2307 | ||
|
|
eb453a2e8a | ||
|
|
308ac93399 | ||
|
|
84417f0bb1 | ||
|
|
0bb3389b3b | ||
|
|
22b3062151 | ||
|
|
373b8a3139 | ||
|
|
db21942244 | ||
|
|
c55f1ef610 | ||
|
|
51b543ff40 | ||
|
|
8752a0de6e | ||
|
|
7160e79fbf | ||
|
|
b73fcbff88 | ||
|
|
1b5b07e752 | ||
|
|
ab997aac84 | ||
|
|
a097161dbc | ||
|
|
e6750507c8 | ||
|
|
0deb98b4c6 | ||
|
|
04a0ff6322 | ||
|
|
b4764d3bc3 | ||
|
|
9281c4a335 | ||
|
|
3c2dd6f4ff | ||
|
|
8e48d141c8 | ||
|
|
8a6ee5fb35 | ||
|
|
aa786c7ebd | ||
|
|
23be2d2189 | ||
|
|
622a435465 | ||
|
|
56680f8da6 | ||
|
|
321a960c13 | ||
|
|
4668852b0b | ||
|
|
c3973117c8 | ||
|
|
d456e4ce30 | ||
|
|
ec78f62992 | ||
|
|
f5fa30cb22 | ||
|
|
46a8db286c | ||
|
|
acff793f28 | ||
|
|
53b58500f7 |
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
*
|
||||
!Containerfile*
|
||||
!Dockerfile
|
||||
!README.md
|
||||
!LICENSE.md
|
||||
!bin/container-entrypoint.sh
|
||||
!examples/
|
||||
!extra/
|
||||
!meshtastic/
|
||||
!poetry.lock
|
||||
!pyproject.toml
|
||||
22
.github/CONTRIBUTING.md
vendored
Normal file
22
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Contributing to Meshtastic Python
|
||||
|
||||
## Development resources
|
||||
- [API Documentation](https://python.meshtastic.org/)
|
||||
- [Meshtastic Python Development](https://meshtastic.org/docs/development/python/)
|
||||
- [Building Meshtastic Python](https://meshtastic.org/docs/development/python/building/)
|
||||
- [Using the Meshtastic Python Library](https://meshtastic.org/docs/development/python/library/)
|
||||
|
||||
## How to check your code (pytest/pylint) before a PR
|
||||
- [Pre-requisites](https://meshtastic.org/docs/development/python/building/#pre-requisites)
|
||||
- also execute `poetry install --all-extras --with dev,powermon` for all optional dependencies
|
||||
- check your code with github ci actions locally
|
||||
- You need to have act installed. You can get it at https://nektosact.com/
|
||||
- on linux: `act -P ubuntu-latest=-self-hosted --matrix "python-version:3.12"`
|
||||
- on windows:
|
||||
- linux checks (linux docker): `act --matrix "python-version:3.12"`
|
||||
- windows checks (windows host): `act -P ubuntu-latest=-self-hosted --matrix "python-version:3.12"`
|
||||
- or run all locally:
|
||||
- run `poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"`
|
||||
- run `poetry run mypy meshtastic/`
|
||||
- run `poetry run pytest`
|
||||
- more commands see [CI workflow](https://github.com/meshtastic/python/blob/master/.github/workflows/ci.yml)
|
||||
206
.github/copilot-instructions.md
vendored
Normal file
206
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
# Copilot Instructions for Meshtastic Python
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is the Meshtastic Python library and CLI - a Python API for interacting with Meshtastic mesh radio devices. It supports communication via Serial, TCP, and BLE interfaces.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Language**: Python 3.9 - 3.14
|
||||
- **Package Manager**: Poetry
|
||||
- **Testing**: pytest with hypothesis for property-based testing
|
||||
- **Linting**: pylint
|
||||
- **Type Checking**: mypy (working toward strict mode)
|
||||
- **Documentation**: pdoc3
|
||||
- **License**: GPL-3.0
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
meshtastic/ # Main library package
|
||||
├── __init__.py # Core interface classes and pub/sub topics
|
||||
├── __main__.py # CLI entry point
|
||||
├── mesh_interface.py # Base interface class for all connection types
|
||||
├── serial_interface.py
|
||||
├── tcp_interface.py
|
||||
├── ble_interface.py
|
||||
├── node.py # Node representation and configuration
|
||||
├── protobuf/ # Generated Protocol Buffer files (*_pb2.py, *_pb2.pyi)
|
||||
├── tests/ # Unit and integration tests
|
||||
├── powermon/ # Power monitoring tools
|
||||
└── analysis/ # Data analysis tools
|
||||
examples/ # Usage examples
|
||||
protobufs/ # Protocol Buffer source definitions
|
||||
```
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Style Guidelines
|
||||
|
||||
- Follow PEP 8 style conventions
|
||||
- Use type hints for function parameters and return values
|
||||
- Document public functions and classes with docstrings
|
||||
- Prefer explicit imports over wildcard imports
|
||||
- Use `logging` module instead of print statements for debug output
|
||||
|
||||
### Type Annotations
|
||||
|
||||
- Add type hints to all new code
|
||||
- Use `Optional[T]` for nullable types
|
||||
- Use `Dict`, `List`, `Tuple` from `typing` module for Python 3.9 compatibility
|
||||
- Protobuf types are in `meshtastic.protobuf.*_pb2` modules
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- Classes: `PascalCase` (e.g., `MeshInterface`, `SerialInterface`)
|
||||
- Functions/methods: `camelCase` for public API (e.g., `sendText`, `sendData`)
|
||||
- Internal functions: `snake_case` with leading underscore (e.g., `_send_packet`)
|
||||
- Constants: `UPPER_SNAKE_CASE` (e.g., `BROADCAST_ADDR`, `LOCAL_ADDR`)
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use custom exception classes when appropriate (e.g., `MeshInterface.MeshInterfaceError`)
|
||||
- Provide meaningful error messages
|
||||
- Use `our_exit()` from `meshtastic.util` for CLI exits with error codes
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Organization
|
||||
|
||||
Tests are in `meshtastic/tests/` and use pytest markers:
|
||||
|
||||
- `@pytest.mark.unit` - Fast unit tests (default)
|
||||
- `@pytest.mark.unitslow` - Slower unit tests
|
||||
- `@pytest.mark.int` - Integration tests
|
||||
- `@pytest.mark.smoke1` - Single device smoke tests
|
||||
- `@pytest.mark.smoke2` - Two device smoke tests
|
||||
- `@pytest.mark.smokevirt` - Virtual device smoke tests
|
||||
- `@pytest.mark.examples` - Example validation tests
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run unit tests only (default)
|
||||
make test
|
||||
# or
|
||||
pytest -m unit
|
||||
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run with coverage
|
||||
make cov
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
- Use `pytest` fixtures from `conftest.py`
|
||||
- Use `hypothesis` for property-based testing where appropriate
|
||||
- Mock external dependencies (serial ports, network connections)
|
||||
- Test file naming: `test_<module_name>.py`
|
||||
|
||||
## Pub/Sub Events
|
||||
|
||||
The library uses pypubsub for event handling. Key topics:
|
||||
|
||||
- `meshtastic.connection.established` - Connection successful
|
||||
- `meshtastic.connection.lost` - Connection lost
|
||||
- `meshtastic.receive.text(packet)` - Text message received
|
||||
- `meshtastic.receive.position(packet)` - Position update received
|
||||
- `meshtastic.receive.data.portnum(packet)` - Data packet by port number
|
||||
- `meshtastic.node.updated(node)` - Node database changed
|
||||
- `meshtastic.log.line(line)` - Raw log line from device
|
||||
|
||||
## Protocol Buffers
|
||||
|
||||
- Protobuf definitions are in `protobufs/meshtastic/`
|
||||
- Generated Python files are in `meshtastic/protobuf/`
|
||||
- Never edit `*_pb2.py` or `*_pb2.pyi` files directly
|
||||
- Regenerate with: `make protobufs` or `./bin/regen-protobufs.sh`
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating an Interface
|
||||
|
||||
```python
|
||||
import meshtastic.serial_interface
|
||||
|
||||
# Auto-detect device
|
||||
iface = meshtastic.serial_interface.SerialInterface()
|
||||
|
||||
# Specific device
|
||||
iface = meshtastic.serial_interface.SerialInterface(devPath="/dev/ttyUSB0")
|
||||
|
||||
# Always close when done
|
||||
iface.close()
|
||||
|
||||
# Or use context manager
|
||||
with meshtastic.serial_interface.SerialInterface() as iface:
|
||||
iface.sendText("Hello mesh")
|
||||
```
|
||||
|
||||
### Sending Messages
|
||||
|
||||
```python
|
||||
# Text message (broadcast)
|
||||
iface.sendText("Hello")
|
||||
|
||||
# Text message to specific node
|
||||
iface.sendText("Hello", destinationId="!abcd1234")
|
||||
|
||||
# Binary data
|
||||
iface.sendData(data, portNum=portnums_pb2.PRIVATE_APP)
|
||||
```
|
||||
|
||||
### Subscribing to Events
|
||||
|
||||
```python
|
||||
from pubsub import pub
|
||||
|
||||
def on_receive(packet, interface):
|
||||
print(f"Received: {packet}")
|
||||
|
||||
pub.subscribe(on_receive, "meshtastic.receive")
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Install dependencies: `poetry install --all-extras --with dev`
|
||||
2. Make changes
|
||||
3. Run linting: `poetry run pylint meshtastic examples/`
|
||||
4. Run type checking: `poetry run mypy meshtastic/`
|
||||
5. Run tests: `poetry run pytest -m unit`
|
||||
6. Update documentation if needed
|
||||
|
||||
## CLI Development
|
||||
|
||||
The CLI is in `meshtastic/__main__.py`. When adding new CLI commands:
|
||||
|
||||
- Use argparse for argument parsing
|
||||
- Support `--dest` for specifying target node
|
||||
- Provide `--help` documentation
|
||||
- Handle errors gracefully with meaningful messages
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Required
|
||||
- `pyserial` - Serial port communication
|
||||
- `protobuf` - Protocol Buffers
|
||||
- `pypubsub` - Pub/sub messaging
|
||||
- `bleak` - BLE communication
|
||||
- `tabulate` - Table formatting
|
||||
- `pyyaml` - YAML config support
|
||||
- `requests` - HTTP requests
|
||||
|
||||
### Optional (extras)
|
||||
- `cli` extra: `pyqrcode`, `print-color`, `dotmap`, `argcomplete`
|
||||
- `tunnel` extra: `pytap2`
|
||||
- `analysis` extra: `dash`, `pandas`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Always test with actual Meshtastic hardware when possible
|
||||
- Be mindful of radio regulations in your region
|
||||
- The nodedb (`interface.nodes`) is read-only
|
||||
- Packet IDs are random 32-bit integers
|
||||
- Default timeout is 300 seconds for operations
|
||||
79
.github/workflows/container-build.yaml
vendored
Normal file
79
.github/workflows/container-build.yaml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Create and publish Container image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*.*.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- container: Containerfile.debian
|
||||
autotag: false
|
||||
suffix: -debian
|
||||
- container: Containerfile.alpine
|
||||
autotag: ${{ github.ref == 'refs/heads/master' && 'true' || 'auto' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Container registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=edge
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
flavor: |
|
||||
latest=${{ matrix.autotag }}
|
||||
suffix=${{ matrix.suffix }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: ${{ matrix.container }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
33
.github/workflows/update_protobufs.yml
vendored
33
.github/workflows/update_protobufs.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: "Update protobufs"
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-protobufs:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -9,23 +12,34 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Update Submodule
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
git pull --recurse-submodules
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install poetry
|
||||
|
||||
- name: Update protobuf submodule
|
||||
run: |
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --recursive
|
||||
git submodule update --remote --recursive
|
||||
|
||||
- name: Download nanopb
|
||||
run: |
|
||||
wget https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
|
||||
curl -L -o nanopb-0.4.8-linux-x86.tar.gz https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.8-linux-x86.tar.gz
|
||||
tar xvzf nanopb-0.4.8-linux-x86.tar.gz
|
||||
mv nanopb-0.4.8-linux-x86 nanopb-0.4.8
|
||||
|
||||
- name: Install poetry (needed by regen-protobufs.sh)
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install poetry
|
||||
poetry install --with dev
|
||||
|
||||
- name: Re-generate protocol buffers
|
||||
run: |
|
||||
@@ -38,4 +52,9 @@ jobs:
|
||||
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
|
||||
git add protobufs
|
||||
git add meshtastic/protobuf
|
||||
git commit -m "Update protobuf submodule" && git push || echo "No changes to commit"
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
git commit -m "Update protobufs"
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,4 +17,5 @@ examples/__pycache__
|
||||
meshtastic.spec
|
||||
.hypothesis/
|
||||
coverage.xml
|
||||
.ipynb_checkpoints
|
||||
.ipynb_checkpoints
|
||||
.cursor/
|
||||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@@ -4,6 +4,7 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "meshtastic BLE",
|
||||
"type": "debugpy",
|
||||
@@ -261,7 +262,14 @@
|
||||
"module": "meshtastic",
|
||||
"justMyCode": true,
|
||||
"args": ["--nodes", "--show-fields", "AKA,Pubkey,Role,Role,Role,Latitude,Latitude,deviceMetrics.voltage"]
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"name": "meshtastic --export-config",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "meshtastic",
|
||||
"justMyCode": true,
|
||||
"args": ["--export-config", "config.json"]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
1
Containerfile
Symbolic link
1
Containerfile
Symbolic link
@@ -0,0 +1 @@
|
||||
Containerfile.alpine
|
||||
40
Containerfile.alpine
Normal file
40
Containerfile.alpine
Normal file
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
|
||||
|
||||
ARG TARGET_VERSION="3.11-alpine"
|
||||
ARG TARGET_ARCH="library"
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY pyproject.toml poetry.lock /tmp/build/
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps build-base libffi-dev && \
|
||||
pip install --no-cache-dir 'poetry==2.4.1' && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
|
||||
|
||||
COPY . /tmp/build/
|
||||
|
||||
RUN poetry build --format wheel --no-interaction
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
|
||||
|
||||
RUN apk add --no-cache libffi && \
|
||||
addgroup -S meshtastic && \
|
||||
adduser -S -G meshtastic -h /home/meshtastic meshtastic
|
||||
|
||||
COPY --from=builder /tmp/build/dist/*.whl /tmp/
|
||||
|
||||
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
|
||||
rm -f /tmp/meshtastic-*.whl
|
||||
|
||||
COPY "./bin/container-entrypoint.sh" "/init"
|
||||
RUN chmod 0755 /init
|
||||
|
||||
WORKDIR /home/meshtastic
|
||||
USER meshtastic
|
||||
|
||||
ENTRYPOINT [ "/init" ]
|
||||
37
Containerfile.debian
Normal file
37
Containerfile.debian
Normal file
@@ -0,0 +1,37 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
|
||||
|
||||
ARG TARGET_VERSION="3.11"
|
||||
ARG TARGET_ARCH="library"
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY pyproject.toml poetry.lock /tmp/build/
|
||||
|
||||
RUN pip install --no-cache-dir 'poetry==2.4.1' && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
|
||||
|
||||
COPY . /tmp/build/
|
||||
|
||||
RUN poetry build --format wheel --no-interaction
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
|
||||
|
||||
RUN useradd --system --create-home --home-dir /home/meshtastic meshtastic
|
||||
|
||||
COPY --from=builder /tmp/build/dist/*.whl /tmp/
|
||||
|
||||
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
|
||||
rm -f /tmp/meshtastic-*.whl
|
||||
|
||||
COPY "./bin/container-entrypoint.sh" "/init"
|
||||
RUN chmod 0755 /init
|
||||
|
||||
WORKDIR /home/meshtastic
|
||||
USER meshtastic
|
||||
|
||||
ENTRYPOINT [ "/init" ]
|
||||
1
Dockerfile
Symbolic link
1
Dockerfile
Symbolic link
@@ -0,0 +1 @@
|
||||
Containerfile
|
||||
18
README.md
18
README.md
@@ -27,6 +27,24 @@ This small library (and example application) provides an easy API for sending an
|
||||
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.
|
||||
|
||||
## Container usage
|
||||
|
||||
Container images are published to GHCR for this repository. The container entrypoint defaults to running `meshtastic`,
|
||||
so CLI flags can be passed directly:
|
||||
|
||||
```bash
|
||||
docker run --rm ghcr.io/meshtastic/python --help
|
||||
```
|
||||
|
||||
To run another command, pass it explicitly (for example, a shell):
|
||||
|
||||
```bash
|
||||
docker run --rm -it --entrypoint /bin/sh ghcr.io/meshtastic/python
|
||||
```
|
||||
|
||||
The container runs as a non-root user by default. When talking to local hardware, pass the serial device through
|
||||
explicitly (for example `--device /dev/ttyUSB0:/dev/ttyUSB0`) and ensure host device permissions allow access.
|
||||
|
||||
## Call for Contributors
|
||||
|
||||
This library and CLI has gone without a consistent maintainer for a while, and there's many improvements that could be made. We're all volunteers here and help is extremely appreciated, whether in implementing your own needs or helping maintain the library and CLI in general.
|
||||
|
||||
24
bin/container-entrypoint.sh
Executable file
24
bin/container-entrypoint.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
|
||||
#
|
||||
# A beginning user should be able to docker run image bash (or sh) without
|
||||
# needing to learn about --entrypoint
|
||||
# https://github.com/docker-library/official-images#consistency
|
||||
|
||||
set -eu
|
||||
|
||||
bin='meshtastic'
|
||||
|
||||
# run command if it is not starting with a "-" and is an executable in PATH
|
||||
if [ "${#}" -le 0 ] || \
|
||||
[ "${1#-}" != "${1}" ] || \
|
||||
[ -d "${1}" ] || \
|
||||
! command -v "${1}" > '/dev/null' 2>&1; then
|
||||
entrypoint='true'
|
||||
fi
|
||||
|
||||
exec ${entrypoint:+${bin:?}} "${@}"
|
||||
|
||||
exit 0
|
||||
332
bin/inject_nanopb_options.py
Normal file
332
bin/inject_nanopb_options.py
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inject nanopb .options constraints as inline field options into a .proto file.
|
||||
|
||||
The nanopb .options file format is specific to the nanopb C generator and is
|
||||
ignored by standard protoc (including --python_out). By injecting the options
|
||||
directly into the proto file's field declarations, protoc will embed them in
|
||||
the serialized descriptor, making them accessible in Python via:
|
||||
|
||||
from meshtastic.protobuf import mesh_pb2, nanopb_pb2
|
||||
field = mesh_pb2.DESCRIPTOR.message_types_by_name['User'].fields_by_name['long_name']
|
||||
opts = field.GetOptions().Extensions[nanopb_pb2.nanopb]
|
||||
print(opts.max_size) # 40
|
||||
|
||||
Usage:
|
||||
inject_nanopb_options.py <options_file> <proto_file>
|
||||
|
||||
The proto_file is modified in-place. Intended to operate on temporary copies
|
||||
generated by regen-protobufs.sh, not the source .proto files.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
# IntSize enum values from nanopb.proto
|
||||
INT_SIZE_ENUM = {8: "IS_8", 16: "IS_16", 32: "IS_32", 64: "IS_64"}
|
||||
|
||||
# Options that are valid proto FieldOptions and useful outside of C code generation.
|
||||
# We skip C-only options (anonymous_oneof, no_unions, skip_message, packed_struct,
|
||||
# packed_enum, mangle_names, callback_datatype, callback_function, descriptorsize,
|
||||
# type_override) since they either don't apply to proto fields or are C-specific.
|
||||
FIELD_OPTIONS = frozenset(
|
||||
{
|
||||
"max_size",
|
||||
"max_length",
|
||||
"max_count",
|
||||
"int_size",
|
||||
"fixed_length",
|
||||
"fixed_count",
|
||||
"long_names",
|
||||
"proto3",
|
||||
"default_has",
|
||||
"sort_by_tag",
|
||||
"msgid",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def parse_value(s: str) -> Any:
|
||||
"""Convert an option value string to an appropriate Python type."""
|
||||
if re.fullmatch(r"-?[0-9]+", s):
|
||||
return int(s)
|
||||
if s.lower() == "true":
|
||||
return True
|
||||
if s.lower() == "false":
|
||||
return False
|
||||
return s
|
||||
|
||||
|
||||
def parse_options_file(
|
||||
path: Path,
|
||||
) -> Tuple[Dict[Tuple[str, ...], Dict[str, Any]], Dict[str, Dict[str, Any]]]:
|
||||
"""Parse a nanopb .options file.
|
||||
|
||||
Returns:
|
||||
specific: maps (message_path..., field_name) -> {option: value}
|
||||
e.g. ('User', 'long_name') or ('Route', 'Link', 'uid')
|
||||
wildcard: maps field_name -> {option: value}
|
||||
applies to any field with this name in the same proto file
|
||||
"""
|
||||
specific: Dict[Tuple[str, ...], Dict[str, Any]] = {}
|
||||
wildcard: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
# Strip inline comments
|
||||
comment_pos = line.find("#")
|
||||
if comment_pos >= 0:
|
||||
line = line[:comment_pos]
|
||||
line = line.strip().lstrip("*").strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
tokens = line.split()
|
||||
if len(tokens) < 2:
|
||||
continue
|
||||
|
||||
pattern = tokens[0]
|
||||
opts: Dict[str, Any] = {}
|
||||
for tok in tokens[1:]:
|
||||
if ":" in tok:
|
||||
k, v = tok.split(":", 1)
|
||||
if k in FIELD_OPTIONS:
|
||||
opts[k] = parse_value(v)
|
||||
|
||||
if not opts:
|
||||
continue
|
||||
|
||||
if "." in pattern:
|
||||
# e.g. "User.long_name" -> key=('User', 'long_name')
|
||||
# or "Route.Link.uid" -> key=('Route', 'Link', 'uid')
|
||||
parts = tuple(pattern.split("."))
|
||||
if parts in specific:
|
||||
specific[parts].update(opts)
|
||||
else:
|
||||
specific[parts] = opts
|
||||
else:
|
||||
# wildcard: applies to any field with this name
|
||||
if pattern in wildcard:
|
||||
wildcard[pattern].update(opts)
|
||||
else:
|
||||
wildcard[pattern] = opts
|
||||
|
||||
return specific, wildcard
|
||||
|
||||
|
||||
def format_nanopb_opts(opts: Dict[str, Any]) -> str:
|
||||
"""Format a dict of nanopb options as a proto field options annotation string."""
|
||||
parts = []
|
||||
for k, v in opts.items():
|
||||
if k == "int_size":
|
||||
enum_val = INT_SIZE_ENUM.get(v, f"IS_{v}")
|
||||
parts.append(f"(nanopb).int_size = {enum_val}")
|
||||
elif isinstance(v, bool):
|
||||
parts.append(f"(nanopb).{k} = {'true' if v else 'false'}")
|
||||
else:
|
||||
parts.append(f"(nanopb).{k} = {v}")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def message_path_matches(
|
||||
context_stack: List[Tuple[str, str]], msg_path: Tuple[str, ...]
|
||||
) -> bool:
|
||||
"""Check if the current message context ends with msg_path.
|
||||
|
||||
context_stack entries are ('message'|'oneof'|'enum', name).
|
||||
msg_path is the tuple of message names from the options pattern,
|
||||
e.g. ('User',) or ('Route', 'Link').
|
||||
"""
|
||||
msg_names = [name for kind, name in context_stack if kind == "message"]
|
||||
n = len(msg_path)
|
||||
return len(msg_names) >= n and tuple(msg_names[-n:]) == msg_path
|
||||
|
||||
|
||||
def inject_into_proto(
|
||||
content: str,
|
||||
specific: Dict[Tuple[str, ...], Dict[str, Any]],
|
||||
wildcard: Dict[str, Dict[str, Any]],
|
||||
nanopb_import_path: str,
|
||||
) -> str:
|
||||
"""Inject nanopb field options into a proto file's text content.
|
||||
|
||||
Adds an import for nanopb.proto if not already present.
|
||||
Returns the modified content.
|
||||
"""
|
||||
if not specific and not wildcard:
|
||||
return content
|
||||
|
||||
lines = content.split("\n")
|
||||
|
||||
# Check if nanopb is already imported (after sed fixup, it will be
|
||||
# 'meshtastic/protobuf/nanopb.proto')
|
||||
nanopb_already_imported = any(
|
||||
"nanopb.proto" in line
|
||||
for line in lines
|
||||
if line.strip().startswith("import")
|
||||
)
|
||||
|
||||
# Track the index of the last import line so we can insert after it
|
||||
last_import_idx = max(
|
||||
(
|
||||
i
|
||||
for i, line in enumerate(lines)
|
||||
if line.strip().startswith("import ") and line.strip().endswith(";")
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
|
||||
# State
|
||||
context_stack: List[Tuple[str, str]] = [] # ('message'|'oneof'|'enum', name)
|
||||
result: List[str] = []
|
||||
import_added = nanopb_already_imported
|
||||
|
||||
# Patterns for proto structural elements
|
||||
message_re = re.compile(r"^(\s*)message\s+(\w+)\s*\{")
|
||||
oneof_re = re.compile(r"^(\s*)oneof\s+(\w+)\s*\{")
|
||||
enum_re = re.compile(r"^(\s*)enum\s+(\w+)\s*\{")
|
||||
close_re = re.compile(r"^\s*\}")
|
||||
|
||||
# Pattern for field declarations:
|
||||
# indent [optional|repeated] type name = number [options] ;
|
||||
# We exclude map<> fields (different syntax, nanopb handles them differently)
|
||||
# and enum value lines (no type keyword before the name).
|
||||
field_re = re.compile(
|
||||
r"^(\s*)" # (1) indent
|
||||
r"(optional\s+|repeated\s+)?" # (2) optional qualifier
|
||||
r"([\w.]+)\s+" # (3) field type (possibly qualified like google.protobuf.Any)
|
||||
r"(\w+)\s*" # (4) field name
|
||||
r"=\s*(\d+)" # (5) field number
|
||||
r"(?:\s*\[([^\]]*)\])?" # (6) existing options, without brackets
|
||||
r"\s*;" # trailing semicolon
|
||||
)
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Insert nanopb import right after the last existing import line.
|
||||
# Only do this when there IS an existing import (last_import_idx >= 0);
|
||||
# if there are no imports we fall through to the syntax-line fallback below.
|
||||
if not import_added and last_import_idx >= 0 and i == last_import_idx + 1:
|
||||
result.append(f'import "{nanopb_import_path}";')
|
||||
import_added = True
|
||||
|
||||
# --- Track message/oneof/enum nesting ---
|
||||
m = message_re.match(line)
|
||||
if m:
|
||||
context_stack.append(("message", m.group(2)))
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
m = oneof_re.match(line)
|
||||
if m:
|
||||
context_stack.append(("oneof", m.group(2)))
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
m = enum_re.match(line)
|
||||
if m:
|
||||
context_stack.append(("enum", m.group(2)))
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
if close_re.match(line) and context_stack:
|
||||
context_stack.pop()
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
# Skip field injection inside enum bodies (enum values look like fields
|
||||
# but should not have nanopb options added)
|
||||
in_enum = bool(context_stack) and context_stack[-1][0] == "enum"
|
||||
|
||||
# --- Try to match and modify a field declaration ---
|
||||
m = field_re.match(line)
|
||||
if m and not in_enum:
|
||||
indent = m.group(1)
|
||||
qualifier = m.group(2) or ""
|
||||
ftype = m.group(3)
|
||||
fname = m.group(4)
|
||||
fnum = m.group(5)
|
||||
existing_opts = m.group(6) or ""
|
||||
|
||||
# Collect applicable nanopb options (wildcard < specific)
|
||||
extra: Dict[str, Any] = {}
|
||||
|
||||
# 1. Wildcard: any field with this name in this proto file
|
||||
if fname in wildcard:
|
||||
extra.update(wildcard[fname])
|
||||
|
||||
# 2. Specific: check all keys whose last element is fname and whose
|
||||
# preceding path matches the current message context
|
||||
for key, opts in specific.items():
|
||||
if key[-1] == fname:
|
||||
msg_path = key[:-1]
|
||||
if message_path_matches(context_stack, msg_path):
|
||||
extra.update(opts)
|
||||
break
|
||||
|
||||
if extra:
|
||||
nanopb_str = format_nanopb_opts(extra)
|
||||
if existing_opts.strip():
|
||||
opts_block = f"[{existing_opts}, {nanopb_str}]"
|
||||
else:
|
||||
opts_block = f"[{nanopb_str}]"
|
||||
qual = qualifier.rstrip()
|
||||
sep = " " if qual else ""
|
||||
line = f"{indent}{qual}{sep}{ftype} {fname} = {fnum} {opts_block};"
|
||||
|
||||
result.append(line)
|
||||
|
||||
# Edge case: if there were no import lines, add nanopb import after syntax line
|
||||
if not import_added:
|
||||
for i, line in enumerate(result):
|
||||
if line.strip().startswith("syntax") and line.strip().endswith(";"):
|
||||
result.insert(i + 1, f'import "{nanopb_import_path}";')
|
||||
break
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Parse an .options file and inject its constraints into a .proto file in-place."""
|
||||
if len(sys.argv) != 3:
|
||||
print(
|
||||
f"Usage: {sys.argv[0]} <options_file> <proto_file>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
opts_path = Path(sys.argv[1])
|
||||
proto_path = Path(sys.argv[2])
|
||||
|
||||
if not opts_path.exists():
|
||||
print(f"Options file not found: {opts_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not proto_path.exists():
|
||||
print(f"Proto file not found: {proto_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
specific, wildcard = parse_options_file(opts_path)
|
||||
total = len(specific) + len(wildcard)
|
||||
|
||||
if total == 0:
|
||||
print(f" [{opts_path.name}] No injectable options found, skipping.")
|
||||
return 0
|
||||
|
||||
content = proto_path.read_text(encoding="utf-8")
|
||||
|
||||
# After regen-protobufs.sh's sed fixup, the nanopb import path is:
|
||||
nanopb_import_path = "meshtastic/protobuf/nanopb.proto"
|
||||
|
||||
modified = inject_into_proto(content, specific, wildcard, nanopb_import_path)
|
||||
proto_path.write_text(modified, encoding="utf-8")
|
||||
|
||||
print(
|
||||
f" [{opts_path.name}] Injected {len(specific)} specific + "
|
||||
f"{len(wildcard)} wildcard option(s) into {proto_path.name}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,6 +6,12 @@ set -e
|
||||
#gsed -i 's/import "\//import ".\//g' ./protobufs/meshtastic/*
|
||||
#gsed -i 's/package meshtastic;//g' ./protobufs/meshtastic/*
|
||||
|
||||
POETRYDIR=$(poetry env info --path)
|
||||
|
||||
if [[ -z "${POETRYDIR}" ]]; then
|
||||
poetry install
|
||||
fi
|
||||
|
||||
# protoc looks for mypy plugin in the python path
|
||||
source $(poetry env info --path)/bin/activate
|
||||
|
||||
@@ -22,6 +28,7 @@ OUTDIR=${TMPDIR}/out
|
||||
PYIDIR=${TMPDIR}/out
|
||||
mkdir -p "${OUTDIR}" "${INDIR}" "${PYIDIR}"
|
||||
cp ./protobufs/meshtastic/*.proto "${INDIR}"
|
||||
cp ./protobufs/meshtastic/*.options "${INDIR}"
|
||||
cp ./protobufs/nanopb.proto "${INDIR}"
|
||||
|
||||
# OS-X sed is apparently a little different and expects an arg for -i
|
||||
@@ -39,6 +46,19 @@ $SEDCMD 's/^import "meshtastic\//import "meshtastic\/protobuf\//' "${INDIR}/"*.p
|
||||
|
||||
$SEDCMD 's/^import "nanopb.proto"/import "meshtastic\/protobuf\/nanopb.proto"/' "${INDIR}/"*.proto
|
||||
|
||||
# Inject nanopb .options constraints as inline proto field options so that
|
||||
# protoc --python_out embeds them in the generated descriptors. Python code
|
||||
# can then read them via:
|
||||
# field.GetOptions().Extensions[nanopb_pb2.nanopb].max_size
|
||||
echo "Injecting nanopb options into proto files..."
|
||||
for OPTS_FILE in "${INDIR}"/*.options; do
|
||||
BASENAME=$(basename "${OPTS_FILE}" .options)
|
||||
PROTO_FILE="${INDIR}/${BASENAME}.proto"
|
||||
if [ -f "${PROTO_FILE}" ]; then
|
||||
python3 ./bin/inject_nanopb_options.py "${OPTS_FILE}" "${PROTO_FILE}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate the python files
|
||||
./nanopb-0.4.8/generator-bin/protoc -I=$TMPDIR/in --python_out "${OUTDIR}" "--mypy_out=${PYIDIR}" $INDIR/*.proto
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ owner_short: BOB
|
||||
|
||||
channel_url: https://www.meshtastic.org/e/#CgMSAQESCDgBQANIAVAe
|
||||
|
||||
canned_messages: Hi|Bye|Yes|No|Ok
|
||||
ringtone: 24:d=32,o=5,b=565:f6,p,f6,4p,p,f6,p,f6,2p,p,b6,p,b6,p,b6,p,b6,p,b,p,b,p,b,p,b,p,b,p,b,p,b,p,b,1p.,2p.,p
|
||||
|
||||
location:
|
||||
lat: 35.88888
|
||||
lon: -93.88888
|
||||
|
||||
70
examples/CONTRIBUTING.md
Normal file
70
examples/CONTRIBUTING.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Contributing Example Scripts
|
||||
|
||||
Use this guide when adding or updating scripts in `examples/`.
|
||||
|
||||
## Must-have checklist before opening a PR
|
||||
|
||||
1. Script teaches one clear thing (its primary learning goal).
|
||||
2. File name matches that goal.
|
||||
3. Top docstring states purpose, transport scope, behavior, and expected output.
|
||||
4. Script has safe shutdown (`with ...` or `finally`) and graceful `KeyboardInterrupt` handling.
|
||||
5. Argument handling is clear (`argparse` preferred).
|
||||
6. Errors are explicit (no bare `except:`).
|
||||
7. The script is not a near-duplicate of an existing example.
|
||||
|
||||
## Choose the right teaching goal
|
||||
|
||||
Each example should have one primary lesson. Keep it focused.
|
||||
|
||||
- Good: "Send one text message over serial."
|
||||
- Good: "Print inbound text messages."
|
||||
- Avoid: discovery + chat + config mutation all in one script unless that combined flow is the lesson.
|
||||
|
||||
## Transport scope must be explicit
|
||||
|
||||
State exactly what transports are supported and why.
|
||||
|
||||
- Serial-only when that keeps the example simplest.
|
||||
- Multi-transport (Serial/TCP/BLE) only when transport selection is part of the lesson.
|
||||
- If TCP/BLE are supported, expose explicit flags (`--host`, `--ble`) and document defaults.
|
||||
|
||||
## Behavior and output should be predictable
|
||||
|
||||
Readers should know if the script sends, receives, mutates config, or combines those.
|
||||
|
||||
- Receive examples: subscribe to the narrowest pubsub topic that matches the lesson.
|
||||
- Send examples: clarify destination behavior (broadcast default vs explicit destination).
|
||||
- Mutation examples: clearly document side effects.
|
||||
|
||||
Output should make success easy to confirm:
|
||||
|
||||
- Print concise, stable status/event lines.
|
||||
- Avoid noisy debug output unless the script is specifically diagnostic-focused.
|
||||
|
||||
## Cleanup and error handling
|
||||
|
||||
- Use context managers where practical; otherwise close interfaces in `finally`.
|
||||
- Handle `KeyboardInterrupt` cleanly.
|
||||
- Exit non-zero for invalid args, connection/setup failures, or command failures.
|
||||
|
||||
## Naming guidance
|
||||
|
||||
Use descriptive names tied to the teaching goal.
|
||||
|
||||
- Prefer names like `tcp_connection_info_once.py` over `pub_sub_example.py`.
|
||||
- Prefer names like `tcp_pubsub_send_and_receive.py` over `pub_sub_example2.py`.
|
||||
- Avoid generic names such as `example2.py`.
|
||||
|
||||
Keep existing filenames only when compatibility or discoverability outweighs clarity.
|
||||
|
||||
## New script vs extending an existing one
|
||||
|
||||
Create a new script when:
|
||||
|
||||
- The learning goal is genuinely distinct.
|
||||
- Combining behaviors would make either example harder to understand.
|
||||
|
||||
Extend an existing script when:
|
||||
|
||||
- The change deepens the same lesson.
|
||||
- The resulting script remains focused and readable.
|
||||
@@ -1,21 +1,42 @@
|
||||
"""Simple program to demo how to use meshtastic library.
|
||||
To run: python examples/get_hw.py
|
||||
"""Print the local node hardware model.
|
||||
|
||||
Purpose: show the narrowest read-only local hardware lookup.
|
||||
Transport scope: Serial only.
|
||||
Behavior: reads local node metadata and prints hwModel.
|
||||
Expected output: one hardware model line, if available.
|
||||
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
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()
|
||||
def main() -> int:
|
||||
"""Print the hardware model for the local node."""
|
||||
if len(sys.argv) != 1:
|
||||
print(f"usage: {sys.argv[0]}")
|
||||
print("Print the hardware model for the local node.")
|
||||
return 3
|
||||
|
||||
parser = argparse.ArgumentParser(description="Print local Meshtastic hardware model")
|
||||
parser.parse_args()
|
||||
|
||||
try:
|
||||
with meshtastic.serial_interface.SerialInterface() as iface:
|
||||
if iface.nodes:
|
||||
for node in iface.nodes.values():
|
||||
if node["num"] == iface.myInfo.my_node_num:
|
||||
print(node["user"]["hwModel"])
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not read hardware model: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
"""Simple program to demo how to use meshtastic library.
|
||||
To run: python examples/hello_world_serial.py
|
||||
"""Send one text message over serial.
|
||||
|
||||
Purpose: minimal send-only example.
|
||||
Transport scope: Serial only.
|
||||
Behavior: sends one message and exits.
|
||||
Expected output: no output on success.
|
||||
Cleanup/error handling: exits with code 3 for bad args, closes interface on exit.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import meshtastic
|
||||
import meshtastic.serial_interface
|
||||
|
||||
# simple arg check
|
||||
if len(sys.argv) < 2:
|
||||
print(f"usage: {sys.argv[0]} message")
|
||||
sys.exit(3)
|
||||
|
||||
# By default will try to find a meshtastic device,
|
||||
# otherwise provide a device path like /dev/ttyUSB0
|
||||
iface = meshtastic.serial_interface.SerialInterface()
|
||||
iface.sendText(sys.argv[1])
|
||||
iface.close()
|
||||
def main() -> int:
|
||||
"""Parse arguments and send one text message."""
|
||||
if len(sys.argv) < 2:
|
||||
print(f"usage: {sys.argv[0]} message")
|
||||
return 3
|
||||
|
||||
parser = argparse.ArgumentParser(description="Send one Meshtastic text message over serial")
|
||||
parser.add_argument("message", help="Message text to broadcast")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
with meshtastic.serial_interface.SerialInterface() as iface:
|
||||
iface.sendText(args.message)
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not send message: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -1,20 +1,46 @@
|
||||
"""Simple program to demo how to use meshtastic library.
|
||||
To run: python examples/info.py
|
||||
"""Show a concise local node summary over serial.
|
||||
|
||||
Purpose: read local node identity and metadata in one place.
|
||||
Transport scope: Serial only.
|
||||
Behavior: reads node database, prints local node ID/name/hardware model.
|
||||
Expected output: 1-3 summary lines describing the local node.
|
||||
Cleanup/error handling: closes interface on exit and prints clear errors on failure.
|
||||
"""
|
||||
|
||||
import meshtastic
|
||||
import meshtastic.serial_interface
|
||||
|
||||
iface = meshtastic.serial_interface.SerialInterface()
|
||||
|
||||
# call showInfo() just to ensure values are populated
|
||||
# info = iface.showInfo()
|
||||
def main() -> int:
|
||||
"""Print local node summary fields."""
|
||||
try:
|
||||
with meshtastic.serial_interface.SerialInterface() as iface:
|
||||
local_num = iface.myInfo.my_node_num
|
||||
local_node = None
|
||||
if iface.nodes:
|
||||
for node in iface.nodes.values():
|
||||
if node["num"] == local_num:
|
||||
local_node = node
|
||||
break
|
||||
|
||||
if not local_node:
|
||||
print(f"Local node not found in node database (node num: {local_num}).")
|
||||
return 1
|
||||
|
||||
user = local_node.get("user", {})
|
||||
print(f"Node number: {local_num}")
|
||||
print(f"Node ID: {local_node.get('id', 'unknown')}")
|
||||
print(
|
||||
"Name: "
|
||||
f"{user.get('longName', 'unknown')} ({user.get('shortName', 'unknown')})"
|
||||
)
|
||||
print(f"Hardware model: {user.get('hwModel', 'unknown')}")
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not read local node summary: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if iface.nodes:
|
||||
for n in iface.nodes.values():
|
||||
if n["num"] == iface.myInfo.my_node_num:
|
||||
print(n["user"]["hwModel"])
|
||||
break
|
||||
|
||||
iface.close()
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
64
examples/meshtastic_serial_message_reader.py
Normal file
64
examples/meshtastic_serial_message_reader.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Passively monitor incoming text messages over serial.
|
||||
|
||||
Purpose: receive-only monitor for text messages.
|
||||
Transport scope: Serial only.
|
||||
Behavior: subscribes to text receive events and prints timestamp/channel/sender/text.
|
||||
Expected output: one line per received text message.
|
||||
Cleanup/error handling: graceful Ctrl+C exit and explicit connection errors.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from pubsub import pub
|
||||
import meshtastic.serial_interface
|
||||
|
||||
_TZ_NAME = time.tzname[time.localtime().tm_isdst > 0]
|
||||
|
||||
|
||||
def on_receive(packet: dict[str, Any], interface: Any) -> None: # pylint: disable=unused-argument
|
||||
"""Print a compact line for each received text packet."""
|
||||
decoded = packet.get("decoded", {})
|
||||
if decoded.get("portnum") != "TEXT_MESSAGE_APP":
|
||||
return
|
||||
|
||||
message = decoded.get("text")
|
||||
if not message:
|
||||
return
|
||||
|
||||
channel_num = packet.get("channel", 0)
|
||||
sender_id = packet.get("fromId", "unknown")
|
||||
message_time = datetime.now().strftime(f"%a %b %d %Y %H:%M:%S {_TZ_NAME}")
|
||||
print(f"{message_time} : {channel_num} : {sender_id} : {message}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Connect over serial and print inbound text messages."""
|
||||
parser = argparse.ArgumentParser(description="Read incoming Meshtastic text over serial")
|
||||
parser.add_argument("--port", default=None, help="Serial port path (default: auto-detect)")
|
||||
args = parser.parse_args()
|
||||
|
||||
pub.subscribe(on_receive, "meshtastic.receive")
|
||||
|
||||
iface: Optional[meshtastic.serial_interface.SerialInterface] = None
|
||||
try:
|
||||
iface = meshtastic.serial_interface.SerialInterface(devPath=args.port)
|
||||
print("Connected. Listening for text messages. Press Ctrl+C to exit.")
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not monitor serial messages: {exc}")
|
||||
return 1
|
||||
finally:
|
||||
if iface:
|
||||
iface.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,30 +0,0 @@
|
||||
"""Simple program to demo how to use meshtastic library.
|
||||
To run: python examples/pub_sub_example.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from pubsub import pub
|
||||
|
||||
import meshtastic
|
||||
import meshtastic.tcp_interface
|
||||
|
||||
# simple arg check
|
||||
if len(sys.argv) < 2:
|
||||
print(f"usage: {sys.argv[0]} host")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
|
||||
"""This is called when we (re)connect to the radio."""
|
||||
print(interface.myInfo)
|
||||
interface.close()
|
||||
|
||||
|
||||
pub.subscribe(onConnection, "meshtastic.connection.established")
|
||||
|
||||
try:
|
||||
iface = meshtastic.tcp_interface.TCPInterface(sys.argv[1])
|
||||
except:
|
||||
print(f"Error: Could not connect to {sys.argv[1]}")
|
||||
sys.exit(1)
|
||||
@@ -1,39 +0,0 @@
|
||||
"""Simple program to demo how to use meshtastic library.
|
||||
To run: python examples/pub_sub_example2.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pubsub import pub
|
||||
|
||||
import meshtastic
|
||||
import meshtastic.tcp_interface
|
||||
|
||||
# simple arg check
|
||||
if len(sys.argv) < 2:
|
||||
print(f"usage: {sys.argv[0]} host")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def onReceive(packet, interface): # pylint: disable=unused-argument
|
||||
"""called when a packet arrives"""
|
||||
print(f"Received: {packet}")
|
||||
|
||||
|
||||
def onConnection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
|
||||
"""called when we (re)connect to the radio"""
|
||||
# defaults to broadcast, specify a destination ID if you wish
|
||||
interface.sendText("hello mesh")
|
||||
|
||||
|
||||
pub.subscribe(onReceive, "meshtastic.receive")
|
||||
pub.subscribe(onConnection, "meshtastic.connection.established")
|
||||
try:
|
||||
iface = meshtastic.tcp_interface.TCPInterface(hostname=sys.argv[1])
|
||||
while True:
|
||||
time.sleep(1000)
|
||||
iface.close()
|
||||
except Exception as ex:
|
||||
print(f"Error: Could not connect to {sys.argv[1]} {ex}")
|
||||
sys.exit(1)
|
||||
73
examples/replymessage.py
Normal file
73
examples/replymessage.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Auto-reply to received text messages.
|
||||
|
||||
Purpose: demonstrate receive callback + generated reply flow.
|
||||
Transport scope: Serial default, optional TCP/BLE.
|
||||
Behavior: listens for text, prints message metadata, sends one reply per text message.
|
||||
Expected output: "Connected..." plus message/reply lines while running.
|
||||
Cleanup/error handling: clear connect failures and graceful Ctrl+C close.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
from typing import Any, Optional, Union
|
||||
from pubsub import pub
|
||||
import meshtastic.serial_interface
|
||||
import meshtastic.tcp_interface
|
||||
import meshtastic.ble_interface
|
||||
from meshtastic.mesh_interface import MeshInterface
|
||||
|
||||
def onReceive(packet: dict, interface: MeshInterface) -> None:
|
||||
"""Reply to every received packet with some info"""
|
||||
text: Optional[str] = packet.get("decoded", {}).get("text")
|
||||
if text:
|
||||
rx_snr: Any = packet.get("rxSnr", "unknown")
|
||||
hop_limit: Any = packet.get("hopLimit", "unknown")
|
||||
print(f"message: {text}")
|
||||
reply: str = f"got msg '{text}' with rxSnr: {rx_snr} and hopLimit: {hop_limit}"
|
||||
print("Sending reply: ", reply)
|
||||
interface.sendText(reply)
|
||||
|
||||
def onConnection(interface: MeshInterface, topic: Any = pub.AUTO_TOPIC) -> None: # pylint: disable=unused-argument
|
||||
"""called when we (re)connect to the radio"""
|
||||
print("Connected. Will auto-reply to all messages while running.")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Meshtastic Auto-Reply Feature Demo")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--host", help="Connect via TCP to this hostname or IP")
|
||||
group.add_argument("--ble", help="Connect via BLE to this MAC address")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
pub.subscribe(onReceive, "meshtastic.receive")
|
||||
pub.subscribe(onConnection, "meshtastic.connection.established")
|
||||
|
||||
iface: Optional[Union[
|
||||
meshtastic.tcp_interface.TCPInterface,
|
||||
meshtastic.ble_interface.BLEInterface,
|
||||
meshtastic.serial_interface.SerialInterface
|
||||
]] = None
|
||||
|
||||
# defaults to serial, use --host for TCP or --ble for Bluetooth
|
||||
try:
|
||||
if args.host:
|
||||
# note: timeout only applies after connection, not during the initial connect attempt
|
||||
# TCPInterface.myConnect() calls socket.create_connection() without a timeout
|
||||
iface = meshtastic.tcp_interface.TCPInterface(hostname=args.host, timeout=10)
|
||||
elif args.ble:
|
||||
iface = meshtastic.ble_interface.BLEInterface(address=args.ble, timeout=10)
|
||||
else:
|
||||
iface = meshtastic.serial_interface.SerialInterface(timeout=10)
|
||||
except KeyboardInterrupt as exc:
|
||||
raise SystemExit(0) from exc
|
||||
except Exception as e:
|
||||
print(f"Error: Could not connect. {e}")
|
||||
raise SystemExit(1) from e
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
if iface:
|
||||
iface.close()
|
||||
@@ -1,7 +1,13 @@
|
||||
"""Program to scan for hardware
|
||||
To run: python examples/scan_for_devices.py
|
||||
"""Scan host serial hardware for supported Meshtastic devices.
|
||||
|
||||
Purpose: host-side discovery without opening a radio session.
|
||||
Transport scope: none (OS/device scanning only).
|
||||
Behavior: scans vendor IDs, lists matched devices, and candidate active ports.
|
||||
Expected output: vendor ID list, zero-or-more detected devices, and port list.
|
||||
Cleanup/error handling: exits with code 3 for bad args and code 1 on scan errors.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from meshtastic.util import (
|
||||
@@ -10,20 +16,38 @@ from meshtastic.util import (
|
||||
get_unique_vendor_ids,
|
||||
)
|
||||
|
||||
# simple arg check
|
||||
if len(sys.argv) != 1:
|
||||
print(f"usage: {sys.argv[0]}")
|
||||
print("Detect which device we might have.")
|
||||
sys.exit(3)
|
||||
|
||||
vids = get_unique_vendor_ids()
|
||||
print(f"Searching for all devices with these vendor ids {vids}")
|
||||
def main() -> int:
|
||||
"""Run device detection and print candidate ports."""
|
||||
if len(sys.argv) != 1:
|
||||
print(f"usage: {sys.argv[0]}")
|
||||
print("Detect which device we might have.")
|
||||
return 3
|
||||
|
||||
sds = detect_supported_devices()
|
||||
if len(sds) > 0:
|
||||
print("Detected possible devices:")
|
||||
for d in sds:
|
||||
print(f" name:{d.name}{d.version} firmware:{d.for_firmware}")
|
||||
parser = argparse.ArgumentParser(description="Scan host for supported Meshtastic devices")
|
||||
parser.parse_args()
|
||||
|
||||
ports = active_ports_on_supported_devices(sds)
|
||||
print(f"ports:{ports}")
|
||||
try:
|
||||
vids = get_unique_vendor_ids()
|
||||
print(f"Searching for all devices with these vendor ids {vids}")
|
||||
|
||||
supported_devices = detect_supported_devices()
|
||||
if supported_devices:
|
||||
print("Detected possible devices:")
|
||||
for device in supported_devices:
|
||||
print(
|
||||
f" name:{device.name}{device.version} firmware:{device.for_firmware}"
|
||||
)
|
||||
else:
|
||||
print("Detected possible devices: none")
|
||||
|
||||
ports = active_ports_on_supported_devices(supported_devices)
|
||||
print(f"ports:{ports}")
|
||||
except Exception as exc:
|
||||
print(f"Error: device scan failed: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
"""Simple program to demo how to use meshtastic library.
|
||||
To run: python examples/set_owner.py Bobby 333
|
||||
"""Set local owner long/short name over serial.
|
||||
|
||||
Purpose: demonstrate a local config mutation workflow.
|
||||
Transport scope: Serial only.
|
||||
Behavior: updates owner long name and optional short name.
|
||||
Expected output: prints the owner values being applied.
|
||||
Cleanup/error handling: exits with code 3 for bad args and closes interface on exit.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
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()
|
||||
def main() -> int:
|
||||
"""Parse args and set owner fields."""
|
||||
if len(sys.argv) < 2:
|
||||
print(f"usage: {sys.argv[0]} long_name [short_name]")
|
||||
return 3
|
||||
|
||||
parser = argparse.ArgumentParser(description="Set Meshtastic local owner information")
|
||||
parser.add_argument("long_name", help="Owner long name")
|
||||
parser.add_argument("short_name", nargs="?", default=None, help="Owner short name")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Setting owner long_name={args.long_name}, short_name={args.short_name}")
|
||||
try:
|
||||
with meshtastic.serial_interface.SerialInterface() as iface:
|
||||
iface.localNode.setOwner(args.long_name, args.short_name)
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not set owner: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
"""Simple program to show serial ports.
|
||||
"""List serial ports currently visible to Meshtastic helpers.
|
||||
|
||||
Purpose: fastest host-side serial port enumeration.
|
||||
Transport scope: none (host serial listing only).
|
||||
Behavior: prints result of `findPorts()`.
|
||||
Expected output: list-like representation of available candidate ports.
|
||||
Cleanup/error handling: exits with code 1 on unexpected scan error.
|
||||
"""
|
||||
|
||||
from meshtastic.util import findPorts
|
||||
|
||||
print(findPorts())
|
||||
|
||||
def main() -> int:
|
||||
"""Print discovered serial ports."""
|
||||
try:
|
||||
print(findPorts())
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not list ports: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
41
examples/tcp_connection_info_once.py
Normal file
41
examples/tcp_connection_info_once.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Connect over TCP, print connection info once, then exit.
|
||||
|
||||
Purpose: demonstrate pubsub connection lifecycle callback.
|
||||
Transport scope: TCP only.
|
||||
Behavior: subscribe to `meshtastic.connection.established`, print `myInfo`, then close.
|
||||
Expected output: one object/line showing local radio info after connect.
|
||||
Cleanup/error handling: explicit connect failure message and clean close on callback.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
from pubsub import pub
|
||||
import meshtastic.tcp_interface
|
||||
|
||||
|
||||
def on_connection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
|
||||
"""Print local radio info when connected, then close."""
|
||||
print(interface.myInfo)
|
||||
interface.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Parse args, connect, and wait for established callback."""
|
||||
parser = argparse.ArgumentParser(description="Print radio info on TCP connect and exit")
|
||||
parser.add_argument("host", help="TCP hostname or IP of the Meshtastic node")
|
||||
args = parser.parse_args()
|
||||
|
||||
pub.subscribe(on_connection, "meshtastic.connection.established")
|
||||
|
||||
try:
|
||||
meshtastic.tcp_interface.TCPInterface(args.host)
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not connect to {args.host}: {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,14 +1,47 @@
|
||||
"""Demonstration of how to look up a radio's location via its LAN connection.
|
||||
Before running, connect your machine to the same WiFi network as the radio.
|
||||
"""
|
||||
"""Look up local node position over TCP.
|
||||
|
||||
Purpose: demonstrate read-only position lookup via LAN/TCP.
|
||||
Transport scope: TCP only.
|
||||
Behavior: connects, reads local node position, prints it, then exits.
|
||||
Expected output: position dict for local node.
|
||||
Cleanup/error handling: explicit connect/read failures and clean close.
|
||||
"""
|
||||
# pylint: disable=duplicate-code
|
||||
|
||||
import argparse
|
||||
|
||||
import meshtastic
|
||||
import meshtastic.tcp_interface
|
||||
|
||||
radio_hostname = "meshtastic.local" # Can also be an IP
|
||||
iface = meshtastic.tcp_interface.TCPInterface(radio_hostname)
|
||||
my_node_num = iface.myInfo.my_node_num
|
||||
pos = iface.nodesByNum[my_node_num]["position"]
|
||||
print(pos)
|
||||
|
||||
iface.close()
|
||||
def main() -> int:
|
||||
"""Connect over TCP and print local node position."""
|
||||
parser = argparse.ArgumentParser(description="Print local node position over TCP")
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
default="meshtastic.local",
|
||||
help="TCP hostname or IP (default: meshtastic.local)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
iface = None
|
||||
try:
|
||||
iface = meshtastic.tcp_interface.TCPInterface(args.host)
|
||||
my_node_num = iface.myInfo.my_node_num
|
||||
pos = iface.nodesByNum[my_node_num].get("position")
|
||||
if pos is None:
|
||||
print(f"No position available for local node {my_node_num}")
|
||||
return 1
|
||||
print(pos)
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not read position from {args.host}: {exc}")
|
||||
return 1
|
||||
finally:
|
||||
if iface:
|
||||
iface.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
54
examples/tcp_pubsub_send_and_receive.py
Normal file
54
examples/tcp_pubsub_send_and_receive.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Send once on connect and print received packets over TCP.
|
||||
|
||||
Purpose: demonstrate pubsub send-on-connect plus receive callback flow.
|
||||
Transport scope: TCP only.
|
||||
Behavior: sends "hello mesh" at connect, prints packets while running.
|
||||
Expected output: "Connected..." plus "Received: ..." lines for inbound packets.
|
||||
Cleanup/error handling: graceful Ctrl+C exit and clean interface close.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from pubsub import pub
|
||||
from meshtastic.tcp_interface import TCPInterface
|
||||
|
||||
|
||||
def on_receive(packet, interface): # pylint: disable=unused-argument
|
||||
"""Print each inbound packet."""
|
||||
print(f"Received: {packet}")
|
||||
|
||||
|
||||
def on_connection(interface, topic=pub.AUTO_TOPIC): # pylint: disable=unused-argument
|
||||
"""Send a broadcast text when connected."""
|
||||
print("Connected. Sending one broadcast message.")
|
||||
interface.sendText("hello mesh")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Parse args, connect via TCP, and run callbacks."""
|
||||
parser = argparse.ArgumentParser(description="TCP pubsub send-and-receive example")
|
||||
parser.add_argument("host", help="TCP hostname or IP of the Meshtastic node")
|
||||
args = parser.parse_args()
|
||||
|
||||
pub.subscribe(on_receive, "meshtastic.receive")
|
||||
pub.subscribe(on_connection, "meshtastic.connection.established")
|
||||
|
||||
iface = None
|
||||
try:
|
||||
iface = TCPInterface(hostname=args.host)
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
except Exception as exc:
|
||||
print(f"Error: Could not connect to {args.host}: {exc}")
|
||||
return 1
|
||||
finally:
|
||||
if iface:
|
||||
iface.close()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
73
examples/textchat.py
Normal file
73
examples/textchat.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Interactive text chat demo.
|
||||
|
||||
Purpose: demonstrate bidirectional text chat loop.
|
||||
Transport scope: Serial default, optional TCP/BLE.
|
||||
Behavior: prints incoming messages and sends each typed line as text.
|
||||
Expected output: incoming sender/text lines and sent messages reaching peers.
|
||||
Cleanup/error handling: explicit connect errors and graceful Ctrl+C / EOF close.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from typing import Any, Optional, Union
|
||||
from pubsub import pub
|
||||
import meshtastic.serial_interface
|
||||
import meshtastic.tcp_interface
|
||||
import meshtastic.ble_interface
|
||||
from meshtastic.mesh_interface import MeshInterface
|
||||
|
||||
def onReceive(packet: dict, interface: MeshInterface) -> None: # pylint: disable=unused-argument
|
||||
"""called when a packet arrives"""
|
||||
text: Optional[str] = packet.get("decoded", {}).get("text")
|
||||
if text:
|
||||
sender: str = packet.get("fromId", "unknown")
|
||||
print(f"{sender}: {text}")
|
||||
|
||||
def onConnection(interface: MeshInterface, topic: Any = pub.AUTO_TOPIC) -> None: # pylint: disable=unused-argument
|
||||
"""called when we (re)connect to the radio"""
|
||||
print("Connected. Type a message and press Enter to send. Ctrl+C to exit.")
|
||||
|
||||
parser = argparse.ArgumentParser(description="Meshtastic text chat demo")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--host", help="Connect via TCP to this hostname or IP")
|
||||
group.add_argument("--ble", help="Connect via BLE to this MAC address or device name")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
pub.subscribe(onReceive, "meshtastic.receive")
|
||||
pub.subscribe(onConnection, "meshtastic.connection.established")
|
||||
|
||||
iface: Optional[Union[
|
||||
meshtastic.tcp_interface.TCPInterface,
|
||||
meshtastic.ble_interface.BLEInterface,
|
||||
meshtastic.serial_interface.SerialInterface
|
||||
]] = None
|
||||
|
||||
# defaults to serial, use --host for TCP or --ble for Bluetooth
|
||||
try:
|
||||
if args.host:
|
||||
# note: timeout only applies after connection, not during the initial connect attempt
|
||||
# TCPInterface.myConnect() calls socket.create_connection() without a timeout
|
||||
iface = meshtastic.tcp_interface.TCPInterface(hostname=args.host, timeout=10)
|
||||
elif args.ble:
|
||||
iface = meshtastic.ble_interface.BLEInterface(address=args.ble, timeout=10)
|
||||
else:
|
||||
iface = meshtastic.serial_interface.SerialInterface(timeout=10)
|
||||
except KeyboardInterrupt as exc:
|
||||
raise SystemExit(0) from exc
|
||||
except Exception as e:
|
||||
print(f"Error: Could not connect. {e}")
|
||||
raise SystemExit(1) from e
|
||||
|
||||
assert iface is not None
|
||||
try:
|
||||
while True:
|
||||
line = input()
|
||||
if line:
|
||||
iface.sendText(line)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except EOFError:
|
||||
pass
|
||||
finally:
|
||||
if iface:
|
||||
iface.close()
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Program to create and delete waypoint
|
||||
To run:
|
||||
python3 examples/waypoint.py --port /dev/ttyUSB0 create 45 test the_desc_2 '2024-12-18T23:05:23' 48.74 7.35
|
||||
python3 examples/waypoint.py delete 45
|
||||
"""Create or delete a waypoint.
|
||||
|
||||
Purpose: demonstrate waypoint mutation API (create/delete).
|
||||
Transport scope: Serial only.
|
||||
Behavior: sends waypoint create/delete request and prints API response.
|
||||
Expected output: request response object printed to stdout.
|
||||
Cleanup/error handling: explicit argument parsing and clean interface close.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -12,25 +15,28 @@ import meshtastic
|
||||
import meshtastic.serial_interface
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='waypoint',
|
||||
description='Create and delete Meshtastic waypoint')
|
||||
parser.add_argument('--port', default=None)
|
||||
parser.add_argument('--debug', default=False, action='store_true')
|
||||
prog="waypoint", description="Create and delete Meshtastic waypoint"
|
||||
)
|
||||
parser.add_argument("--port", default=None)
|
||||
parser.add_argument("--debug", default=False, action="store_true")
|
||||
|
||||
subparsers = parser.add_subparsers(dest='cmd')
|
||||
parser_delete = subparsers.add_parser('delete', help='Delete a waypoint')
|
||||
parser_delete.add_argument('id', help="id of the waypoint")
|
||||
subparsers = parser.add_subparsers(dest="cmd", required=True)
|
||||
parser_delete = subparsers.add_parser("delete", help="Delete a waypoint")
|
||||
parser_delete.add_argument("id", type=int, help="ID of the waypoint")
|
||||
|
||||
parser_create = subparsers.add_parser('create', help='Create a new waypoint')
|
||||
parser_create.add_argument('id', help="id of the waypoint")
|
||||
parser_create.add_argument('name', help="name of the waypoint")
|
||||
parser_create.add_argument('description', help="description of the waypoint")
|
||||
parser_create.add_argument('expire', help="expiration date of the waypoint as interpreted by datetime.fromisoformat")
|
||||
parser_create.add_argument('latitude', help="latitude of the waypoint")
|
||||
parser_create.add_argument('longitude', help="longitude of the waypoint")
|
||||
parser_create = subparsers.add_parser("create", help="Create a new waypoint")
|
||||
parser_create.add_argument("id", type=int, help="ID of the waypoint")
|
||||
parser_create.add_argument("name", help="Name of the waypoint")
|
||||
parser_create.add_argument("description", help="Description of the waypoint")
|
||||
parser_create.add_argument("icon", help="Icon of the waypoint")
|
||||
parser_create.add_argument(
|
||||
"expire",
|
||||
help="Expiration time as ISO timestamp accepted by datetime.fromisoformat",
|
||||
)
|
||||
parser_create.add_argument("latitude", type=float, help="Latitude of the waypoint")
|
||||
parser_create.add_argument("longitude", type=float, help="Longitude of the waypoint")
|
||||
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
# By default will try to find a meshtastic device,
|
||||
# otherwise provide a device path like /dev/ttyUSB0
|
||||
@@ -39,17 +45,16 @@ if args.debug:
|
||||
else:
|
||||
d = None
|
||||
with meshtastic.serial_interface.SerialInterface(args.port, debugOut=d) as iface:
|
||||
if args.cmd == 'create':
|
||||
if args.cmd == "create":
|
||||
p = iface.sendWaypoint(
|
||||
waypoint_id=int(args.id),
|
||||
waypoint_id=args.id,
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
icon=args.icon,
|
||||
expire=int(datetime.datetime.fromisoformat(args.expire).timestamp()),
|
||||
latitude=float(args.latitude),
|
||||
longitude=float(args.longitude),
|
||||
latitude=args.latitude,
|
||||
longitude=args.longitude,
|
||||
)
|
||||
else:
|
||||
p = iface.deleteWaypoint(int(args.id))
|
||||
p = iface.deleteWaypoint(args.id)
|
||||
print(p)
|
||||
|
||||
# iface.close()
|
||||
|
||||
@@ -35,6 +35,7 @@ type of packet, you should subscribe to the full topic name. If you want to see
|
||||
- `meshtastic.receive.data.portnum(packet)` (where portnum is an integer or well known PortNum enum)
|
||||
- `meshtastic.node.updated(node = NodeInfo)` - published when a node in the DB changes (appears, location changed, username changed, etc...)
|
||||
- `meshtastic.log.line(line)` - a raw unparsed log line from the radio
|
||||
- `meshtastic.clientNotification(notification, interface) - a ClientNotification sent from the radio
|
||||
|
||||
We receive position, user, or data packets from the mesh. You probably only care about `meshtastic.receive.data`. The first argument for
|
||||
that publish will be the packet. Text or binary data packets (from `sendData` or `sendText`) will both arrive this way. If you print packet
|
||||
@@ -128,6 +129,7 @@ NODELESS_WANT_CONFIG_ID = 69420
|
||||
|
||||
publishingThread = DeferredExecution("publishing")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ResponseHandler(NamedTuple):
|
||||
"""A pending response callback, waiting for a response to one of our messages"""
|
||||
@@ -159,31 +161,53 @@ def _onTextReceive(iface, asDict):
|
||||
#
|
||||
# Usually btw this problem is caused by apps sending binary data but setting the payload type to
|
||||
# text.
|
||||
logging.debug(f"in _onTextReceive() asDict:{asDict}")
|
||||
logger.debug(f"in _onTextReceive() asDict:{asDict}")
|
||||
try:
|
||||
asBytes = asDict["decoded"]["payload"]
|
||||
asDict["decoded"]["text"] = asBytes.decode("utf-8")
|
||||
except Exception as ex:
|
||||
logging.error(f"Malformatted utf8 in text message: {ex}")
|
||||
logger.error(f"Malformatted utf8 in text message: {ex}")
|
||||
_receiveInfoUpdate(iface, asDict)
|
||||
|
||||
|
||||
def _onPositionReceive(iface, asDict):
|
||||
"""Special auto parsing for received messages"""
|
||||
logging.debug(f"in _onPositionReceive() asDict:{asDict}")
|
||||
logger.debug(f"in _onPositionReceive() asDict:{asDict}")
|
||||
if "decoded" in asDict:
|
||||
if "position" in asDict["decoded"] and "from" in asDict:
|
||||
p = asDict["decoded"]["position"]
|
||||
logging.debug(f"p:{p}")
|
||||
logger.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
|
||||
logger.debug(f"after fixup p:{p}")
|
||||
# For the local node, only accept position updates with equal
|
||||
# or better precision. The local GPS is authoritative, and
|
||||
# low-precision echoes from the mesh (e.g., map reports relayed
|
||||
# by other nodes) should not overwrite it.
|
||||
# For remote nodes, always accept the latest position since
|
||||
# any update from them reflects their current state.
|
||||
node = iface._getOrCreateByNum(asDict["from"])
|
||||
is_local_node = (
|
||||
iface.myInfo is not None
|
||||
and asDict["from"] == iface.myInfo.my_node_num
|
||||
)
|
||||
if is_local_node:
|
||||
existing = node.get("position", {})
|
||||
existing_precision = existing.get("precisionBits", 0) or 0
|
||||
new_precision = p.get("precisionBits", 0) or 0
|
||||
if existing_precision == 0 or new_precision >= existing_precision:
|
||||
node["position"] = p
|
||||
else:
|
||||
logger.debug(
|
||||
f"Ignoring low-precision position echo for local node "
|
||||
f"({new_precision} < {existing_precision})"
|
||||
)
|
||||
else:
|
||||
node["position"] = p
|
||||
|
||||
|
||||
def _onNodeInfoReceive(iface, asDict):
|
||||
"""Special auto parsing for received messages"""
|
||||
logging.debug(f"in _onNodeInfoReceive() asDict:{asDict}")
|
||||
logger.debug(f"in _onNodeInfoReceive() asDict:{asDict}")
|
||||
if "decoded" in asDict:
|
||||
if "user" in asDict["decoded"] and "from" in asDict:
|
||||
p = asDict["decoded"]["user"]
|
||||
@@ -197,7 +221,7 @@ def _onNodeInfoReceive(iface, asDict):
|
||||
|
||||
def _onTelemetryReceive(iface, asDict):
|
||||
"""Automatically update device metrics on received packets"""
|
||||
logging.debug(f"in _onTelemetryReceive() asDict:{asDict}")
|
||||
logger.debug(f"in _onTelemetryReceive() asDict:{asDict}")
|
||||
if "from" not in asDict:
|
||||
return
|
||||
|
||||
@@ -221,7 +245,7 @@ def _onTelemetryReceive(iface, asDict):
|
||||
updateObj = telemetry.get(toUpdate)
|
||||
newMetrics = node.get(toUpdate, {})
|
||||
newMetrics.update(updateObj)
|
||||
logging.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}")
|
||||
logger.debug(f"updating {toUpdate} metrics for {asDict['from']} to {newMetrics}")
|
||||
node[toUpdate] = newMetrics
|
||||
|
||||
def _receiveInfoUpdate(iface, asDict):
|
||||
@@ -233,7 +257,7 @@ def _receiveInfoUpdate(iface, asDict):
|
||||
|
||||
def _onAdminReceive(iface, asDict):
|
||||
"""Special auto parsing for received messages"""
|
||||
logging.debug(f"in _onAdminReceive() asDict:{asDict}")
|
||||
logger.debug(f"in _onAdminReceive() asDict:{asDict}")
|
||||
if "decoded" in asDict and "from" in asDict and "admin" in asDict["decoded"]:
|
||||
adminMessage = asDict["decoded"]["admin"]["raw"]
|
||||
iface._getOrCreateByNum(asDict["from"])["adminSessionPassKey"] = adminMessage.session_passkey
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
# We just hit the 1600 line limit for main.py, but I currently have a huge set of powermon/structured logging changes
|
||||
# later we can have a separate changelist to refactor main.py into smaller files
|
||||
# pylint: disable=too-many-lines
|
||||
# pylint: disable=R0917,C0302
|
||||
|
||||
from typing import List, Optional, Union
|
||||
from types import ModuleType
|
||||
|
||||
import argparse
|
||||
|
||||
argcomplete: Union[None, ModuleType] = None
|
||||
try:
|
||||
import argcomplete # type: ignore
|
||||
@@ -36,6 +37,7 @@ try:
|
||||
except ImportError as e:
|
||||
have_test = False
|
||||
|
||||
import meshtastic.ota
|
||||
import meshtastic.util
|
||||
import meshtastic.serial_interface
|
||||
import meshtastic.tcp_interface
|
||||
@@ -59,15 +61,17 @@ except ImportError as e:
|
||||
have_powermon = False
|
||||
powermon_exception = e
|
||||
meter = None
|
||||
from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2
|
||||
from meshtastic.protobuf import admin_pb2, channel_pb2, config_pb2, portnums_pb2, mesh_pb2
|
||||
from meshtastic.version import get_active_version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def onReceive(packet, interface) -> None:
|
||||
"""Callback invoked when a packet arrives"""
|
||||
args = mt_config.args
|
||||
try:
|
||||
d = packet.get("decoded")
|
||||
logging.debug(f"in onReceive() d:{d}")
|
||||
logger.debug(f"in onReceive() d:{d}")
|
||||
|
||||
# Exit once we receive a reply
|
||||
if (
|
||||
@@ -82,12 +86,17 @@ def onReceive(packet, interface) -> None:
|
||||
if d is not None and args and args.reply:
|
||||
msg = d.get("text")
|
||||
if msg:
|
||||
rxSnr = packet["rxSnr"]
|
||||
hopLimit = packet["hopLimit"]
|
||||
print(f"message: {msg}")
|
||||
reply = f"got msg '{msg}' with rxSnr: {rxSnr} and hopLimit: {hopLimit}"
|
||||
print("Sending reply: ", reply)
|
||||
interface.sendText(reply)
|
||||
rxChannel = packet.get("channel", 0)
|
||||
targetChannel = int(args.ch_index or 0)
|
||||
if rxChannel == targetChannel:
|
||||
rxSnr = packet["rxSnr"]
|
||||
hopLimit = packet["hopLimit"]
|
||||
print(f"message: {msg}")
|
||||
reply = f"got msg '{msg}' with rxSnr: {rxSnr} and hopLimit: {hopLimit}"
|
||||
print(f"Received channel {rxChannel}. Sending reply: {reply}")
|
||||
interface.sendText(reply,channelIndex=rxChannel)
|
||||
else:
|
||||
print(f"Ignored message on channel {rxChannel} (waiting for channel {targetChannel})")
|
||||
|
||||
except Exception as ex:
|
||||
print(f"Warning: Error processing received packet: {ex}.")
|
||||
@@ -101,7 +110,7 @@ def onConnection(interface, topic=pub.AUTO_TOPIC) -> None: # pylint: disable=W0
|
||||
def checkChannel(interface: MeshInterface, channelIndex: int) -> bool:
|
||||
"""Given an interface and channel index, return True if that channel is non-disabled on the local node"""
|
||||
ch = interface.localNode.getChannelByChannelIndex(channelIndex)
|
||||
logging.debug(f"ch:{ch}")
|
||||
logger.debug(f"ch:{ch}")
|
||||
return ch and ch.role != channel_pb2.Channel.Role.DISABLED
|
||||
|
||||
|
||||
@@ -114,7 +123,7 @@ def getPref(node, comp_name) -> bool:
|
||||
else:
|
||||
pref_value = meshtastic.util.toStr(pref_value)
|
||||
print(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}")
|
||||
logging.debug(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}")
|
||||
logger.debug(f"{str(config_type.name)}.{uni_name}: {str(pref_value)}")
|
||||
|
||||
name = splitCompoundName(comp_name)
|
||||
wholeField = name[0] == name[1] # We want the whole field
|
||||
@@ -123,8 +132,8 @@ def getPref(node, comp_name) -> bool:
|
||||
# Note: protobufs has the keys in snake_case, so snake internally
|
||||
snake_name = meshtastic.util.camel_to_snake(name[1])
|
||||
uni_name = camel_name if mt_config.camel_case else snake_name
|
||||
logging.debug(f"snake_name:{snake_name} camel_name:{camel_name}")
|
||||
logging.debug(f"use camel:{mt_config.camel_case}")
|
||||
logger.debug(f"snake_name:{snake_name} camel_name:{camel_name}")
|
||||
logger.debug(f"use camel:{mt_config.camel_case}")
|
||||
|
||||
# First validate the input
|
||||
localConfig = node.localConfig
|
||||
@@ -155,11 +164,11 @@ def getPref(node, comp_name) -> bool:
|
||||
config_values = getattr(config, config_type.name)
|
||||
if not wholeField:
|
||||
pref_value = getattr(config_values, pref.name)
|
||||
repeated = pref.label == pref.LABEL_REPEATED
|
||||
repeated = _is_repeated_field(pref)
|
||||
_printSetting(config_type, uni_name, pref_value, repeated)
|
||||
else:
|
||||
for field in config_values.ListFields():
|
||||
repeated = field[0].label == field[0].LABEL_REPEATED
|
||||
repeated = _is_repeated_field(field[0])
|
||||
_printSetting(config_type, field[0].name, field[1], repeated)
|
||||
else:
|
||||
# Always show whole field for remote node
|
||||
@@ -198,8 +207,8 @@ def setPref(config, comp_name, raw_val) -> bool:
|
||||
snake_name = meshtastic.util.camel_to_snake(name[-1])
|
||||
camel_name = meshtastic.util.snake_to_camel(name[-1])
|
||||
uni_name = camel_name if mt_config.camel_case else snake_name
|
||||
logging.debug(f"snake_name:{snake_name}")
|
||||
logging.debug(f"camel_name:{camel_name}")
|
||||
logger.debug(f"snake_name:{snake_name}")
|
||||
logger.debug(f"camel_name:{camel_name}")
|
||||
|
||||
objDesc = config.DESCRIPTOR
|
||||
config_part = config
|
||||
@@ -223,7 +232,7 @@ def setPref(config, comp_name, raw_val) -> bool:
|
||||
val = meshtastic.util.fromStr(raw_val)
|
||||
else:
|
||||
val = raw_val
|
||||
logging.debug(f"valStr:{raw_val} val:{val}")
|
||||
logger.debug(f"valStr:{raw_val} val:{val}")
|
||||
|
||||
if snake_name == "wifi_psk" and len(str(raw_val)) < 8:
|
||||
print("Warning: network.wifi_psk must be 8 or more characters.")
|
||||
@@ -250,7 +259,7 @@ def setPref(config, comp_name, raw_val) -> bool:
|
||||
return False
|
||||
|
||||
# repeating fields need to be handled with append, not setattr
|
||||
if pref.label != pref.LABEL_REPEATED:
|
||||
if not _is_repeated_field(pref):
|
||||
try:
|
||||
if config_type.message_type is not None:
|
||||
config_values = getattr(config_part, config_type.name)
|
||||
@@ -274,7 +283,8 @@ def setPref(config, comp_name, raw_val) -> bool:
|
||||
else:
|
||||
print(f"Adding '{raw_val}' to the {pref.name} list")
|
||||
cur_vals = [x for x in getattr(config_values, pref.name) if x not in [0, "", b""]]
|
||||
cur_vals.append(val)
|
||||
if val not in cur_vals:
|
||||
cur_vals.append(val)
|
||||
getattr(config_values, pref.name)[:] = cur_vals
|
||||
return True
|
||||
|
||||
@@ -339,32 +349,60 @@ def onConnected(interface):
|
||||
# can include lat/long/alt etc: latitude = 37.5, longitude = -122.1
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setFixedPosition(lat, lon, alt)
|
||||
|
||||
if args.set_owner or args.set_owner_short:
|
||||
if args.set_owner or args.set_owner_short or args.set_is_unmessageable:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
if args.set_owner and args.set_owner_short:
|
||||
print(f"Setting device owner to {args.set_owner} and short name to {args.set_owner_short}")
|
||||
elif args.set_owner:
|
||||
print(f"Setting device owner to {args.set_owner}")
|
||||
else: # short name only
|
||||
print(f"Setting device owner short to {args.set_owner_short}")
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(long_name=args.set_owner, short_name=args.set_owner_short)
|
||||
|
||||
# TODO: add to export-config and configure
|
||||
long_name = args.set_owner.strip() if args.set_owner else None
|
||||
short_name = args.set_owner_short.strip() if args.set_owner_short else None
|
||||
|
||||
if long_name is not None and not long_name:
|
||||
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
|
||||
|
||||
if short_name is not None and not short_name:
|
||||
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
|
||||
if long_name and short_name:
|
||||
print(f"Setting device owner to {long_name} and short name to {short_name}")
|
||||
elif long_name:
|
||||
print(f"Setting device owner to {long_name}")
|
||||
elif short_name:
|
||||
print(f"Setting device owner short to {short_name}")
|
||||
|
||||
unmessagable = None
|
||||
if args.set_is_unmessageable is not None:
|
||||
unmessagable = (
|
||||
meshtastic.util.fromStr(args.set_is_unmessageable)
|
||||
if isinstance(args.set_is_unmessageable, str)
|
||||
else args.set_is_unmessageable
|
||||
)
|
||||
print(f"Setting device owner is_unmessageable to {unmessagable}")
|
||||
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
|
||||
long_name=long_name,
|
||||
short_name=short_name,
|
||||
is_unmessagable=unmessagable
|
||||
)
|
||||
|
||||
if args.set_canned_message:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
print(f"Setting canned plugin message to {args.set_canned_message}")
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).set_canned_message(
|
||||
args.set_canned_message
|
||||
)
|
||||
node = interface.getNode(args.dest, False, **getNode_kwargs)
|
||||
if node.module_available(mesh_pb2.CANNEDMSG_CONFIG):
|
||||
print(f"Setting canned plugin message to {args.set_canned_message}")
|
||||
node.set_canned_message(args.set_canned_message)
|
||||
else:
|
||||
print("Canned Message module is excluded by firmware; skipping set.")
|
||||
|
||||
# TODO: add to export-config and configure
|
||||
if args.set_ringtone:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
print(f"Setting ringtone to {args.set_ringtone}")
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).set_ringtone(args.set_ringtone)
|
||||
node = interface.getNode(args.dest, False, **getNode_kwargs)
|
||||
if node.module_available(mesh_pb2.EXTNOTIF_CONFIG):
|
||||
print(f"Setting ringtone to {args.set_ringtone}")
|
||||
node.set_ringtone(args.set_ringtone)
|
||||
else:
|
||||
print("External Notification is excluded by firmware; skipping ringtone set.")
|
||||
|
||||
if args.pos_fields:
|
||||
# If --pos-fields invoked with args, set position fields
|
||||
@@ -402,6 +440,8 @@ def onConnected(interface):
|
||||
print(" ".join(fieldNames))
|
||||
|
||||
if args.set_ham:
|
||||
if not args.set_ham.strip():
|
||||
meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters")
|
||||
closeNow = True
|
||||
print(f"Setting Ham ID to {args.set_ham} and turning off encryption")
|
||||
interface.getNode(args.dest, **getNode_kwargs).setOwner(args.set_ham, is_licensed=True)
|
||||
@@ -418,6 +458,41 @@ def onConnected(interface):
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).rebootOTA()
|
||||
|
||||
if args.ota_update:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
|
||||
if not isinstance(interface, meshtastic.tcp_interface.TCPInterface):
|
||||
meshtastic.util.our_exit(
|
||||
"Error: OTA update currently requires a TCP connection to the node (use --host)."
|
||||
)
|
||||
|
||||
ota = meshtastic.ota.ESP32WiFiOTA(args.ota_update, interface.hostname)
|
||||
|
||||
print(f"Triggering OTA update on {interface.hostname}...")
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).startOTA(
|
||||
ota_mode=admin_pb2.OTAMode.OTA_WIFI,
|
||||
ota_file_hash=ota.hash_bytes()
|
||||
)
|
||||
|
||||
print("Waiting for device to reboot into OTA mode...")
|
||||
time.sleep(5)
|
||||
|
||||
retries = 5
|
||||
while retries > 0:
|
||||
try:
|
||||
ota.update()
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
retries -= 1
|
||||
if retries == 0:
|
||||
meshtastic.util.our_exit(f"\nOTA update failed: {e}")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
print("\nOTA update completed successfully!")
|
||||
|
||||
if args.enter_dfu:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
@@ -477,6 +552,13 @@ def onConnected(interface):
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).resetNodeDb()
|
||||
|
||||
if args.add_contact:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).addContactURL(
|
||||
args.add_contact
|
||||
)
|
||||
|
||||
if args.sendtext:
|
||||
closeNow = True
|
||||
channelIndex = mt_config.channel_index or 0
|
||||
@@ -578,7 +660,7 @@ def onConnected(interface):
|
||||
time.sleep(1)
|
||||
if interface.gotResponse:
|
||||
break
|
||||
logging.debug(f"end of gpio_rd")
|
||||
logger.debug(f"end of gpio_rd")
|
||||
|
||||
if args.gpio_watch:
|
||||
bitmask = int(args.gpio_watch, 16)
|
||||
@@ -644,11 +726,20 @@ def onConnected(interface):
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction()
|
||||
|
||||
if "owner" in configuration:
|
||||
# Validate owner name before setting
|
||||
owner_name = str(configuration["owner"]).strip()
|
||||
if not owner_name:
|
||||
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
|
||||
print(f"Setting device owner to {configuration['owner']}")
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(configuration["owner"])
|
||||
time.sleep(0.5)
|
||||
|
||||
if "owner_short" in configuration:
|
||||
# Validate owner short name before setting
|
||||
owner_short_name = str(configuration["owner_short"]).strip()
|
||||
if not owner_short_name:
|
||||
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
print(
|
||||
f"Setting device owner short to {configuration['owner_short']}"
|
||||
)
|
||||
@@ -656,8 +747,13 @@ def onConnected(interface):
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
|
||||
long_name=None, short_name=configuration["owner_short"]
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "ownerShort" in configuration:
|
||||
# Validate owner short name before setting
|
||||
owner_short_name = str(configuration["ownerShort"]).strip()
|
||||
if not owner_short_name:
|
||||
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
print(
|
||||
f"Setting device owner short to {configuration['ownerShort']}"
|
||||
)
|
||||
@@ -665,14 +761,27 @@ def onConnected(interface):
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
|
||||
long_name=None, short_name=configuration["ownerShort"]
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "channel_url" in configuration:
|
||||
print("Setting channel url to", configuration["channel_url"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channel_url"])
|
||||
time.sleep(0.5)
|
||||
|
||||
if "channelUrl" in configuration:
|
||||
print("Setting channel url to", configuration["channelUrl"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channelUrl"])
|
||||
time.sleep(0.5)
|
||||
|
||||
if "canned_messages" in configuration:
|
||||
print("Setting canned message messages to", configuration["canned_messages"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).set_canned_message(configuration["canned_messages"])
|
||||
time.sleep(0.5)
|
||||
|
||||
if "ringtone" in configuration:
|
||||
print("Setting ringtone to", configuration["ringtone"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).set_ringtone(configuration["ringtone"])
|
||||
time.sleep(0.5)
|
||||
|
||||
if "location" in configuration:
|
||||
alt = 0
|
||||
@@ -691,6 +800,7 @@ def onConnected(interface):
|
||||
print(f"Fixing longitude at {lon} degrees")
|
||||
print("Setting device position")
|
||||
interface.localNode.setFixedPosition(lat, lon, alt)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "config" in configuration:
|
||||
localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig
|
||||
@@ -701,6 +811,7 @@ def onConnected(interface):
|
||||
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
|
||||
meshtastic.util.camel_to_snake(section)
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "module_config" in configuration:
|
||||
moduleConfig = interface.getNode(args.dest, **getNode_kwargs).moduleConfig
|
||||
@@ -713,6 +824,7 @@ def onConnected(interface):
|
||||
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
|
||||
meshtastic.util.camel_to_snake(section)
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction()
|
||||
print("Writing modified configuration to device")
|
||||
@@ -721,9 +833,20 @@ def onConnected(interface):
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
print("Exporting configuration of remote nodes is not supported.")
|
||||
return
|
||||
# export the configuration (the opposite of '--configure')
|
||||
|
||||
closeNow = True
|
||||
export_config(interface)
|
||||
config_txt = export_config(interface)
|
||||
|
||||
if args.export_config == "-":
|
||||
# Output to stdout (preserves legacy use of `> file.yaml`)
|
||||
print(config_txt)
|
||||
else:
|
||||
try:
|
||||
with open(args.export_config, "w", encoding="utf-8") as f:
|
||||
f.write(config_txt)
|
||||
print(f"Exported configuration to {args.export_config}")
|
||||
except Exception as e:
|
||||
meshtastic.util.our_exit(f"ERROR: Failed to write config file: {e}")
|
||||
|
||||
if args.ch_set_url:
|
||||
closeNow = True
|
||||
@@ -894,12 +1017,14 @@ def onConnected(interface):
|
||||
if args.get_canned_message:
|
||||
closeNow = True
|
||||
print("")
|
||||
interface.getNode(args.dest, **getNode_kwargs).get_canned_message()
|
||||
messages = interface.getNode(args.dest, **getNode_kwargs).get_canned_message()
|
||||
print(f"canned_plugin_message:{messages}")
|
||||
|
||||
if args.get_ringtone:
|
||||
closeNow = True
|
||||
print("")
|
||||
interface.getNode(args.dest, **getNode_kwargs).get_ringtone()
|
||||
ringtone = interface.getNode(args.dest, **getNode_kwargs).get_ringtone()
|
||||
print(f"ringtone:{ringtone}")
|
||||
|
||||
if args.info:
|
||||
print("")
|
||||
@@ -956,6 +1081,20 @@ def onConnected(interface):
|
||||
else:
|
||||
print("Install pyqrcode to view a QR code printed to terminal.")
|
||||
|
||||
if args.contact_qr:
|
||||
closeNow = True
|
||||
url = interface.getNode(args.dest, True, **getNode_kwargs).getContactURL(
|
||||
args.contact_qr,
|
||||
should_ignore=args.contact_ignore,
|
||||
manually_verified=args.contact_verified,
|
||||
)
|
||||
print(f"Contact URL: {url}")
|
||||
if pyqrcode is not None:
|
||||
qr = pyqrcode.create(url)
|
||||
print(qr.terminal())
|
||||
else:
|
||||
print("Install pyqrcode to view a QR code printed to terminal.")
|
||||
|
||||
log_set: Optional = None # type: ignore[annotation-unchecked]
|
||||
# we need to keep a reference to the logset so it doesn't get GCed early
|
||||
|
||||
@@ -991,7 +1130,7 @@ def onConnected(interface):
|
||||
# Even if others said we could close, stay open if the user asked for a tunnel
|
||||
closeNow = False
|
||||
if interface.noProto:
|
||||
logging.warning(f"Not starting Tunnel - disabled by noProto")
|
||||
logger.warning(f"Not starting Tunnel - disabled by noProto")
|
||||
else:
|
||||
if args.tunnel_net:
|
||||
tunnel.Tunnel(interface, subnet=args.tunnel_net)
|
||||
@@ -1054,15 +1193,49 @@ def subscribe() -> None:
|
||||
|
||||
# pub.subscribe(onNode, "meshtastic.node")
|
||||
|
||||
def _is_repeated_field(field_desc) -> bool:
|
||||
"""Return True if the protobuf field is repeated.
|
||||
Protobuf 6.31.0 and later use an is_repeated property, while older versions compare against the label field.
|
||||
"""
|
||||
if hasattr(field_desc, "is_repeated"):
|
||||
return bool(field_desc.is_repeated)
|
||||
return field_desc.label == field_desc.LABEL_REPEATED
|
||||
|
||||
def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str]]) -> None:
|
||||
"""Ensure that missing default=True keys are present in the config_dict and set to False."""
|
||||
for path in true_defaults:
|
||||
d = config_dict
|
||||
for key in path[:-1]:
|
||||
if key not in d or not isinstance(d[key], dict):
|
||||
d[key] = {}
|
||||
d = d[key]
|
||||
if path[-1] not in d:
|
||||
d[path[-1]] = False
|
||||
|
||||
def export_config(interface) -> str:
|
||||
"""used in --export-config"""
|
||||
configObj = {}
|
||||
|
||||
# A list of configuration keys that should be set to False if they are missing
|
||||
config_true_defaults = {
|
||||
("bluetooth", "enabled"),
|
||||
("lora", "sx126xRxBoostedGain"),
|
||||
("lora", "txEnabled"),
|
||||
("lora", "usePreset"),
|
||||
("position", "positionBroadcastSmartEnabled"),
|
||||
("security", "serialEnabled"),
|
||||
}
|
||||
|
||||
module_true_defaults = {
|
||||
("mqtt", "encryptionEnabled"),
|
||||
}
|
||||
|
||||
owner = interface.getLongName()
|
||||
owner_short = interface.getShortName()
|
||||
channel_url = interface.localNode.getURL()
|
||||
myinfo = interface.getMyNodeInfo()
|
||||
canned_messages = interface.getCannedMessage()
|
||||
ringtone = interface.getRingtone()
|
||||
pos = myinfo.get("position")
|
||||
lat = None
|
||||
lon = None
|
||||
@@ -1081,6 +1254,10 @@ def export_config(interface) -> str:
|
||||
configObj["channelUrl"] = channel_url
|
||||
else:
|
||||
configObj["channel_url"] = channel_url
|
||||
if canned_messages:
|
||||
configObj["canned_messages"] = canned_messages
|
||||
if ringtone:
|
||||
configObj["ringtone"] = ringtone
|
||||
# lat and lon don't make much sense without the other (so fill with 0s), and alt isn't meaningful without both
|
||||
if lat or lon:
|
||||
configObj["location"] = {"lat": lat or float(0), "lon": lon or float(0)}
|
||||
@@ -1088,6 +1265,7 @@ def export_config(interface) -> str:
|
||||
configObj["location"]["alt"] = alt
|
||||
|
||||
config = MessageToDict(interface.localNode.localConfig) #checkme - Used as a dictionary here and a string below
|
||||
#was used as a string here and a Dictionary above
|
||||
if config:
|
||||
# Convert inner keys to correct snake/camelCase
|
||||
prefs = {}
|
||||
@@ -1110,6 +1288,8 @@ def export_config(interface) -> str:
|
||||
else:
|
||||
configObj["config"] = config
|
||||
|
||||
set_missing_flags_false(configObj["config"], config_true_defaults)
|
||||
|
||||
module_config = MessageToDict(interface.localNode.moduleConfig)
|
||||
if module_config:
|
||||
# Convert inner keys to correct snake/camelCase
|
||||
@@ -1122,10 +1302,11 @@ def export_config(interface) -> str:
|
||||
else:
|
||||
configObj["module_config"] = prefs
|
||||
|
||||
set_missing_flags_false(configObj["module_config"], module_true_defaults)
|
||||
|
||||
config_txt = "# start of Meshtastic configure yaml\n" #checkme - "config" (now changed to config_out)
|
||||
#was used as a string here and a Dictionary above
|
||||
config_txt += yaml.dump(configObj)
|
||||
print(config_txt)
|
||||
return config_txt
|
||||
|
||||
|
||||
@@ -1153,14 +1334,14 @@ def create_power_meter():
|
||||
meter = SimPowerSupply()
|
||||
|
||||
if meter and v:
|
||||
logging.info(f"Setting power supply to {v} volts")
|
||||
logger.info(f"Setting power supply to {v} volts")
|
||||
meter.v = v
|
||||
meter.powerOn()
|
||||
|
||||
if args.power_wait:
|
||||
input("Powered on, press enter to continue...")
|
||||
else:
|
||||
logging.info("Powered-on, waiting for device to boot")
|
||||
logger.info("Powered-on, waiting for device to boot")
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
@@ -1174,6 +1355,10 @@ def common():
|
||||
format="%(levelname)s file:%(filename)s %(funcName)s line:%(lineno)s %(message)s",
|
||||
)
|
||||
|
||||
# set all meshtastic loggers to DEBUG
|
||||
if not (args.debug or args.listen) and args.debuglib:
|
||||
logging.getLogger('meshtastic').setLevel(logging.DEBUG)
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help(sys.stderr)
|
||||
meshtastic.util.our_exit("", 1)
|
||||
@@ -1182,6 +1367,22 @@ def common():
|
||||
meshtastic.util.support_info()
|
||||
meshtastic.util.our_exit("", 0)
|
||||
|
||||
# Early validation for owner names before attempting device connection
|
||||
if hasattr(args, 'set_owner') and args.set_owner is not None:
|
||||
stripped_long_name = args.set_owner.strip()
|
||||
if not stripped_long_name:
|
||||
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
|
||||
|
||||
if hasattr(args, 'set_owner_short') and args.set_owner_short is not None:
|
||||
stripped_short_name = args.set_owner_short.strip()
|
||||
if not stripped_short_name:
|
||||
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
|
||||
if hasattr(args, 'set_ham') and args.set_ham is not None:
|
||||
stripped_ham_name = args.set_ham.strip()
|
||||
if not stripped_ham_name:
|
||||
meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters")
|
||||
|
||||
if have_powermon:
|
||||
create_power_meter()
|
||||
|
||||
@@ -1199,7 +1400,7 @@ def common():
|
||||
args.seriallog = "none" # assume no debug output in this case
|
||||
|
||||
if args.deprecated is not None:
|
||||
logging.error(
|
||||
logger.error(
|
||||
"This option has been deprecated, see help below for the correct replacement..."
|
||||
)
|
||||
parser.print_help(sys.stderr)
|
||||
@@ -1218,10 +1419,10 @@ def common():
|
||||
logfile = sys.stdout
|
||||
elif args.seriallog == "none":
|
||||
args.seriallog = None
|
||||
logging.debug("Not logging serial output")
|
||||
logger.debug("Not logging serial output")
|
||||
logfile = None
|
||||
else:
|
||||
logging.info(f"Logging serial output to {args.seriallog}")
|
||||
logger.info(f"Logging serial output to {args.seriallog}")
|
||||
# Note: using "line buffering"
|
||||
# pylint: disable=R1732
|
||||
logfile = open(args.seriallog, "w+", buffering=1, encoding="utf8")
|
||||
@@ -1229,17 +1430,64 @@ def common():
|
||||
|
||||
subscribe()
|
||||
if args.ble_scan:
|
||||
logging.debug("BLE scan starting")
|
||||
logger.debug("BLE scan starting")
|
||||
for x in BLEInterface.scan():
|
||||
print(f"Found: name='{x.name}' address='{x.address}'")
|
||||
meshtastic.util.our_exit("BLE scan finished", 0)
|
||||
elif args.ble:
|
||||
client = BLEInterface(
|
||||
args.ble if args.ble != "any" else None,
|
||||
debugOut=logfile,
|
||||
noProto=args.noproto,
|
||||
noNodes=args.no_nodes,
|
||||
)
|
||||
try:
|
||||
client = BLEInterface(
|
||||
args.ble if args.ble != "any" else None,
|
||||
debugOut=logfile,
|
||||
noProto=args.noproto,
|
||||
noNodes=args.no_nodes,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
except BLEInterface.BLEError as e:
|
||||
if e.kind == BLEInterface.BLEError.DEVICE_NOT_FOUND:
|
||||
meshtastic.util.our_exit(
|
||||
"BLE device not found.\n\n"
|
||||
"Possible causes:\n"
|
||||
" - Bluetooth is disabled on the Meshtastic device\n"
|
||||
" - Device is in deep sleep mode\n"
|
||||
" - Device is out of range\n\n"
|
||||
"Try:\n"
|
||||
" - Press the reset button on your device\n"
|
||||
" - Run 'meshtastic --ble-scan' to see available devices",
|
||||
1,
|
||||
)
|
||||
elif e.kind == BLEInterface.BLEError.MULTIPLE_DEVICES:
|
||||
meshtastic.util.our_exit(
|
||||
"Multiple Meshtastic BLE devices found.\n\n"
|
||||
"Please specify which device to connect to:\n"
|
||||
" - Run 'meshtastic --ble-scan' to list devices\n"
|
||||
" - Use 'meshtastic --ble <name_or_address>' to connect",
|
||||
1,
|
||||
)
|
||||
elif e.kind == BLEInterface.BLEError.WRITE_ERROR:
|
||||
meshtastic.util.our_exit(
|
||||
"Failed to write to BLE device.\n\n"
|
||||
"Possible causes:\n"
|
||||
" - Device requires pairing PIN (check device screen)\n"
|
||||
" - On Linux: user not in 'bluetooth' group\n"
|
||||
" - Connection was interrupted\n\n"
|
||||
"Try:\n"
|
||||
" - Restart Bluetooth on your computer\n"
|
||||
" - Reset the Meshtastic device",
|
||||
1,
|
||||
)
|
||||
elif e.kind == BLEInterface.BLEError.READ_ERROR:
|
||||
meshtastic.util.our_exit(
|
||||
"Failed to read from BLE device.\n\n"
|
||||
"The device may have disconnected unexpectedly.\n\n"
|
||||
"Try:\n"
|
||||
" - Move closer to the device\n"
|
||||
" - Reset the Meshtastic device\n"
|
||||
" - Restart Bluetooth on your computer",
|
||||
1,
|
||||
)
|
||||
else:
|
||||
meshtastic.util.our_exit(f"BLE error: {e}", 1)
|
||||
elif args.host:
|
||||
try:
|
||||
if ":" in args.host:
|
||||
@@ -1253,6 +1501,7 @@ def common():
|
||||
debugOut=logfile,
|
||||
noProto=args.noproto,
|
||||
noNodes=args.no_nodes,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
except Exception as ex:
|
||||
meshtastic.util.our_exit(f"Error connecting to {args.host}:{ex}", 1)
|
||||
@@ -1263,6 +1512,7 @@ def common():
|
||||
debugOut=logfile,
|
||||
noProto=args.noproto,
|
||||
noNodes=args.no_nodes,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
# Handle the case where the serial device is not found
|
||||
@@ -1293,6 +1543,23 @@ def common():
|
||||
message += " Please close any applications or webpages that may be using the device and try again.\n"
|
||||
message += f"\nOriginal error: {ex}"
|
||||
meshtastic.util.our_exit(message)
|
||||
except MeshInterface.MeshInterfaceError as ex:
|
||||
msg = str(ex)
|
||||
if "Timed out" in msg:
|
||||
meshtastic.util.our_exit(
|
||||
"Connection timed out.\n\n"
|
||||
"Possible causes:\n"
|
||||
" - Device is rebooting\n"
|
||||
" - Device firmware is updating\n"
|
||||
" - Serial connection was interrupted\n\n"
|
||||
"Try:\n"
|
||||
" - Wait a few seconds and try again\n"
|
||||
" - Check if device is fully booted (LED patterns)\n"
|
||||
" - Reconnect the USB cable",
|
||||
1,
|
||||
)
|
||||
else:
|
||||
meshtastic.util.our_exit(f"Connection error: {ex}", 1)
|
||||
if client.devPath is None:
|
||||
try:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
@@ -1300,6 +1567,7 @@ def common():
|
||||
debugOut=logfile,
|
||||
noProto=args.noproto,
|
||||
noNodes=args.no_nodes,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
except Exception as ex:
|
||||
meshtastic.util.our_exit(
|
||||
@@ -1320,7 +1588,7 @@ def common():
|
||||
while True:
|
||||
time.sleep(1000)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Exiting due to keyboard interrupt")
|
||||
logger.info("Exiting due to keyboard interrupt")
|
||||
|
||||
# don't call exit, background threads might be running still
|
||||
# sys.exit(0)
|
||||
@@ -1409,8 +1677,10 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
|
||||
)
|
||||
group.add_argument(
|
||||
"--export-config",
|
||||
help="Export the configuration in yaml(.yml) format.",
|
||||
action="store_true",
|
||||
nargs="?",
|
||||
const="-", # default to "-" if no value provided
|
||||
metavar="FILE",
|
||||
help="Export device config as YAML (to stdout if no file given)"
|
||||
)
|
||||
return parser
|
||||
|
||||
@@ -1532,6 +1802,11 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
||||
"--set-ham", help="Set licensed Ham ID and turn off encryption", action="store"
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--set-is-unmessageable", "--set-is-unmessagable",
|
||||
help="Set if a node is messageable or not", action="store"
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-set-url", "--seturl",
|
||||
help="Set all channels and set LoRa config from a supplied URL",
|
||||
@@ -1604,6 +1879,24 @@ def addChannelConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPa
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--contact-qr",
|
||||
help="Display a QR code for a node's contact data. "
|
||||
"Use the node ID with a '!' or '0x' prefix or the node number. "
|
||||
"Also shows the shareable contact URL.",
|
||||
metavar="!xxxxxxxx",
|
||||
)
|
||||
group.add_argument(
|
||||
"--contact-verified",
|
||||
help="Set the IS_KEY_MANUALLY_VERIFIED bit in the generated contact URL",
|
||||
action="store_true",
|
||||
)
|
||||
group.add_argument(
|
||||
"--contact-ignore",
|
||||
help="Mark this contact as blocked/ignored in the generated contact URL",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-enable",
|
||||
help="Enable the specified channel. Use --ch-add instead whenever possible.",
|
||||
@@ -1741,7 +2034,10 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--reply", help="Reply to received messages", action="store_true"
|
||||
"--reply",
|
||||
help="Reply to received messages on the channel they were received. "
|
||||
"If '--ch-index' is set, only messages on that channel are replied to.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
return parser
|
||||
@@ -1762,10 +2058,18 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
||||
|
||||
group.add_argument(
|
||||
"--reboot-ota",
|
||||
help="Tell the destination node to reboot into factory firmware (ESP32)",
|
||||
help="Tell the destination node to reboot into factory firmware (ESP32, firmware version <2.7.18)",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ota-update",
|
||||
help="Perform an OTA update on the local node (ESP32, firmware version >=2.7.18, WiFi/TCP only for now). "
|
||||
"Specify the path to the firmware file.",
|
||||
metavar="FIRMWARE_FILE",
|
||||
action="store",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--enter-dfu",
|
||||
help="Tell the destination node to enter DFU mode (NRF52)",
|
||||
@@ -1830,6 +2134,13 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--add-contact",
|
||||
help="Add a contact (User) to the NodeDB from a shareable URL. "
|
||||
"Example: https://meshtastic.org/v/#<base64>",
|
||||
metavar="URL",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--set-time",
|
||||
help="Set the time to the provided unix epoch timestamp, or the system's current time if omitted or 0.",
|
||||
@@ -1920,6 +2231,10 @@ def initParser():
|
||||
"--debug", help="Show API library debug log messages", action="store_true"
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--debuglib", help="Show only API library debug log messages", action="store_true"
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--test",
|
||||
help="Run stress test against all connected Meshtastic devices",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from typing import cast, List
|
||||
|
||||
import dash_bootstrap_components as dbc # type: ignore[import-untyped]
|
||||
@@ -143,8 +144,8 @@ def create_dash(slog_path: str) -> Dash:
|
||||
"""
|
||||
app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
|
||||
|
||||
dpwr = read_pandas(f"{slog_path}/power.feather")
|
||||
dslog = read_pandas(f"{slog_path}/slog.feather")
|
||||
dpwr = read_pandas(os.path.join(slog_path, "power.feather"))
|
||||
dslog = read_pandas(os.path.join(slog_path, "slog.feather"))
|
||||
|
||||
pmon_raises = get_pmon_raises(dslog)
|
||||
|
||||
@@ -190,7 +191,7 @@ def main():
|
||||
parser = create_argparser()
|
||||
args = parser.parse_args()
|
||||
if not args.slog:
|
||||
args.slog = f"{root_dir()}/latest"
|
||||
args.slog = os.path.join(root_dir(), "latest")
|
||||
|
||||
app = create_dash(slog_path=args.slog)
|
||||
port = 8051
|
||||
|
||||
@@ -4,9 +4,10 @@ import asyncio
|
||||
import atexit
|
||||
import logging
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import io
|
||||
from threading import Thread
|
||||
from threading import Thread, Event
|
||||
from typing import List, Optional
|
||||
|
||||
import google.protobuf
|
||||
@@ -23,6 +24,7 @@ FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
|
||||
FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
|
||||
LEGACY_LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2"
|
||||
LOGRADIO_UUID = "5a3d6e49-06e6-4423-9944-e9de8cdf9547"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BLEInterface(MeshInterface):
|
||||
@@ -31,32 +33,43 @@ class BLEInterface(MeshInterface):
|
||||
class BLEError(Exception):
|
||||
"""An exception class for BLE errors."""
|
||||
|
||||
def __init__(
|
||||
DEVICE_NOT_FOUND = "device_not_found"
|
||||
MULTIPLE_DEVICES = "multiple_devices"
|
||||
READ_ERROR = "read_error"
|
||||
WRITE_ERROR = "write_error"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
def __init__(self, message: str, kind: str = UNKNOWN):
|
||||
super().__init__(message)
|
||||
self.kind = kind
|
||||
|
||||
def __init__( # pylint: disable=R0917
|
||||
self,
|
||||
address: Optional[str],
|
||||
noProto: bool = False,
|
||||
debugOut: Optional[io.TextIOWrapper]=None,
|
||||
noNodes: bool = False,
|
||||
timeout: int = 300,
|
||||
) -> None:
|
||||
MeshInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes
|
||||
self, debugOut=debugOut, noProto=noProto, noNodes=noNodes, timeout=timeout
|
||||
)
|
||||
|
||||
self.should_read = False
|
||||
|
||||
logging.debug("Threads starting")
|
||||
logger.debug("Threads starting")
|
||||
self._want_receive = True
|
||||
self._receiveThread: Optional[Thread] = Thread(
|
||||
target=self._receiveFromRadioImpl, name="BLEReceive", daemon=True
|
||||
)
|
||||
self._receiveThread.start()
|
||||
logging.debug("Threads running")
|
||||
logger.debug("Threads running")
|
||||
|
||||
self.client: Optional[BLEClient] = None
|
||||
try:
|
||||
logging.debug(f"BLE connecting to: {address if address else 'any'}")
|
||||
logger.debug(f"BLE connecting to: {address if address else 'any'}")
|
||||
self.client = self.connect(address)
|
||||
logging.debug("BLE connected")
|
||||
logger.debug("BLE connected")
|
||||
except BLEInterface.BLEError as e:
|
||||
self.close()
|
||||
raise e
|
||||
@@ -69,13 +82,13 @@ class BLEInterface(MeshInterface):
|
||||
if self.client.has_characteristic(LOGRADIO_UUID):
|
||||
self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
|
||||
|
||||
logging.debug("Mesh configure starting")
|
||||
logger.debug("Mesh configure starting")
|
||||
self._startConfig()
|
||||
if not self.noProto:
|
||||
self._waitConnected(timeout=60.0)
|
||||
self.waitForConfig()
|
||||
|
||||
logging.debug("Register FROMNUM notify callback")
|
||||
logger.debug("Register FROMNUM notify callback")
|
||||
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
|
||||
|
||||
# We MUST run atexit (if we can) because otherwise (at least on linux) the BLE device is not disconnected
|
||||
@@ -83,12 +96,23 @@ class BLEInterface(MeshInterface):
|
||||
# Note: the on disconnected callback will call our self.close which will make us nicely wait for threads to exit
|
||||
self._exit_handler = atexit.register(self.client.disconnect)
|
||||
|
||||
def __repr__(self):
|
||||
rep = f"BLEInterface(address={self.client.address if self.client else None!r}"
|
||||
if self.debugOut is not None:
|
||||
rep += f", debugOut={self.debugOut!r}"
|
||||
if self.noProto:
|
||||
rep += ", noProto=True"
|
||||
if self.noNodes:
|
||||
rep += ", noNodes=True"
|
||||
rep += ")"
|
||||
return rep
|
||||
|
||||
def from_num_handler(self, _, b: bytes) -> None: # pylint: disable=C0116
|
||||
"""Handle callbacks for fromnum notify.
|
||||
Note: this method does not need to be async because it is just setting a bool.
|
||||
"""
|
||||
from_num = struct.unpack("<I", bytes(b))[0]
|
||||
logging.debug(f"FROMNUM notify: {from_num}")
|
||||
logger.debug(f"FROMNUM notify: {from_num}")
|
||||
self.should_read = True
|
||||
|
||||
async def log_radio_handler(self, _, b): # pylint: disable=C0116
|
||||
@@ -103,7 +127,7 @@ class BLEInterface(MeshInterface):
|
||||
)
|
||||
self._handleLogLine(message)
|
||||
except google.protobuf.message.DecodeError:
|
||||
logging.warning("Malformed LogRecord received. Skipping.")
|
||||
logger.warning("Malformed LogRecord received. Skipping.")
|
||||
|
||||
async def legacy_log_radio_handler(self, _, b): # pylint: disable=C0116
|
||||
log_radio = b.decode("utf-8").replace("\n", "")
|
||||
@@ -113,7 +137,7 @@ class BLEInterface(MeshInterface):
|
||||
def scan() -> List[BLEDevice]:
|
||||
"""Scan for available BLE devices."""
|
||||
with BLEClient() as client:
|
||||
logging.info("Scanning for BLE devices (takes 10 seconds)...")
|
||||
logger.info("Scanning for BLE devices (takes 10 seconds)...")
|
||||
response = client.discover(
|
||||
timeout=10, return_adv=True, service_uuids=[SERVICE_UUID]
|
||||
)
|
||||
@@ -143,11 +167,13 @@ class BLEInterface(MeshInterface):
|
||||
|
||||
if len(addressed_devices) == 0:
|
||||
raise BLEInterface.BLEError(
|
||||
f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it."
|
||||
f"No Meshtastic BLE peripheral with identifier or address '{address}' found. Try --ble-scan to find it.",
|
||||
BLEInterface.BLEError.DEVICE_NOT_FOUND,
|
||||
)
|
||||
if len(addressed_devices) > 1:
|
||||
raise BLEInterface.BLEError(
|
||||
f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found."
|
||||
f"More than one Meshtastic BLE peripheral with identifier or address '{address}' found.",
|
||||
BLEInterface.BLEError.MULTIPLE_DEVICES,
|
||||
)
|
||||
return addressed_devices[0]
|
||||
|
||||
@@ -163,7 +189,7 @@ class BLEInterface(MeshInterface):
|
||||
|
||||
# Bleak docs recommend always doing a scan before connecting (even if we know addr)
|
||||
device = self.find_device(address)
|
||||
client = BLEClient(device.address, disconnected_callback=lambda _: self.close)
|
||||
client = BLEClient(device.address, disconnected_callback=lambda _: self.close())
|
||||
client.connect()
|
||||
client.discover()
|
||||
return client
|
||||
@@ -175,29 +201,32 @@ class BLEInterface(MeshInterface):
|
||||
retries: int = 0
|
||||
while self._want_receive:
|
||||
if self.client is None:
|
||||
logging.debug(f"BLE client is None, shutting down")
|
||||
logger.debug(f"BLE client is None, shutting down")
|
||||
self._want_receive = False
|
||||
continue
|
||||
try:
|
||||
b = bytes(self.client.read_gatt_char(FROMRADIO_UUID))
|
||||
except BleakDBusError as e:
|
||||
# Device disconnected probably, so end our read loop immediately
|
||||
logging.debug(f"Device disconnected, shutting down {e}")
|
||||
logger.debug(f"Device disconnected, shutting down {e}")
|
||||
self._want_receive = False
|
||||
except BleakError as e:
|
||||
# We were definitely disconnected
|
||||
if "Not connected" in str(e):
|
||||
logging.debug(f"Device disconnected, shutting down {e}")
|
||||
logger.debug(f"Device disconnected, shutting down {e}")
|
||||
self._want_receive = False
|
||||
else:
|
||||
raise BLEInterface.BLEError("Error reading BLE") from e
|
||||
raise BLEInterface.BLEError(
|
||||
"Error reading BLE",
|
||||
BLEInterface.BLEError.READ_ERROR,
|
||||
) from e
|
||||
if not b:
|
||||
if retries < 5:
|
||||
time.sleep(0.1)
|
||||
retries += 1
|
||||
continue
|
||||
break
|
||||
logging.debug(f"FROMRADIO read: {b.hex()}")
|
||||
logger.debug(f"FROMRADIO read: {b.hex()}")
|
||||
self._handleFromRadio(b)
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
@@ -205,7 +234,7 @@ class BLEInterface(MeshInterface):
|
||||
def _sendToRadioImpl(self, toRadio) -> None:
|
||||
b: bytes = toRadio.SerializeToString()
|
||||
if b and self.client: # we silently ignore writes while we are shutting down
|
||||
logging.debug(f"TORADIO write: {b.hex()}")
|
||||
logger.debug(f"TORADIO write: {b.hex()}")
|
||||
try:
|
||||
self.client.write_gatt_char(
|
||||
TORADIO_UUID, b, response=True
|
||||
@@ -213,7 +242,8 @@ class BLEInterface(MeshInterface):
|
||||
# search Bleak src for org.bluez.Error.InProgress
|
||||
except Exception as e:
|
||||
raise BLEInterface.BLEError(
|
||||
"Error writing BLE (are you in the 'bluetooth' user group? did you enter the pairing PIN on your computer?)"
|
||||
"Error writing BLE (are you in the 'bluetooth' user group? did you enter the pairing PIN on your computer?)",
|
||||
BLEInterface.BLEError.WRITE_ERROR,
|
||||
) from e
|
||||
# Allow to propagate and then make sure we read
|
||||
time.sleep(0.01)
|
||||
@@ -223,7 +253,7 @@ class BLEInterface(MeshInterface):
|
||||
try:
|
||||
MeshInterface.close(self)
|
||||
except Exception as e:
|
||||
logging.error(f"Error closing mesh interface: {e}")
|
||||
logger.error(f"Error closing mesh interface: {e}")
|
||||
|
||||
if self._want_receive:
|
||||
self._want_receive = False # Tell the thread we want it to stop
|
||||
@@ -245,14 +275,16 @@ class BLEClient:
|
||||
"""Client for managing connection to a BLE device"""
|
||||
|
||||
def __init__(self, address=None, **kwargs) -> None:
|
||||
self._loop_ready = Event()
|
||||
self._eventLoop = asyncio.new_event_loop()
|
||||
self._eventThread = Thread(
|
||||
target=self._run_event_loop, name="BLEClient", daemon=True
|
||||
)
|
||||
self._eventThread.start()
|
||||
self._loop_ready.wait() # Wait for event loop to be running
|
||||
|
||||
if not address:
|
||||
logging.debug("No address provided - only discover method will work.")
|
||||
logger.debug("No address provided - only discover method will work.")
|
||||
return
|
||||
|
||||
self.bleak_client = BleakClient(address, **kwargs)
|
||||
@@ -293,13 +325,28 @@ class BLEClient:
|
||||
self.close()
|
||||
|
||||
def async_await(self, coro, timeout=None): # pylint: disable=C0116
|
||||
return self.async_run(coro).result(timeout)
|
||||
"""Wait for async operation to complete.
|
||||
|
||||
On macOS, CoreBluetooth requires occasional I/O operations for
|
||||
callbacks to be properly delivered. The debug logging provides this
|
||||
I/O when enabled, allowing the system to process pending callbacks.
|
||||
"""
|
||||
logger.debug(f"async_await: waiting for {coro}")
|
||||
future = self.async_run(coro)
|
||||
# On macOS without debug logging, callbacks may not be delivered
|
||||
# unless we trigger some I/O. This is a known quirk of CoreBluetooth.
|
||||
sys.stdout.flush()
|
||||
result = future.result(timeout)
|
||||
logger.debug("async_await: complete")
|
||||
return result
|
||||
|
||||
def async_run(self, coro): # pylint: disable=C0116
|
||||
return asyncio.run_coroutine_threadsafe(coro, self._eventLoop)
|
||||
|
||||
def _run_event_loop(self):
|
||||
try:
|
||||
# Signal ready from WITHIN the loop to guarantee it's actually running
|
||||
self._eventLoop.call_soon(self._loop_ready.set)
|
||||
self._eventLoop.run_forever()
|
||||
finally:
|
||||
self._eventLoop.close()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Mesh Interface class
|
||||
"""
|
||||
# pylint: disable=R0917
|
||||
# pylint: disable=R0917,C0302
|
||||
|
||||
import collections
|
||||
import json
|
||||
@@ -17,6 +17,7 @@ from decimal import Decimal
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
import google.protobuf.json_format
|
||||
|
||||
try:
|
||||
import print_color # type: ignore[import-untyped]
|
||||
except ImportError as e:
|
||||
@@ -46,6 +47,7 @@ from meshtastic.util import (
|
||||
stripnl,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _timeago(delta_secs: int) -> str:
|
||||
"""Convert a number of seconds in the past into a short, friendly string
|
||||
@@ -88,7 +90,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
super().__init__(self.message)
|
||||
|
||||
def __init__(
|
||||
self, debugOut=None, noProto: bool = False, noNodes: bool = False
|
||||
self, debugOut=None, noProto: bool = False, noNodes: bool = False, timeout: int = 300
|
||||
) -> None:
|
||||
"""Constructor
|
||||
|
||||
@@ -97,13 +99,14 @@ class MeshInterface: # pylint: disable=R0902
|
||||
link - just be a dumb serial client.
|
||||
noNodes -- If True, instruct the node to not send its nodedb
|
||||
on startup, just other configuration information.
|
||||
timeout -- How long to wait for replies (default: 300 seconds)
|
||||
"""
|
||||
self.debugOut = debugOut
|
||||
self.nodes: Optional[Dict[str, Dict]] = None # FIXME
|
||||
self.isConnected: threading.Event = threading.Event()
|
||||
self.noProto: bool = noProto
|
||||
self.localNode: meshtastic.node.Node = meshtastic.node.Node(
|
||||
self, -1
|
||||
self, -1, timeout=timeout
|
||||
) # We fixup nodenum later
|
||||
self.myInfo: Optional[
|
||||
mesh_pb2.MyNodeInfo
|
||||
@@ -117,7 +120,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
self.failure = (
|
||||
None # If we've encountered a fatal exception it will be kept here
|
||||
)
|
||||
self._timeout: Timeout = Timeout()
|
||||
self._timeout: Timeout = Timeout(maxSecs=timeout)
|
||||
self._acknowledgment: Acknowledgment = Acknowledgment()
|
||||
self.heartbeatTimer: Optional[threading.Timer] = None
|
||||
random.seed() # FIXME, we should not clobber the random seedval here, instead tell user they must call it
|
||||
@@ -149,11 +152,11 @@ class MeshInterface: # pylint: disable=R0902
|
||||
|
||||
def __exit__(self, exc_type, exc_value, trace):
|
||||
if exc_type is not None and exc_value is not None:
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"An exception of type {exc_type} with value {exc_value} has occurred"
|
||||
)
|
||||
if trace is not None:
|
||||
logging.error(f"Traceback: {trace}")
|
||||
logger.error(f"Traceback:\n{''.join(traceback.format_tb(trace))}")
|
||||
self.close()
|
||||
|
||||
@staticmethod
|
||||
@@ -250,6 +253,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
"channel": "Channel",
|
||||
"lastHeard": "LastHeard",
|
||||
"since": "Since",
|
||||
"isFavorite": "Fav",
|
||||
|
||||
}
|
||||
|
||||
@@ -281,7 +285,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
|
||||
def getNestedValue(node_dict: Dict[str, Any], key_path: str) -> Any:
|
||||
if key_path.index(".") < 0:
|
||||
logging.debug("getNestedValue was called without a nested path.")
|
||||
logger.debug("getNestedValue was called without a nested path.")
|
||||
return None
|
||||
keys = key_path.split(".")
|
||||
value: Optional[Union[str, dict]] = node_dict
|
||||
@@ -297,14 +301,14 @@ class MeshInterface: # pylint: disable=R0902
|
||||
showFields = ["N", "user.longName", "user.id", "user.shortName", "user.hwModel", "user.publicKey",
|
||||
"user.role", "position.latitude", "position.longitude", "position.altitude",
|
||||
"deviceMetrics.batteryLevel", "deviceMetrics.channelUtilization",
|
||||
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "lastHeard", "since"]
|
||||
"deviceMetrics.airUtilTx", "snr", "hopsAway", "channel", "isFavorite", "lastHeard", "since"]
|
||||
else:
|
||||
# Always at least include the row number.
|
||||
showFields.insert(0, "N")
|
||||
|
||||
rows: List[Dict[str, Any]] = []
|
||||
if self.nodesByNum:
|
||||
logging.debug(f"self.nodes:{self.nodes}")
|
||||
logger.debug(f"self.nodes:{self.nodes}")
|
||||
for node in self.nodesByNum.values():
|
||||
if not includeSelf and node["num"] == self.localNode.nodeNum:
|
||||
continue
|
||||
@@ -339,6 +343,8 @@ class MeshInterface: # pylint: disable=R0902
|
||||
formatted_value = "Powered"
|
||||
else:
|
||||
formatted_value = formatFloat(raw_value, 0, "%")
|
||||
elif field == "isFavorite":
|
||||
formatted_value = "*" if raw_value else ""
|
||||
elif field == "lastHeard":
|
||||
formatted_value = getLH(raw_value)
|
||||
elif field == "position.latitude":
|
||||
@@ -383,7 +389,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
n = meshtastic.node.Node(self, nodeId, timeout=timeout)
|
||||
# Only request device settings and channel info when necessary
|
||||
if requestChannels:
|
||||
logging.debug("About to requestChannels")
|
||||
logger.debug("About to requestChannels")
|
||||
n.requestChannels()
|
||||
retries_left = requestChannelAttempts
|
||||
last_index: int = 0
|
||||
@@ -411,7 +417,9 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse: bool = False,
|
||||
onResponse: Optional[Callable[[dict], Any]] = None,
|
||||
channelIndex: int = 0,
|
||||
portNum: portnums_pb2.PortNum.ValueType = portnums_pb2.PortNum.TEXT_MESSAGE_APP
|
||||
portNum: portnums_pb2.PortNum.ValueType = portnums_pb2.PortNum.TEXT_MESSAGE_APP,
|
||||
replyId: Optional[int]=None,
|
||||
hopLimit: Optional[int]=None,
|
||||
):
|
||||
"""Send a utf8 string to some other node, if the node has a display it
|
||||
will also be shown on the device.
|
||||
@@ -428,6 +436,8 @@ class MeshInterface: # pylint: disable=R0902
|
||||
send an application layer response
|
||||
portNum -- the application portnum (similar to IP port numbers)
|
||||
of the destination, see portnums.proto for a list
|
||||
replyId -- the ID of the message that this packet is a response to
|
||||
hopLimit {int} -- hop limit to use
|
||||
|
||||
Returns the sent packet. The id field will be populated in this packet
|
||||
and can be used to track future message acks/naks.
|
||||
@@ -441,6 +451,8 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
replyId=replyId,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
|
||||
|
||||
@@ -450,8 +462,9 @@ class MeshInterface: # pylint: disable=R0902
|
||||
destinationId: Union[int, str] = BROADCAST_ADDR,
|
||||
onResponse: Optional[Callable[[dict], Any]] = None,
|
||||
channelIndex: int = 0,
|
||||
hopLimit: Optional[int]=None,
|
||||
):
|
||||
"""Send an alert text to some other node. This is similar to a text message,
|
||||
"""Send an alert text to some other node. This is similar to a text message,
|
||||
but carries a higher priority and is capable of generating special notifications
|
||||
on certain clients.
|
||||
|
||||
@@ -461,6 +474,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
Keyword Arguments:
|
||||
destinationId {nodeId or nodeNum} -- where to send this
|
||||
message (default: {BROADCAST_ADDR})
|
||||
hopLimit {int} -- hop limit to use
|
||||
|
||||
Returns the sent packet. The id field will be populated in this packet
|
||||
and can be used to track future message acks/naks.
|
||||
@@ -474,9 +488,22 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=False,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
priority=mesh_pb2.MeshPacket.Priority.ALERT
|
||||
priority=mesh_pb2.MeshPacket.Priority.ALERT,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
|
||||
def sendMqttClientProxyMessage(self, topic: str, data: bytes):
|
||||
"""Send an MQTT Client Proxy message to the radio.
|
||||
|
||||
Topic and data should be the MQTT topic and the message
|
||||
payload from an MQTT broker, respectively."""
|
||||
prox = mesh_pb2.MqttClientProxyMessage()
|
||||
prox.topic = topic
|
||||
prox.data = data
|
||||
toRadio = mesh_pb2.ToRadio()
|
||||
toRadio.mqttClientProxyMessage.CopyFrom(prox)
|
||||
self._sendToRadio(toRadio)
|
||||
|
||||
def sendData(
|
||||
self,
|
||||
data,
|
||||
@@ -491,6 +518,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
pkiEncrypted: Optional[bool]=False,
|
||||
publicKey: Optional[bytes]=None,
|
||||
priority: mesh_pb2.MeshPacket.Priority.ValueType=mesh_pb2.MeshPacket.Priority.RELIABLE,
|
||||
replyId: Optional[int]=None,
|
||||
): # pylint: disable=R0913
|
||||
"""Send a data packet to some other node
|
||||
|
||||
@@ -515,17 +543,18 @@ class MeshInterface: # pylint: disable=R0902
|
||||
will implicitly be true.
|
||||
channelIndex -- channel number to use
|
||||
hopLimit -- hop limit to use
|
||||
replyId -- the ID of the message that this packet is a response to
|
||||
|
||||
Returns the sent packet. The id field will be populated in this packet
|
||||
and can be used to track future message acks/naks.
|
||||
"""
|
||||
|
||||
if getattr(data, "SerializeToString", None):
|
||||
logging.debug(f"Serializing protobuf as data: {stripnl(data)}")
|
||||
logger.debug(f"Serializing protobuf as data: {stripnl(data)}")
|
||||
data = data.SerializeToString()
|
||||
|
||||
logging.debug(f"len(data): {len(data)}")
|
||||
logging.debug(
|
||||
logger.debug(f"len(data): {len(data)}")
|
||||
logger.debug(
|
||||
f"mesh_pb2.Constants.DATA_PAYLOAD_LEN: {mesh_pb2.Constants.DATA_PAYLOAD_LEN}"
|
||||
)
|
||||
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
|
||||
@@ -542,11 +571,13 @@ class MeshInterface: # pylint: disable=R0902
|
||||
meshPacket.decoded.portnum = portNum
|
||||
meshPacket.decoded.want_response = wantResponse
|
||||
meshPacket.id = self._generatePacketId()
|
||||
if replyId is not None:
|
||||
meshPacket.decoded.reply_id = replyId
|
||||
if priority is not None:
|
||||
meshPacket.priority = priority
|
||||
|
||||
if onResponse is not None:
|
||||
logging.debug(f"Setting a response handler for requestId {meshPacket.id}")
|
||||
logger.debug(f"Setting a response handler for requestId {meshPacket.id}")
|
||||
self._addResponseHandler(meshPacket.id, onResponse, ackPermitted=onResponseAckPermitted)
|
||||
p = self._sendPacket(meshPacket, destinationId, wantAck=wantAck, hopLimit=hopLimit, pkiEncrypted=pkiEncrypted, publicKey=publicKey)
|
||||
return p
|
||||
@@ -560,6 +591,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantAck: bool = False,
|
||||
wantResponse: bool = False,
|
||||
channelIndex: int = 0,
|
||||
hopLimit: Optional[int]=None,
|
||||
):
|
||||
"""
|
||||
Send a position packet to some other node (normally a broadcast)
|
||||
@@ -573,15 +605,15 @@ class MeshInterface: # pylint: disable=R0902
|
||||
p = mesh_pb2.Position()
|
||||
if latitude != 0.0:
|
||||
p.latitude_i = int(latitude / 1e-7)
|
||||
logging.debug(f"p.latitude_i:{p.latitude_i}")
|
||||
logger.debug(f"p.latitude_i:{p.latitude_i}")
|
||||
|
||||
if longitude != 0.0:
|
||||
p.longitude_i = int(longitude / 1e-7)
|
||||
logging.debug(f"p.longitude_i:{p.longitude_i}")
|
||||
logger.debug(f"p.longitude_i:{p.longitude_i}")
|
||||
|
||||
if altitude != 0:
|
||||
p.altitude = int(altitude)
|
||||
logging.debug(f"p.altitude:{p.altitude}")
|
||||
logger.debug(f"p.altitude:{p.altitude}")
|
||||
|
||||
if wantResponse:
|
||||
onResponse = self.onResponsePosition
|
||||
@@ -596,6 +628,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForPosition()
|
||||
@@ -648,11 +681,19 @@ class MeshInterface: # pylint: disable=R0902
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
# extend timeout based on number of nodes, limit by configured hopLimit
|
||||
waitFactor = min(len(self.nodes) - 1 if self.nodes else 0, hopLimit)
|
||||
nodes_based_factor = (len(self.nodes) - 1) if self.nodes else (hopLimit + 1)
|
||||
waitFactor = max(1, min(nodes_based_factor, hopLimit + 1))
|
||||
self.waitForTraceRoute(waitFactor)
|
||||
|
||||
def onResponseTraceRoute(self, p: dict):
|
||||
"""on response for trace route"""
|
||||
if p["decoded"]["portnum"] == "ROUTING_APP":
|
||||
error = p["decoded"]["routing"]["errorReason"]
|
||||
if error != "NONE":
|
||||
print(f"Traceroute failed: {error}")
|
||||
self._acknowledgment.receivedTraceRoute = True
|
||||
return
|
||||
|
||||
UNK_SNR = -128 # Value representing unknown SNR
|
||||
|
||||
routeDiscovery = mesh_pb2.RouteDiscovery()
|
||||
@@ -701,7 +742,8 @@ class MeshInterface: # pylint: disable=R0902
|
||||
destinationId: Union[int, str] = BROADCAST_ADDR,
|
||||
wantResponse: bool = False,
|
||||
channelIndex: int = 0,
|
||||
telemetryType: str = "device_metrics"
|
||||
telemetryType: str = "device_metrics",
|
||||
hopLimit: Optional[int]=None,
|
||||
):
|
||||
"""Send telemetry and optionally ask for a response"""
|
||||
r = telemetry_pb2.Telemetry()
|
||||
@@ -748,6 +790,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForTelemetry()
|
||||
@@ -808,6 +851,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
self,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
expire: int,
|
||||
waypoint_id: Optional[int] = None,
|
||||
latitude: float = 0.0,
|
||||
@@ -816,6 +860,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantAck: bool = True,
|
||||
wantResponse: bool = False,
|
||||
channelIndex: int = 0,
|
||||
hopLimit: Optional[int]=None,
|
||||
): # pylint: disable=R0913
|
||||
"""
|
||||
Send a waypoint packet to some other node (normally a broadcast)
|
||||
@@ -826,21 +871,22 @@ class MeshInterface: # pylint: disable=R0902
|
||||
w = mesh_pb2.Waypoint()
|
||||
w.name = name
|
||||
w.description = description
|
||||
w.icon = icon
|
||||
w.expire = expire
|
||||
if waypoint_id is None:
|
||||
# Generate a waypoint's id, NOT a packet ID.
|
||||
# same algorithm as https://github.com/meshtastic/js/blob/715e35d2374276a43ffa93c628e3710875d43907/src/meshDevice.ts#L791
|
||||
seed = secrets.randbits(32)
|
||||
w.id = math.floor(seed * math.pow(2, -32) * 1e9)
|
||||
logging.debug(f"w.id:{w.id}")
|
||||
logger.debug(f"w.id:{w.id}")
|
||||
else:
|
||||
w.id = waypoint_id
|
||||
if latitude != 0.0:
|
||||
w.latitude_i = int(latitude * 1e7)
|
||||
logging.debug(f"w.latitude_i:{w.latitude_i}")
|
||||
logger.debug(f"w.latitude_i:{w.latitude_i}")
|
||||
if longitude != 0.0:
|
||||
w.longitude_i = int(longitude * 1e7)
|
||||
logging.debug(f"w.longitude_i:{w.longitude_i}")
|
||||
logger.debug(f"w.longitude_i:{w.longitude_i}")
|
||||
|
||||
if wantResponse:
|
||||
onResponse = self.onResponseWaypoint
|
||||
@@ -855,6 +901,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForWaypoint()
|
||||
@@ -867,12 +914,13 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantAck: bool = True,
|
||||
wantResponse: bool = False,
|
||||
channelIndex: int = 0,
|
||||
hopLimit: Optional[int]=None,
|
||||
):
|
||||
"""
|
||||
Send a waypoint deletion packet to some other node (normally a broadcast)
|
||||
|
||||
NB: The id must be the waypoint's id and not the id of the packet creation.
|
||||
|
||||
|
||||
Returns the sent packet. The id field will be populated in this packet and
|
||||
can be used to track future message acks/naks.
|
||||
"""
|
||||
@@ -893,6 +941,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForWaypoint()
|
||||
@@ -955,7 +1004,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
else:
|
||||
nodeNum = node["num"]
|
||||
else:
|
||||
logging.warning("Warning: There were no self.nodes.")
|
||||
logger.warning("Warning: There were no self.nodes.")
|
||||
|
||||
meshPacket.to = nodeNum
|
||||
meshPacket.want_ack = wantAck
|
||||
@@ -979,11 +1028,11 @@ class MeshInterface: # pylint: disable=R0902
|
||||
|
||||
toRadio.packet.CopyFrom(meshPacket)
|
||||
if self.noProto:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"Not sending packet because protocol use is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
logging.debug(f"Sending packet: {stripnl(meshPacket)}")
|
||||
logger.debug(f"Sending packet: {stripnl(meshPacket)}")
|
||||
self._sendToRadio(toRadio)
|
||||
return meshPacket
|
||||
|
||||
@@ -1034,7 +1083,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
"""Get info about my node."""
|
||||
if self.myInfo is None or self.nodesByNum is None:
|
||||
return None
|
||||
logging.debug(f"self.nodesByNum:{self.nodesByNum}")
|
||||
logger.debug(f"self.nodesByNum:{self.nodesByNum}")
|
||||
return self.nodesByNum.get(self.myInfo.my_node_num)
|
||||
|
||||
def getMyUser(self):
|
||||
@@ -1065,6 +1114,20 @@ class MeshInterface: # pylint: disable=R0902
|
||||
return user.get("publicKey", None)
|
||||
return None
|
||||
|
||||
def getCannedMessage(self):
|
||||
"""Get canned message"""
|
||||
node = self.localNode
|
||||
if node is not None:
|
||||
return node.get_canned_message()
|
||||
return None
|
||||
|
||||
def getRingtone(self):
|
||||
"""Get ringtone"""
|
||||
node = self.localNode
|
||||
if node is not None:
|
||||
return node.get_ringtone()
|
||||
return None
|
||||
|
||||
def _waitConnected(self, timeout=30.0):
|
||||
"""Block until the initial node db download is complete, or timeout
|
||||
and raise an exception"""
|
||||
@@ -1110,7 +1173,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
def callback():
|
||||
self.heartbeatTimer = None
|
||||
interval = 300
|
||||
logging.debug(f"Sending heartbeat, interval {interval} seconds")
|
||||
logger.debug(f"Sending heartbeat, interval {interval} seconds")
|
||||
self.heartbeatTimer = threading.Timer(interval, callback)
|
||||
self.heartbeatTimer.start()
|
||||
self.sendHeartbeat()
|
||||
@@ -1168,11 +1231,11 @@ class MeshInterface: # pylint: disable=R0902
|
||||
def _sendToRadio(self, toRadio: mesh_pb2.ToRadio) -> None:
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
if self.noProto:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"Not sending packet because protocol use is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
# logging.debug(f"Sending toRadio: {stripnl(toRadio)}")
|
||||
# logger.debug(f"Sending toRadio: {stripnl(toRadio)}")
|
||||
|
||||
if not toRadio.HasField("packet"):
|
||||
# not a meshpacket -- send immediately, give queue a chance,
|
||||
@@ -1185,38 +1248,38 @@ class MeshInterface: # pylint: disable=R0902
|
||||
resentQueue = collections.OrderedDict()
|
||||
|
||||
while self.queue:
|
||||
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
# logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
while not self._queueHasFreeSpace():
|
||||
logging.debug("Waiting for free space in TX Queue")
|
||||
logger.debug("Waiting for free space in TX Queue")
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
toResend = self.queue.popitem(last=False)
|
||||
except KeyError:
|
||||
break
|
||||
packetId, packet = toResend
|
||||
# logging.warn(f"packet: {packetId:08x} {packet}")
|
||||
# logger.warn(f"packet: {packetId:08x} {packet}")
|
||||
resentQueue[packetId] = packet
|
||||
if packet is False:
|
||||
continue
|
||||
self._queueClaim()
|
||||
if packet != toRadio:
|
||||
logging.debug(f"Resending packet ID {packetId:08x} {packet}")
|
||||
logger.debug(f"Resending packet ID {packetId:08x} {packet}")
|
||||
self._sendToRadioImpl(packet)
|
||||
|
||||
# logging.warn("resentQueue: " + " ".join(f'{k:08x}' for k in resentQueue))
|
||||
# logger.warn("resentQueue: " + " ".join(f'{k:08x}' for k in resentQueue))
|
||||
for packetId, packet in resentQueue.items():
|
||||
if (
|
||||
self.queue.pop(packetId, False) is False
|
||||
): # Packet got acked under us
|
||||
logging.debug(f"packet {packetId:08x} got acked under us")
|
||||
logger.debug(f"packet {packetId:08x} got acked under us")
|
||||
continue
|
||||
if packet:
|
||||
self.queue[packetId] = packet
|
||||
# logging.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
# logger.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
|
||||
def _sendToRadioImpl(self, toRadio: mesh_pb2.ToRadio) -> None:
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
logging.error(f"Subclass must provide toradio: {toRadio}")
|
||||
logger.error(f"Subclass must provide toradio: {toRadio}")
|
||||
|
||||
def _handleConfigComplete(self) -> None:
|
||||
"""
|
||||
@@ -1232,22 +1295,22 @@ class MeshInterface: # pylint: disable=R0902
|
||||
|
||||
def _handleQueueStatusFromRadio(self, queueStatus) -> None:
|
||||
self.queueStatus = queueStatus
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} "
|
||||
)
|
||||
|
||||
if queueStatus.res:
|
||||
return
|
||||
|
||||
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
# logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
justQueued = self.queue.pop(queueStatus.mesh_packet_id, None)
|
||||
|
||||
if justQueued is None and queueStatus.mesh_packet_id != 0:
|
||||
self.queue[queueStatus.mesh_packet_id] = False
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"Reply for unexpected packet ID {queueStatus.mesh_packet_id:08x}"
|
||||
)
|
||||
# logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
# logger.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue))
|
||||
|
||||
def _handleFromRadio(self, fromRadioBytes):
|
||||
"""
|
||||
@@ -1255,30 +1318,30 @@ class MeshInterface: # pylint: disable=R0902
|
||||
|
||||
Called by subclasses."""
|
||||
fromRadio = mesh_pb2.FromRadio()
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"in mesh_interface.py _handleFromRadio() fromRadioBytes: {fromRadioBytes}"
|
||||
)
|
||||
try:
|
||||
fromRadio.ParseFromString(fromRadioBytes)
|
||||
except Exception as ex:
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"Error while parsing FromRadio bytes:{fromRadioBytes} {ex}"
|
||||
)
|
||||
traceback.print_exc()
|
||||
raise ex
|
||||
asDict = google.protobuf.json_format.MessageToDict(fromRadio)
|
||||
logging.debug(f"Received from radio: {fromRadio}")
|
||||
logger.debug(f"Received from radio: {fromRadio}")
|
||||
if fromRadio.HasField("my_info"):
|
||||
self.myInfo = fromRadio.my_info
|
||||
self.localNode.nodeNum = self.myInfo.my_node_num
|
||||
logging.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}")
|
||||
logger.debug(f"Received myinfo: {stripnl(fromRadio.my_info)}")
|
||||
|
||||
elif fromRadio.HasField("metadata"):
|
||||
self.metadata = fromRadio.metadata
|
||||
logging.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
|
||||
logger.debug(f"Received device metadata: {stripnl(fromRadio.metadata)}")
|
||||
|
||||
elif fromRadio.HasField("node_info"):
|
||||
logging.debug(f"Received nodeinfo: {asDict['nodeInfo']}")
|
||||
logger.debug(f"Received nodeinfo: {asDict['nodeInfo']}")
|
||||
|
||||
node = self._getOrCreateByNum(asDict["nodeInfo"]["num"])
|
||||
node.update(asDict["nodeInfo"])
|
||||
@@ -1286,7 +1349,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
newpos = self._fixupPosition(node["position"])
|
||||
node["position"] = newpos
|
||||
except:
|
||||
logging.debug("Node without position")
|
||||
logger.debug("Node without position")
|
||||
|
||||
# no longer necessary since we're mutating directly in nodesByNum via _getOrCreateByNum
|
||||
# self.nodesByNum[node["num"]] = node
|
||||
@@ -1301,7 +1364,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
elif fromRadio.config_complete_id == self.configId:
|
||||
# we ignore the config_complete_id, it is unneeded for our
|
||||
# stream API fromRadio.config_complete_id
|
||||
logging.debug(f"Config complete ID {self.configId}")
|
||||
logger.debug(f"Config complete ID {self.configId}")
|
||||
self._handleConfigComplete()
|
||||
elif fromRadio.HasField("channel"):
|
||||
self._handleChannel(fromRadio.channel)
|
||||
@@ -1311,6 +1374,14 @@ class MeshInterface: # pylint: disable=R0902
|
||||
self._handleLogRecord(fromRadio.log_record)
|
||||
elif fromRadio.HasField("queueStatus"):
|
||||
self._handleQueueStatusFromRadio(fromRadio.queueStatus)
|
||||
elif fromRadio.HasField("clientNotification"):
|
||||
publishingThread.queueWork(
|
||||
lambda: pub.sendMessage(
|
||||
"meshtastic.clientNotification",
|
||||
notification=fromRadio.clientNotification,
|
||||
interface=self,
|
||||
)
|
||||
)
|
||||
|
||||
elif fromRadio.HasField("mqttClientProxyMessage"):
|
||||
publishingThread.queueWork(
|
||||
@@ -1406,9 +1477,13 @@ class MeshInterface: # pylint: disable=R0902
|
||||
self.localNode.moduleConfig.paxcounter.CopyFrom(
|
||||
fromRadio.moduleConfig.paxcounter
|
||||
)
|
||||
elif fromRadio.moduleConfig.HasField("traffic_management"):
|
||||
self.localNode.moduleConfig.traffic_management.CopyFrom(
|
||||
fromRadio.moduleConfig.traffic_management
|
||||
)
|
||||
|
||||
else:
|
||||
logging.debug("Unexpected FromRadio payload")
|
||||
logger.debug("Unexpected FromRadio payload")
|
||||
|
||||
def _fixupPosition(self, position: Dict) -> Dict:
|
||||
"""Convert integer lat/lon into floats
|
||||
@@ -1442,7 +1517,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
try:
|
||||
return self.nodesByNum[num]["user"]["id"] # type: ignore[index]
|
||||
except:
|
||||
logging.debug(f"Node {num} not found for fromId")
|
||||
logger.debug(f"Node {num} not found for fromId")
|
||||
return None
|
||||
|
||||
def _getOrCreateByNum(self, nodeNum):
|
||||
@@ -1498,7 +1573,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
# from might be missing if the nodenum was zero.
|
||||
if not hack and "from" not in asDict:
|
||||
asDict["from"] = 0
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"Device returned a packet we sent, ignoring: {stripnl(asDict)}"
|
||||
)
|
||||
print(
|
||||
@@ -1512,11 +1587,11 @@ class MeshInterface: # pylint: disable=R0902
|
||||
try:
|
||||
asDict["fromId"] = self._nodeNumToId(asDict["from"], False)
|
||||
except Exception as ex:
|
||||
logging.warning(f"Not populating fromId {ex}")
|
||||
logger.warning(f"Not populating fromId {ex}")
|
||||
try:
|
||||
asDict["toId"] = self._nodeNumToId(asDict["to"])
|
||||
except Exception as ex:
|
||||
logging.warning(f"Not populating toId {ex}")
|
||||
logger.warning(f"Not populating toId {ex}")
|
||||
|
||||
# We could provide our objects as DotMaps - which work with . notation or as dictionaries
|
||||
# asObj = DotMap(asDict)
|
||||
@@ -1536,7 +1611,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
# it to prevent confusion
|
||||
if "portnum" not in decoded:
|
||||
decoded["portnum"] = portnum
|
||||
logging.warning(f"portnum was not in decoded. Setting to:{portnum}")
|
||||
logger.warning(f"portnum was not in decoded. Setting to:{portnum}")
|
||||
else:
|
||||
portnum = decoded["portnum"]
|
||||
|
||||
@@ -1568,7 +1643,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
# Is this message in response to a request, if so, look for a handler
|
||||
requestId = decoded.get("requestId")
|
||||
if requestId is not None:
|
||||
logging.debug(f"Got a response for requestId {requestId}")
|
||||
logger.debug(f"Got a response for requestId {requestId}")
|
||||
# We ignore ACK packets unless the callback is named `onAckNak`
|
||||
# or the handler is set as ackPermitted, but send NAKs and
|
||||
# other, data-containing responses to the handlers
|
||||
@@ -1585,12 +1660,12 @@ class MeshInterface: # pylint: disable=R0902
|
||||
or handler.ackPermitted
|
||||
):
|
||||
handler = self.responseHandlers.pop(requestId, None)
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"Calling response handler for requestId {requestId}"
|
||||
)
|
||||
handler.callback(asDict)
|
||||
|
||||
logging.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
|
||||
logger.debug(f"Publishing {topic}: packet={stripnl(asDict)} ")
|
||||
publishingThread.queueWork(
|
||||
lambda: pub.sendMessage(topic, packet=asDict, interface=self)
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import time
|
||||
|
||||
from typing import Optional, Union, List
|
||||
|
||||
from meshtastic.protobuf import admin_pb2, apponly_pb2, channel_pb2, localonly_pb2, mesh_pb2, portnums_pb2
|
||||
from meshtastic.protobuf import admin_pb2, apponly_pb2, channel_pb2, config_pb2, localonly_pb2, mesh_pb2, portnums_pb2
|
||||
from meshtastic.util import (
|
||||
Timeout,
|
||||
camel_to_snake,
|
||||
@@ -16,8 +16,12 @@ from meshtastic.util import (
|
||||
pskToString,
|
||||
stripnl,
|
||||
message_to_json,
|
||||
generate_channel_hash,
|
||||
to_node_num,
|
||||
flags_to_list,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Node:
|
||||
"""A model of a (local or remote) node in the mesh
|
||||
@@ -42,11 +46,40 @@ class Node:
|
||||
|
||||
self.gotResponse = None
|
||||
|
||||
def __repr__(self):
|
||||
r = f"Node({self.iface!r}, 0x{self.nodeNum:08x}"
|
||||
if self.noProto:
|
||||
r += ", noProto=True"
|
||||
if self._timeout.expireTimeout != 300:
|
||||
r += ", timeout={self._timeout.expireTimeout!r}"
|
||||
r += ")"
|
||||
return r
|
||||
|
||||
@staticmethod
|
||||
def position_flags_list(position_flags: int) -> List[str]:
|
||||
"Return a list of position flags from the given flags integer"
|
||||
return flags_to_list(config_pb2.Config.PositionConfig.PositionFlags, position_flags)
|
||||
|
||||
@staticmethod
|
||||
def excluded_modules_list(excluded_modules: int) -> List[str]:
|
||||
"Return a list of excluded modules from the given flags integer"
|
||||
return flags_to_list(mesh_pb2.ExcludedModules, excluded_modules)
|
||||
|
||||
def module_available(self, excluded_bit: int) -> bool:
|
||||
"""Check DeviceMetadata.excluded_modules to see if a module is available."""
|
||||
meta = getattr(self.iface, "metadata", None)
|
||||
if meta is None:
|
||||
return True
|
||||
try:
|
||||
return (meta.excluded_modules & excluded_bit) == 0
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
def showChannels(self):
|
||||
"""Show human readable description of our channels."""
|
||||
print("Channels:")
|
||||
if self.channels:
|
||||
logging.debug(f"self.channels:{self.channels}")
|
||||
logger.debug(f"self.channels:{self.channels}")
|
||||
for c in self.channels:
|
||||
cStr = message_to_json(c.settings)
|
||||
# don't show disabled channels
|
||||
@@ -79,7 +112,7 @@ class Node:
|
||||
|
||||
def requestChannels(self, startingIndex: int = 0):
|
||||
"""Send regular MeshPackets to ask channels."""
|
||||
logging.debug(f"requestChannels for nodeNum:{self.nodeNum}")
|
||||
logger.debug(f"requestChannels for nodeNum:{self.nodeNum}")
|
||||
# only initialize if we're starting out fresh
|
||||
if startingIndex == 0:
|
||||
self.channels = None
|
||||
@@ -88,7 +121,7 @@ class Node:
|
||||
|
||||
def onResponseRequestSettings(self, p):
|
||||
"""Handle the response packets for requesting settings _requestSettings()"""
|
||||
logging.debug(f"onResponseRequestSetting() p:{p}")
|
||||
logger.debug(f"onResponseRequestSetting() p:{p}")
|
||||
config_values = None
|
||||
if "routing" in p["decoded"]:
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
@@ -137,11 +170,10 @@ class Node:
|
||||
p.get_config_request = configType
|
||||
|
||||
else:
|
||||
msgIndex = configType.index
|
||||
if configType.containing_type.name == "LocalConfig":
|
||||
p.get_config_request = msgIndex
|
||||
p.get_config_request = admin_pb2.AdminMessage.ConfigType.Value(configType.name.upper() + "_CONFIG")
|
||||
else:
|
||||
p.get_module_config_request = msgIndex
|
||||
p.get_module_config_request = configType.index
|
||||
|
||||
self._sendAdmin(p, wantResponse=True, onResponse=onResponse)
|
||||
if onResponse:
|
||||
@@ -212,10 +244,12 @@ class Node:
|
||||
p.set_module_config.ambient_lighting.CopyFrom(self.moduleConfig.ambient_lighting)
|
||||
elif config_name == "paxcounter":
|
||||
p.set_module_config.paxcounter.CopyFrom(self.moduleConfig.paxcounter)
|
||||
elif config_name == "traffic_management":
|
||||
p.set_module_config.traffic_management.CopyFrom(self.moduleConfig.traffic_management)
|
||||
else:
|
||||
our_exit(f"Error: No valid config with name {config_name}")
|
||||
|
||||
logging.debug(f"Wrote: {config_name}")
|
||||
logger.debug(f"Wrote: {config_name}")
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
else:
|
||||
@@ -228,7 +262,7 @@ class Node:
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.set_channel.CopyFrom(self.channels[channelIndex])
|
||||
self._sendAdmin(p, adminIndex=adminIndex)
|
||||
logging.debug(f"Wrote channel {channelIndex}")
|
||||
logger.debug(f"Wrote channel {channelIndex}")
|
||||
|
||||
def getChannelByChannelIndex(self, channelIndex):
|
||||
"""Get channel by channelIndex
|
||||
@@ -289,28 +323,37 @@ class Node:
|
||||
return c.index
|
||||
return 0
|
||||
|
||||
def setOwner(self, long_name: Optional[str]=None, short_name: Optional[str]=None, is_licensed: bool=False):
|
||||
def setOwner(self, long_name: Optional[str]=None, short_name: Optional[str]=None, is_licensed: bool=False, is_unmessagable: Optional[bool]=None):
|
||||
"""Set device owner name"""
|
||||
logging.debug(f"in setOwner nodeNum:{self.nodeNum}")
|
||||
logger.debug(f"in setOwner nodeNum:{self.nodeNum}")
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
|
||||
nChars = 4
|
||||
if long_name is not None:
|
||||
long_name = long_name.strip()
|
||||
# Validate that long_name is not empty or whitespace-only
|
||||
if not long_name:
|
||||
our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
|
||||
p.set_owner.long_name = long_name
|
||||
p.set_owner.is_licensed = is_licensed
|
||||
if short_name is not None:
|
||||
short_name = short_name.strip()
|
||||
# Validate that short_name is not empty or whitespace-only
|
||||
if not short_name:
|
||||
our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
if len(short_name) > nChars:
|
||||
short_name = short_name[:nChars]
|
||||
print(f"Maximum is 4 characters, truncated to {short_name}")
|
||||
p.set_owner.short_name = short_name
|
||||
if is_unmessagable is not None:
|
||||
p.set_owner.is_unmessagable = is_unmessagable
|
||||
|
||||
# Note: These debug lines are used in unit tests
|
||||
logging.debug(f"p.set_owner.long_name:{p.set_owner.long_name}:")
|
||||
logging.debug(f"p.set_owner.short_name:{p.set_owner.short_name}:")
|
||||
logging.debug(f"p.set_owner.is_licensed:{p.set_owner.is_licensed}")
|
||||
logger.debug(f"p.set_owner.long_name:{p.set_owner.long_name}:")
|
||||
logger.debug(f"p.set_owner.short_name:{p.set_owner.short_name}:")
|
||||
logger.debug(f"p.set_owner.is_licensed:{p.set_owner.is_licensed}")
|
||||
logger.debug(f"p.set_owner.is_unmessagable:{p.set_owner.is_unmessagable}:")
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
@@ -337,6 +380,46 @@ class Node:
|
||||
s = s.replace("=", "").replace("+", "-").replace("/", "_")
|
||||
return f"https://meshtastic.org/e/#{s}"
|
||||
|
||||
def getContactURL(self, node_id: Union[int, str], should_ignore: bool = False, manually_verified: bool = False):
|
||||
"""Generate a shareable contact URL for the specified node"""
|
||||
nodeNum = to_node_num(node_id)
|
||||
|
||||
node = self.iface.nodesByNum.get(nodeNum)
|
||||
if not node or not node.get("user"):
|
||||
our_exit(f"Warning: Node {node_id} not found in NodeDB")
|
||||
|
||||
contact = admin_pb2.SharedContact()
|
||||
contact.node_num = nodeNum
|
||||
|
||||
u = node["user"]
|
||||
if u.get("id"):
|
||||
contact.user.id = u["id"]
|
||||
if u.get("macaddr"):
|
||||
contact.user.macaddr = base64.b64decode(u["macaddr"])
|
||||
if u.get("longName"):
|
||||
contact.user.long_name = u["longName"]
|
||||
if u.get("shortName"):
|
||||
contact.user.short_name = u["shortName"]
|
||||
if u.get("hwModel") and u["hwModel"] != "UNSET":
|
||||
contact.user.hw_model = mesh_pb2.HardwareModel.Value(u["hwModel"])
|
||||
if u.get("role"):
|
||||
contact.user.role = config_pb2.Config.DeviceConfig.Role.Value(u["role"])
|
||||
if u.get("publicKey"):
|
||||
contact.user.public_key = base64.b64decode(u["publicKey"])
|
||||
if u.get("isLicensed"):
|
||||
contact.user.is_licensed = u["isLicensed"]
|
||||
if u.get("isUnmessagable") is not None:
|
||||
contact.user.is_unmessagable = u["isUnmessagable"]
|
||||
if should_ignore:
|
||||
contact.should_ignore = True
|
||||
if manually_verified:
|
||||
contact.manually_verified = True
|
||||
|
||||
data = contact.SerializeToString()
|
||||
s = base64.urlsafe_b64encode(data).decode("ascii")
|
||||
s = s.replace("=", "").replace("+", "-").replace("/", "_")
|
||||
return f"https://meshtastic.org/v/#{s}"
|
||||
|
||||
def setURL(self, url: str, addOnly: bool = False):
|
||||
"""Set mesh network URL"""
|
||||
if self.localConfig is None or self.channels is None:
|
||||
@@ -393,7 +476,7 @@ class Node:
|
||||
ch.index = i
|
||||
ch.settings.CopyFrom(chs)
|
||||
self.channels[ch.index] = ch
|
||||
logging.debug(f"Channel i:{i} ch:{ch}")
|
||||
logger.debug(f"Channel i:{i} ch:{ch}")
|
||||
self.writeChannel(ch.index)
|
||||
i = i + 1
|
||||
|
||||
@@ -402,9 +485,35 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
self._sendAdmin(p)
|
||||
|
||||
def addContactURL(self, url: str):
|
||||
"""Add a contact (User) to the NodeDB from a shareable URL"""
|
||||
self.ensureSessionKey()
|
||||
|
||||
splitURL = url.split("/#")
|
||||
if len(splitURL) == 1:
|
||||
our_exit(f"Warning: Invalid URL '{url}'")
|
||||
b64 = splitURL[-1]
|
||||
|
||||
missing_padding = len(b64) % 4
|
||||
if missing_padding:
|
||||
b64 += "=" * (4 - missing_padding)
|
||||
|
||||
decodedURL = base64.urlsafe_b64decode(b64)
|
||||
contact = admin_pb2.SharedContact()
|
||||
contact.ParseFromString(decodedURL)
|
||||
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.add_contact.CopyFrom(contact)
|
||||
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
else:
|
||||
onResponse = self.onAckNak
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def onResponseRequestRingtone(self, p):
|
||||
"""Handle the response packet for requesting ringtone part 1"""
|
||||
logging.debug(f"onResponseRequestRingtone() p:{p}")
|
||||
logger.debug(f"onResponseRequestRingtone() p:{p}")
|
||||
errorFound = False
|
||||
if "routing" in p["decoded"]:
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
@@ -417,12 +526,16 @@ class Node:
|
||||
self.ringtonePart = p["decoded"]["admin"][
|
||||
"raw"
|
||||
].get_ringtone_response
|
||||
logging.debug(f"self.ringtonePart:{self.ringtonePart}")
|
||||
logger.debug(f"self.ringtonePart:{self.ringtonePart}")
|
||||
self.gotResponse = True
|
||||
|
||||
def get_ringtone(self):
|
||||
"""Get the ringtone. Concatenate all pieces together and return a single string."""
|
||||
logging.debug(f"in get_ringtone()")
|
||||
logger.debug(f"in get_ringtone()")
|
||||
if not self.module_available(mesh_pb2.EXTNOTIF_CONFIG):
|
||||
logging.warning("External Notification module not present (excluded by firmware)")
|
||||
return None
|
||||
|
||||
if not self.ringtone:
|
||||
p1 = admin_pb2.AdminMessage()
|
||||
p1.get_ringtone_request = True
|
||||
@@ -433,18 +546,20 @@ class Node:
|
||||
while self.gotResponse is False:
|
||||
time.sleep(0.1)
|
||||
|
||||
logging.debug(f"self.ringtone:{self.ringtone}")
|
||||
logger.debug(f"self.ringtone:{self.ringtone}")
|
||||
|
||||
self.ringtone = ""
|
||||
if self.ringtonePart:
|
||||
self.ringtone += self.ringtonePart
|
||||
|
||||
print(f"ringtone:{self.ringtone}")
|
||||
logging.debug(f"ringtone:{self.ringtone}")
|
||||
logger.debug(f"ringtone:{self.ringtone}")
|
||||
return self.ringtone
|
||||
|
||||
def set_ringtone(self, ringtone):
|
||||
"""Set the ringtone. The ringtone length must be less than 230 character."""
|
||||
if not self.module_available(mesh_pb2.EXTNOTIF_CONFIG):
|
||||
logging.warning("External Notification module not present (excluded by firmware)")
|
||||
return None
|
||||
|
||||
if len(ringtone) > 230:
|
||||
our_exit("Warning: The ringtone must be less than 230 characters.")
|
||||
@@ -464,7 +579,7 @@ class Node:
|
||||
if i == 0:
|
||||
p.set_ringtone_message = chunk
|
||||
|
||||
logging.debug(f"Setting ringtone '{chunk}' part {i+1}")
|
||||
logger.debug(f"Setting ringtone '{chunk}' part {i+1}")
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
@@ -474,7 +589,7 @@ class Node:
|
||||
|
||||
def onResponseRequestCannedMessagePluginMessageMessages(self, p):
|
||||
"""Handle the response packet for requesting canned message plugin message part 1"""
|
||||
logging.debug(f"onResponseRequestCannedMessagePluginMessageMessages() p:{p}")
|
||||
logger.debug(f"onResponseRequestCannedMessagePluginMessageMessages() p:{p}")
|
||||
errorFound = False
|
||||
if "routing" in p["decoded"]:
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
@@ -487,14 +602,17 @@ class Node:
|
||||
self.cannedPluginMessageMessages = p["decoded"]["admin"][
|
||||
"raw"
|
||||
].get_canned_message_module_messages_response
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
|
||||
)
|
||||
self.gotResponse = True
|
||||
|
||||
def get_canned_message(self):
|
||||
"""Get the canned message string. Concatenate all pieces together and return a single string."""
|
||||
logging.debug(f"in get_canned_message()")
|
||||
logger.debug(f"in get_canned_message()")
|
||||
if not self.module_available(mesh_pb2.CANNEDMSG_CONFIG):
|
||||
logging.warning("Canned Message module not present (excluded by firmware)")
|
||||
return None
|
||||
if not self.cannedPluginMessage:
|
||||
p1 = admin_pb2.AdminMessage()
|
||||
p1.get_canned_message_module_messages_request = True
|
||||
@@ -507,7 +625,7 @@ class Node:
|
||||
while self.gotResponse is False:
|
||||
time.sleep(0.1)
|
||||
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
|
||||
)
|
||||
|
||||
@@ -515,12 +633,14 @@ class Node:
|
||||
if self.cannedPluginMessageMessages:
|
||||
self.cannedPluginMessage += self.cannedPluginMessageMessages
|
||||
|
||||
print(f"canned_plugin_message:{self.cannedPluginMessage}")
|
||||
logging.debug(f"canned_plugin_message:{self.cannedPluginMessage}")
|
||||
logger.debug(f"canned_plugin_message:{self.cannedPluginMessage}")
|
||||
return self.cannedPluginMessage
|
||||
|
||||
def set_canned_message(self, message):
|
||||
"""Set the canned message. The canned messages length must be less than 200 character."""
|
||||
if not self.module_available(mesh_pb2.CANNEDMSG_CONFIG):
|
||||
logging.warning("Canned Message module not present (excluded by firmware)")
|
||||
return None
|
||||
|
||||
if len(message) > 200:
|
||||
our_exit("Warning: The canned message must be less than 200 characters.")
|
||||
@@ -540,7 +660,7 @@ class Node:
|
||||
if i == 0:
|
||||
p.set_canned_message_module_messages = chunk
|
||||
|
||||
logging.debug(f"Setting canned message '{chunk}' part {i+1}")
|
||||
logger.debug(f"Setting canned message '{chunk}' part {i+1}")
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
@@ -554,7 +674,7 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.exit_simulator = True
|
||||
logging.debug("in exitSimulator()")
|
||||
logger.debug("in exitSimulator()")
|
||||
|
||||
return self._sendAdmin(p)
|
||||
|
||||
@@ -563,7 +683,7 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.reboot_seconds = secs
|
||||
logging.info(f"Telling node to reboot in {secs} seconds")
|
||||
logger.info(f"Telling node to reboot in {secs} seconds")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -577,7 +697,7 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.begin_edit_settings = True
|
||||
logging.info(f"Telling open a transaction to edit settings")
|
||||
logger.info(f"Telling open a transaction to edit settings")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -591,7 +711,7 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.commit_edit_settings = True
|
||||
logging.info(f"Telling node to commit open transaction for editing settings")
|
||||
logger.info(f"Telling node to commit open transaction for editing settings")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -601,11 +721,11 @@ class Node:
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def rebootOTA(self, secs: int = 10):
|
||||
"""Tell the node to reboot into factory firmware."""
|
||||
"""Tell the node to reboot into factory firmware (firmware < 2.7.18)."""
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.reboot_ota_seconds = secs
|
||||
logging.info(f"Telling node to reboot to OTA in {secs} seconds")
|
||||
logger.info(f"Telling node to reboot to OTA in {secs} seconds")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -614,12 +734,28 @@ class Node:
|
||||
onResponse = self.onAckNak
|
||||
return self._sendAdmin(p, onResponse=onResponse)
|
||||
|
||||
def startOTA(
|
||||
self,
|
||||
ota_mode: admin_pb2.OTAMode.ValueType,
|
||||
ota_file_hash: bytes,
|
||||
):
|
||||
"""Tell the node to start OTA mode (firmware >= 2.7.18)."""
|
||||
if self != self.iface.localNode:
|
||||
raise ValueError("startOTA only possible in local node")
|
||||
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.ota_request.reboot_ota_mode = ota_mode
|
||||
p.ota_request.ota_hash = ota_file_hash
|
||||
|
||||
return self._sendAdmin(p)
|
||||
|
||||
def enterDFUMode(self):
|
||||
"""Tell the node to enter DFU mode (NRF52)."""
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.enter_dfu_mode_request = True
|
||||
logging.info(f"Telling node to enable DFU mode")
|
||||
logger.info(f"Telling node to enable DFU mode")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -633,7 +769,7 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.shutdown_seconds = secs
|
||||
logging.info(f"Telling node to shutdown in {secs} seconds")
|
||||
logger.info(f"Telling node to shutdown in {secs} seconds")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -646,7 +782,7 @@ class Node:
|
||||
"""Get the node's metadata."""
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.get_device_metadata_request = True
|
||||
logging.info(f"Requesting device metadata")
|
||||
logger.info(f"Requesting device metadata")
|
||||
|
||||
self._sendAdmin(
|
||||
p, wantResponse=True, onResponse=self.onRequestGetMetadata
|
||||
@@ -658,11 +794,11 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
if full:
|
||||
p.factory_reset_device = True
|
||||
logging.info(f"Telling node to factory reset (full device reset)")
|
||||
p.factory_reset_device = 1
|
||||
logger.info(f"Telling node to factory reset (full device reset)")
|
||||
else:
|
||||
p.factory_reset_config = True
|
||||
logging.info(f"Telling node to factory reset (config reset)")
|
||||
p.factory_reset_config = 1
|
||||
logger.info(f"Telling node to factory reset (config reset)")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -674,11 +810,7 @@ class Node:
|
||||
def removeNode(self, nodeId: Union[int, str]):
|
||||
"""Tell the node to remove a specific node by ID"""
|
||||
self.ensureSessionKey()
|
||||
if isinstance(nodeId, str):
|
||||
if nodeId.startswith("!"):
|
||||
nodeId = int(nodeId[1:], 16)
|
||||
else:
|
||||
nodeId = int(nodeId)
|
||||
nodeId = to_node_num(nodeId)
|
||||
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.remove_by_nodenum = nodeId
|
||||
@@ -692,11 +824,7 @@ class Node:
|
||||
def setFavorite(self, nodeId: Union[int, str]):
|
||||
"""Tell the node to set the specified node ID to be favorited on the NodeDB on the device"""
|
||||
self.ensureSessionKey()
|
||||
if isinstance(nodeId, str):
|
||||
if nodeId.startswith("!"):
|
||||
nodeId = int(nodeId[1:], 16)
|
||||
else:
|
||||
nodeId = int(nodeId)
|
||||
nodeId = to_node_num(nodeId)
|
||||
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.set_favorite_node = nodeId
|
||||
@@ -710,11 +838,7 @@ class Node:
|
||||
def removeFavorite(self, nodeId: Union[int, str]):
|
||||
"""Tell the node to set the specified node ID to be un-favorited on the NodeDB on the device"""
|
||||
self.ensureSessionKey()
|
||||
if isinstance(nodeId, str):
|
||||
if nodeId.startswith("!"):
|
||||
nodeId = int(nodeId[1:], 16)
|
||||
else:
|
||||
nodeId = int(nodeId)
|
||||
nodeId = to_node_num(nodeId)
|
||||
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.remove_favorite_node = nodeId
|
||||
@@ -728,11 +852,7 @@ class Node:
|
||||
def setIgnored(self, nodeId: Union[int, str]):
|
||||
"""Tell the node to set the specified node ID to be ignored on the NodeDB on the device"""
|
||||
self.ensureSessionKey()
|
||||
if isinstance(nodeId, str):
|
||||
if nodeId.startswith("!"):
|
||||
nodeId = int(nodeId[1:], 16)
|
||||
else:
|
||||
nodeId = int(nodeId)
|
||||
nodeId = to_node_num(nodeId)
|
||||
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.set_ignored_node = nodeId
|
||||
@@ -746,11 +866,7 @@ class Node:
|
||||
def removeIgnored(self, nodeId: Union[int, str]):
|
||||
"""Tell the node to set the specified node ID to be un-ignored on the NodeDB on the device"""
|
||||
self.ensureSessionKey()
|
||||
if isinstance(nodeId, str):
|
||||
if nodeId.startswith("!"):
|
||||
nodeId = int(nodeId[1:], 16)
|
||||
else:
|
||||
nodeId = int(nodeId)
|
||||
nodeId = to_node_num(nodeId)
|
||||
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.remove_ignored_node = nodeId
|
||||
@@ -766,7 +882,7 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.nodedb_reset = True
|
||||
logging.info(f"Telling node to reset the NodeDB")
|
||||
logger.info(f"Telling node to reset the NodeDB")
|
||||
|
||||
# If sending to a remote node, wait for ACK/NAK
|
||||
if self == self.iface.localNode:
|
||||
@@ -807,7 +923,7 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.remove_fixed_position = True
|
||||
logging.info(f"Telling node to remove fixed position")
|
||||
logger.info(f"Telling node to remove fixed position")
|
||||
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
@@ -822,7 +938,7 @@ class Node:
|
||||
timeSec = int(time.time())
|
||||
p = admin_pb2.AdminMessage()
|
||||
p.set_time_only = timeSec
|
||||
logging.info(f"Setting node time to {timeSec}")
|
||||
logger.info(f"Setting node time to {timeSec}")
|
||||
|
||||
if self == self.iface.localNode:
|
||||
onResponse = None
|
||||
@@ -854,7 +970,7 @@ class Node:
|
||||
|
||||
def onRequestGetMetadata(self, p):
|
||||
"""Handle the response packet for requesting device metadata getMetadata()"""
|
||||
logging.debug(f"onRequestGetMetadata() p:{p}")
|
||||
logger.debug(f"onRequestGetMetadata() p:{p}")
|
||||
|
||||
if "routing" in p["decoded"]:
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
@@ -866,30 +982,42 @@ class Node:
|
||||
portnums_pb2.PortNum.ROUTING_APP
|
||||
):
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f'Metadata request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
|
||||
)
|
||||
self._timeout.expireTime = time.time() # Do not wait any longer
|
||||
return # Don't try to parse this routing message
|
||||
logging.debug(f"Retrying metadata request.")
|
||||
logger.debug(f"Retrying metadata request.")
|
||||
self.getMetadata()
|
||||
return
|
||||
|
||||
c = p["decoded"]["admin"]["raw"].get_device_metadata_response
|
||||
self._timeout.reset() # We made forward progress
|
||||
logging.debug(f"Received metadata {stripnl(c)}")
|
||||
logger.debug(f"Received metadata {stripnl(c)}")
|
||||
print(f"\nfirmware_version: {c.firmware_version}")
|
||||
print(f"device_state_version: {c.device_state_version}")
|
||||
if c.role in config_pb2.Config.DeviceConfig.Role.values():
|
||||
print(f"role: {config_pb2.Config.DeviceConfig.Role.Name(c.role)}")
|
||||
else:
|
||||
print(f"role: {c.role}")
|
||||
print(f"position_flags: {self.position_flags_list(c.position_flags)}")
|
||||
if c.hw_model in mesh_pb2.HardwareModel.values():
|
||||
print(f"hw_model: {mesh_pb2.HardwareModel.Name(c.hw_model)}")
|
||||
else:
|
||||
print(f"hw_model: {c.hw_model}")
|
||||
print(f"hasPKC: {c.hasPKC}")
|
||||
if c.excluded_modules > 0:
|
||||
print(f"excluded_modules: {self.excluded_modules_list(c.excluded_modules)}")
|
||||
|
||||
def onResponseRequestChannel(self, p):
|
||||
"""Handle the response packet for requesting a channel _requestChannel()"""
|
||||
logging.debug(f"onResponseRequestChannel() p:{p}")
|
||||
logger.debug(f"onResponseRequestChannel() p:{p}")
|
||||
|
||||
if p["decoded"]["portnum"] == portnums_pb2.PortNum.Name(
|
||||
portnums_pb2.PortNum.ROUTING_APP
|
||||
):
|
||||
if p["decoded"]["routing"]["errorReason"] != "NONE":
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f'Channel request failed, error reason: {p["decoded"]["routing"]["errorReason"]}'
|
||||
)
|
||||
self._timeout.expireTime = time.time() # Do not wait any longer
|
||||
@@ -897,18 +1025,18 @@ class Node:
|
||||
lastTried = 0
|
||||
if len(self.partialChannels) > 0:
|
||||
lastTried = self.partialChannels[-1].index
|
||||
logging.debug(f"Retrying previous channel request.")
|
||||
logger.debug(f"Retrying previous channel request.")
|
||||
self._requestChannel(lastTried)
|
||||
return
|
||||
|
||||
c = p["decoded"]["admin"]["raw"].get_channel_response
|
||||
self.partialChannels.append(c)
|
||||
self._timeout.reset() # We made forward progress
|
||||
logging.debug(f"Received channel {stripnl(c)}")
|
||||
logger.debug(f"Received channel {stripnl(c)}")
|
||||
index = c.index
|
||||
|
||||
if index >= 8 - 1:
|
||||
logging.debug("Finished downloading channels")
|
||||
logger.debug("Finished downloading channels")
|
||||
|
||||
self.channels = self.partialChannels
|
||||
self._fixupChannels()
|
||||
@@ -943,11 +1071,11 @@ class Node:
|
||||
print(
|
||||
f"Requesting channel {channelNum} info from remote node (this could take a while)"
|
||||
)
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"Requesting channel {channelNum} info from remote node (this could take a while)"
|
||||
)
|
||||
else:
|
||||
logging.debug(f"Requesting channel {channelNum}")
|
||||
logger.debug(f"Requesting channel {channelNum}")
|
||||
|
||||
return self._sendAdmin(
|
||||
p, wantResponse=True, onResponse=self.onResponseRequestChannel
|
||||
@@ -964,7 +1092,7 @@ class Node:
|
||||
"""Send an admin message to the specified node (or the local node if destNodeNum is zero)"""
|
||||
|
||||
if self.noProto:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f"Not sending packet because protocol use is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
@@ -972,18 +1100,15 @@ class Node:
|
||||
adminIndex == 0
|
||||
): # unless a special channel index was used, we want to use the admin index
|
||||
adminIndex = self.iface.localNode._getAdminChannelIndex()
|
||||
logging.debug(f"adminIndex:{adminIndex}")
|
||||
if isinstance(self.nodeNum, int):
|
||||
nodeid = self.nodeNum
|
||||
else: # assume string starting with !
|
||||
nodeid = int(self.nodeNum[1:],16)
|
||||
logger.debug(f"adminIndex:{adminIndex}")
|
||||
nodeid = to_node_num(self.nodeNum)
|
||||
if "adminSessionPassKey" in self.iface._getOrCreateByNum(nodeid):
|
||||
p.session_passkey = self.iface._getOrCreateByNum(nodeid).get("adminSessionPassKey")
|
||||
return self.iface.sendData(
|
||||
p,
|
||||
self.nodeNum,
|
||||
portNum=portnums_pb2.PortNum.ADMIN_APP,
|
||||
wantAck=False,
|
||||
wantAck=True,
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=adminIndex,
|
||||
@@ -993,13 +1118,27 @@ class Node:
|
||||
def ensureSessionKey(self):
|
||||
"""If our entry in iface.nodesByNum doesn't already have an adminSessionPassKey, make a request to get one"""
|
||||
if self.noProto:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f"Not ensuring session key, because protocol use is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
if isinstance(self.nodeNum, int):
|
||||
nodeid = self.nodeNum
|
||||
else: # assume string starting with !
|
||||
nodeid = int(self.nodeNum[1:],16)
|
||||
nodeid = to_node_num(self.nodeNum)
|
||||
if self.iface._getOrCreateByNum(nodeid).get("adminSessionPassKey") is None:
|
||||
self.requestConfig(admin_pb2.AdminMessage.SESSIONKEY_CONFIG)
|
||||
|
||||
def get_channels_with_hash(self):
|
||||
"""Return a list of dicts with channel info and hash."""
|
||||
result = []
|
||||
if self.channels:
|
||||
for c in self.channels:
|
||||
if c.settings and hasattr(c.settings, "name") and hasattr(c.settings, "psk"):
|
||||
hash_val = generate_channel_hash(c.settings.name, c.settings.psk)
|
||||
else:
|
||||
hash_val = None
|
||||
result.append({
|
||||
"index": c.index,
|
||||
"role": channel_pb2.Channel.Role.Name(c.role),
|
||||
"name": c.settings.name if c.settings and hasattr(c.settings, "name") else "",
|
||||
"hash": hash_val,
|
||||
})
|
||||
return result
|
||||
|
||||
128
meshtastic/ota.py
Normal file
128
meshtastic/ota.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Meshtastic ESP32 Unified OTA
|
||||
"""
|
||||
import os
|
||||
import hashlib
|
||||
import socket
|
||||
import logging
|
||||
from typing import Optional, Callable
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _file_sha256(filename: str):
|
||||
"""Calculate SHA256 hash of a file."""
|
||||
sha256_hash = hashlib.sha256()
|
||||
|
||||
with open(filename, "rb") as f:
|
||||
for byte_block in iter(lambda: f.read(4096), b""):
|
||||
sha256_hash.update(byte_block)
|
||||
|
||||
return sha256_hash
|
||||
|
||||
|
||||
class OTAError(Exception):
|
||||
"""Exception for OTA errors."""
|
||||
|
||||
|
||||
class ESP32WiFiOTA:
|
||||
"""ESP32 WiFi Unified OTA updates."""
|
||||
|
||||
def __init__(self, filename: str, hostname: str, port: int = 3232):
|
||||
self._filename = filename
|
||||
self._hostname = hostname
|
||||
self._port = port
|
||||
self._socket: Optional[socket.socket] = None
|
||||
|
||||
if not os.path.exists(self._filename):
|
||||
raise FileNotFoundError(f"File {self._filename} does not exist")
|
||||
|
||||
self._file_hash = _file_sha256(self._filename)
|
||||
|
||||
def _read_line(self) -> str:
|
||||
"""Read a line from the socket."""
|
||||
if not self._socket:
|
||||
raise ConnectionError("Socket not connected")
|
||||
|
||||
line = b""
|
||||
while not line.endswith(b"\n"):
|
||||
char = self._socket.recv(1)
|
||||
|
||||
if not char:
|
||||
raise ConnectionError("Connection closed while waiting for response")
|
||||
|
||||
line += char
|
||||
|
||||
return line.decode("utf-8").strip()
|
||||
|
||||
def hash_bytes(self) -> bytes:
|
||||
"""Return the hash as bytes."""
|
||||
return self._file_hash.digest()
|
||||
|
||||
def hash_hex(self) -> str:
|
||||
"""Return the hash as a hex string."""
|
||||
return self._file_hash.hexdigest()
|
||||
|
||||
def update(self, progress_callback: Optional[Callable[[int, int], None]] = None):
|
||||
"""Perform the OTA update."""
|
||||
with open(self._filename, "rb") as f:
|
||||
data = f.read()
|
||||
size = len(data)
|
||||
|
||||
logger.info(f"Starting OTA update with {self._filename} ({size} bytes, hash {self.hash_hex()})")
|
||||
|
||||
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._socket.settimeout(15)
|
||||
try:
|
||||
self._socket.connect((self._hostname, self._port))
|
||||
logger.debug(f"Connected to {self._hostname}:{self._port}")
|
||||
|
||||
# Send start command
|
||||
self._socket.sendall(f"OTA {size} {self.hash_hex()}\n".encode("utf-8"))
|
||||
|
||||
# Wait for OK from the device
|
||||
while True:
|
||||
response = self._read_line()
|
||||
if response == "OK":
|
||||
break
|
||||
|
||||
if response == "ERASING":
|
||||
logger.info("Device is erasing flash...")
|
||||
elif response.startswith("ERR "):
|
||||
raise OTAError(f"Device reported error: {response}")
|
||||
else:
|
||||
logger.warning(f"Unexpected response: {response}")
|
||||
|
||||
# Stream firmware
|
||||
sent_bytes = 0
|
||||
chunk_size = 1024
|
||||
while sent_bytes < size:
|
||||
chunk = data[sent_bytes : sent_bytes + chunk_size]
|
||||
self._socket.sendall(chunk)
|
||||
sent_bytes += len(chunk)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(sent_bytes, size)
|
||||
else:
|
||||
print(f"[{sent_bytes / size * 100:5.1f}%] Sent {sent_bytes} of {size} bytes...", end="\r")
|
||||
|
||||
if not progress_callback:
|
||||
print()
|
||||
|
||||
# Wait for OK from device
|
||||
logger.info("Firmware sent, waiting for verification...")
|
||||
while True:
|
||||
response = self._read_line()
|
||||
if response == "OK":
|
||||
logger.info("OTA update completed successfully!")
|
||||
break
|
||||
|
||||
if response.startswith("ERR "):
|
||||
raise OTAError(f"OTA update failed: {response}")
|
||||
elif response != "ACK":
|
||||
logger.warning(f"Unexpected final response: {response}")
|
||||
|
||||
finally:
|
||||
if self._socket:
|
||||
self._socket.close()
|
||||
self._socket = None
|
||||
87
meshtastic/protobuf/admin_pb2.py
generated
87
meshtastic/protobuf/admin_pb2.py
generated
File diff suppressed because one or more lines are too long
613
meshtastic/protobuf/admin_pb2.pyi
generated
613
meshtastic/protobuf/admin_pb2.pyi
generated
@@ -25,6 +25,44 @@ else:
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
|
||||
|
||||
class _OTAMode:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _OTAModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_OTAMode.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
NO_REBOOT_OTA: _OTAMode.ValueType # 0
|
||||
"""
|
||||
Do not reboot into OTA mode
|
||||
"""
|
||||
OTA_BLE: _OTAMode.ValueType # 1
|
||||
"""
|
||||
Reboot into OTA mode for BLE firmware update
|
||||
"""
|
||||
OTA_WIFI: _OTAMode.ValueType # 2
|
||||
"""
|
||||
Reboot into OTA mode for WiFi firmware update
|
||||
"""
|
||||
|
||||
class OTAMode(_OTAMode, metaclass=_OTAModeEnumTypeWrapper):
|
||||
"""
|
||||
Firmware update mode for OTA updates
|
||||
"""
|
||||
|
||||
NO_REBOOT_OTA: OTAMode.ValueType # 0
|
||||
"""
|
||||
Do not reboot into OTA mode
|
||||
"""
|
||||
OTA_BLE: OTAMode.ValueType # 1
|
||||
"""
|
||||
Reboot into OTA mode for BLE firmware update
|
||||
"""
|
||||
OTA_WIFI: OTAMode.ValueType # 2
|
||||
"""
|
||||
Reboot into OTA mode for WiFi firmware update
|
||||
"""
|
||||
global___OTAMode = OTAMode
|
||||
|
||||
@typing.final
|
||||
class AdminMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
@@ -186,6 +224,18 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
TODO: REPLACE
|
||||
"""
|
||||
STATUSMESSAGE_CONFIG: AdminMessage._ModuleConfigType.ValueType # 13
|
||||
"""
|
||||
TODO: REPLACE
|
||||
"""
|
||||
TRAFFICMANAGEMENT_CONFIG: AdminMessage._ModuleConfigType.ValueType # 14
|
||||
"""
|
||||
Traffic management module config
|
||||
"""
|
||||
TAK_CONFIG: AdminMessage._ModuleConfigType.ValueType # 15
|
||||
"""
|
||||
TAK module config
|
||||
"""
|
||||
|
||||
class ModuleConfigType(_ModuleConfigType, metaclass=_ModuleConfigTypeEnumTypeWrapper):
|
||||
"""
|
||||
@@ -244,6 +294,18 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
TODO: REPLACE
|
||||
"""
|
||||
STATUSMESSAGE_CONFIG: AdminMessage.ModuleConfigType.ValueType # 13
|
||||
"""
|
||||
TODO: REPLACE
|
||||
"""
|
||||
TRAFFICMANAGEMENT_CONFIG: AdminMessage.ModuleConfigType.ValueType # 14
|
||||
"""
|
||||
Traffic management module config
|
||||
"""
|
||||
TAK_CONFIG: AdminMessage.ModuleConfigType.ValueType # 15
|
||||
"""
|
||||
TAK module config
|
||||
"""
|
||||
|
||||
class _BackupLocation:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
@@ -270,6 +332,72 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
Backup to the SD card
|
||||
"""
|
||||
|
||||
@typing.final
|
||||
class InputEvent(google.protobuf.message.Message):
|
||||
"""
|
||||
Input event message to be sent to the node.
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
EVENT_CODE_FIELD_NUMBER: builtins.int
|
||||
KB_CHAR_FIELD_NUMBER: builtins.int
|
||||
TOUCH_X_FIELD_NUMBER: builtins.int
|
||||
TOUCH_Y_FIELD_NUMBER: builtins.int
|
||||
event_code: builtins.int
|
||||
"""
|
||||
The input event code
|
||||
"""
|
||||
kb_char: builtins.int
|
||||
"""
|
||||
Keyboard character code
|
||||
"""
|
||||
touch_x: builtins.int
|
||||
"""
|
||||
The touch X coordinate
|
||||
"""
|
||||
touch_y: builtins.int
|
||||
"""
|
||||
The touch Y coordinate
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
event_code: builtins.int = ...,
|
||||
kb_char: builtins.int = ...,
|
||||
touch_x: builtins.int = ...,
|
||||
touch_y: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["event_code", b"event_code", "kb_char", b"kb_char", "touch_x", b"touch_x", "touch_y", b"touch_y"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class OTAEvent(google.protobuf.message.Message):
|
||||
"""
|
||||
User is requesting an over the air update.
|
||||
Node will reboot into the OTA loader
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
REBOOT_OTA_MODE_FIELD_NUMBER: builtins.int
|
||||
OTA_HASH_FIELD_NUMBER: builtins.int
|
||||
reboot_ota_mode: global___OTAMode.ValueType
|
||||
"""
|
||||
Tell the node to reboot into OTA mode for firmware update via BLE or WiFi (ESP32 only for now)
|
||||
"""
|
||||
ota_hash: builtins.bytes
|
||||
"""
|
||||
A 32 byte hash of the OTA firmware.
|
||||
Used to verify the integrity of the firmware before applying an update.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
reboot_ota_mode: global___OTAMode.ValueType = ...,
|
||||
ota_hash: builtins.bytes = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["ota_hash", b"ota_hash", "reboot_ota_mode", b"reboot_ota_mode"]) -> None: ...
|
||||
|
||||
SESSION_PASSKEY_FIELD_NUMBER: builtins.int
|
||||
GET_CHANNEL_REQUEST_FIELD_NUMBER: builtins.int
|
||||
GET_CHANNEL_RESPONSE_FIELD_NUMBER: builtins.int
|
||||
@@ -296,6 +424,7 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
BACKUP_PREFERENCES_FIELD_NUMBER: builtins.int
|
||||
RESTORE_PREFERENCES_FIELD_NUMBER: builtins.int
|
||||
REMOVE_BACKUP_PREFERENCES_FIELD_NUMBER: builtins.int
|
||||
SEND_INPUT_EVENT_FIELD_NUMBER: builtins.int
|
||||
SET_OWNER_FIELD_NUMBER: builtins.int
|
||||
SET_CHANNEL_FIELD_NUMBER: builtins.int
|
||||
SET_CONFIG_FIELD_NUMBER: builtins.int
|
||||
@@ -313,8 +442,11 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
STORE_UI_CONFIG_FIELD_NUMBER: builtins.int
|
||||
SET_IGNORED_NODE_FIELD_NUMBER: builtins.int
|
||||
REMOVE_IGNORED_NODE_FIELD_NUMBER: builtins.int
|
||||
TOGGLE_MUTED_NODE_FIELD_NUMBER: builtins.int
|
||||
BEGIN_EDIT_SETTINGS_FIELD_NUMBER: builtins.int
|
||||
COMMIT_EDIT_SETTINGS_FIELD_NUMBER: builtins.int
|
||||
ADD_CONTACT_FIELD_NUMBER: builtins.int
|
||||
KEY_VERIFICATION_FIELD_NUMBER: builtins.int
|
||||
FACTORY_RESET_DEVICE_FIELD_NUMBER: builtins.int
|
||||
REBOOT_OTA_SECONDS_FIELD_NUMBER: builtins.int
|
||||
EXIT_SIMULATOR_FIELD_NUMBER: builtins.int
|
||||
@@ -322,6 +454,9 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
SHUTDOWN_SECONDS_FIELD_NUMBER: builtins.int
|
||||
FACTORY_RESET_CONFIG_FIELD_NUMBER: builtins.int
|
||||
NODEDB_RESET_FIELD_NUMBER: builtins.int
|
||||
OTA_REQUEST_FIELD_NUMBER: builtins.int
|
||||
SENSOR_CONFIG_FIELD_NUMBER: builtins.int
|
||||
LOCKDOWN_AUTH_FIELD_NUMBER: builtins.int
|
||||
session_passkey: builtins.bytes
|
||||
"""
|
||||
The node generates this key and sends it with any get_x_response packets.
|
||||
@@ -439,6 +574,10 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
Set specified node-num to be un-ignored on the NodeDB on the device
|
||||
"""
|
||||
toggle_muted_node: builtins.int
|
||||
"""
|
||||
Set specified node-num to be muted
|
||||
"""
|
||||
begin_edit_settings: builtins.bool
|
||||
"""
|
||||
Begins an edit transaction for config, module config, owner, and channel settings changes
|
||||
@@ -456,6 +595,7 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
|
||||
Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
|
||||
Deprecated in favor of reboot_ota_mode in 2.7.17
|
||||
"""
|
||||
exit_simulator: builtins.bool
|
||||
"""
|
||||
@@ -474,9 +614,10 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved.
|
||||
"""
|
||||
nodedb_reset: builtins.int
|
||||
nodedb_reset: builtins.bool
|
||||
"""
|
||||
Tell the node to reset the nodedb.
|
||||
When true, favorites are preserved through reset.
|
||||
"""
|
||||
@property
|
||||
def get_channel_response(self) -> meshtastic.protobuf.channel_pb2.Channel:
|
||||
@@ -526,6 +667,13 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use
|
||||
"""
|
||||
|
||||
@property
|
||||
def send_input_event(self) -> global___AdminMessage.InputEvent:
|
||||
"""
|
||||
Send an input event to the node.
|
||||
This is used to trigger physical input events like button presses, touch events, etc.
|
||||
"""
|
||||
|
||||
@property
|
||||
def set_owner(self) -> meshtastic.protobuf.mesh_pb2.User:
|
||||
"""
|
||||
@@ -572,6 +720,43 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
Tell the node to store UI data persistently.
|
||||
"""
|
||||
|
||||
@property
|
||||
def add_contact(self) -> global___SharedContact:
|
||||
"""
|
||||
Add a contact (User) to the nodedb
|
||||
"""
|
||||
|
||||
@property
|
||||
def key_verification(self) -> global___KeyVerificationAdmin:
|
||||
"""
|
||||
Initiate or respond to a key verification request
|
||||
"""
|
||||
|
||||
@property
|
||||
def ota_request(self) -> global___AdminMessage.OTAEvent:
|
||||
"""
|
||||
Tell the node to reset into the OTA Loader
|
||||
"""
|
||||
|
||||
@property
|
||||
def sensor_config(self) -> global___SensorConfig:
|
||||
"""
|
||||
Parameters and sensor configuration
|
||||
"""
|
||||
|
||||
@property
|
||||
def lockdown_auth(self) -> global___LockdownAuth:
|
||||
"""
|
||||
Lockdown passphrase delivery / unlock / lock-now command for hardened
|
||||
firmware builds (see MESHTASTIC_LOCKDOWN). Used to provision the
|
||||
passphrase on first boot, unlock encrypted storage on subsequent
|
||||
reboots, re-verify on already-unlocked devices to authorize a new
|
||||
client connection, or immediately re-lock the device.
|
||||
|
||||
Replaces the earlier scheme that repurposed SecurityConfig.private_key
|
||||
to carry passphrase bytes; that hack is retired.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -601,6 +786,7 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
backup_preferences: global___AdminMessage.BackupLocation.ValueType = ...,
|
||||
restore_preferences: global___AdminMessage.BackupLocation.ValueType = ...,
|
||||
remove_backup_preferences: global___AdminMessage.BackupLocation.ValueType = ...,
|
||||
send_input_event: global___AdminMessage.InputEvent | None = ...,
|
||||
set_owner: meshtastic.protobuf.mesh_pb2.User | None = ...,
|
||||
set_channel: meshtastic.protobuf.channel_pb2.Channel | None = ...,
|
||||
set_config: meshtastic.protobuf.config_pb2.Config | None = ...,
|
||||
@@ -618,22 +804,90 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
store_ui_config: meshtastic.protobuf.device_ui_pb2.DeviceUIConfig | None = ...,
|
||||
set_ignored_node: builtins.int = ...,
|
||||
remove_ignored_node: builtins.int = ...,
|
||||
toggle_muted_node: builtins.int = ...,
|
||||
begin_edit_settings: builtins.bool = ...,
|
||||
commit_edit_settings: builtins.bool = ...,
|
||||
add_contact: global___SharedContact | None = ...,
|
||||
key_verification: global___KeyVerificationAdmin | None = ...,
|
||||
factory_reset_device: builtins.int = ...,
|
||||
reboot_ota_seconds: builtins.int = ...,
|
||||
exit_simulator: builtins.bool = ...,
|
||||
reboot_seconds: builtins.int = ...,
|
||||
shutdown_seconds: builtins.int = ...,
|
||||
factory_reset_config: builtins.int = ...,
|
||||
nodedb_reset: builtins.int = ...,
|
||||
nodedb_reset: builtins.bool = ...,
|
||||
ota_request: global___AdminMessage.OTAEvent | None = ...,
|
||||
sensor_config: global___SensorConfig | None = ...,
|
||||
lockdown_auth: global___LockdownAuth | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["backup_preferences", b"backup_preferences", "begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "get_ui_config_request", b"get_ui_config_request", "get_ui_config_response", b"get_ui_config_response", "nodedb_reset", b"nodedb_reset", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_backup_preferences", b"remove_backup_preferences", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "remove_ignored_node", b"remove_ignored_node", "restore_preferences", b"restore_preferences", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_ignored_node", b"set_ignored_node", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds", "store_ui_config", b"store_ui_config"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["backup_preferences", b"backup_preferences", "begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "get_ui_config_request", b"get_ui_config_request", "get_ui_config_response", b"get_ui_config_response", "nodedb_reset", b"nodedb_reset", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_backup_preferences", b"remove_backup_preferences", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "remove_ignored_node", b"remove_ignored_node", "restore_preferences", b"restore_preferences", "session_passkey", b"session_passkey", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_ignored_node", b"set_ignored_node", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds", "store_ui_config", b"store_ui_config"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["get_channel_request", "get_channel_response", "get_owner_request", "get_owner_response", "get_config_request", "get_config_response", "get_module_config_request", "get_module_config_response", "get_canned_message_module_messages_request", "get_canned_message_module_messages_response", "get_device_metadata_request", "get_device_metadata_response", "get_ringtone_request", "get_ringtone_response", "get_device_connection_status_request", "get_device_connection_status_response", "set_ham_mode", "get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", "enter_dfu_mode_request", "delete_file_request", "set_scale", "backup_preferences", "restore_preferences", "remove_backup_preferences", "set_owner", "set_channel", "set_config", "set_module_config", "set_canned_message_module_messages", "set_ringtone_message", "remove_by_nodenum", "set_favorite_node", "remove_favorite_node", "set_fixed_position", "remove_fixed_position", "set_time_only", "get_ui_config_request", "get_ui_config_response", "store_ui_config", "set_ignored_node", "remove_ignored_node", "begin_edit_settings", "commit_edit_settings", "factory_reset_device", "reboot_ota_seconds", "exit_simulator", "reboot_seconds", "shutdown_seconds", "factory_reset_config", "nodedb_reset"] | None: ...
|
||||
def HasField(self, field_name: typing.Literal["add_contact", b"add_contact", "backup_preferences", b"backup_preferences", "begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "get_ui_config_request", b"get_ui_config_request", "get_ui_config_response", b"get_ui_config_response", "key_verification", b"key_verification", "lockdown_auth", b"lockdown_auth", "nodedb_reset", b"nodedb_reset", "ota_request", b"ota_request", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_backup_preferences", b"remove_backup_preferences", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "remove_ignored_node", b"remove_ignored_node", "restore_preferences", b"restore_preferences", "send_input_event", b"send_input_event", "sensor_config", b"sensor_config", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_ignored_node", b"set_ignored_node", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds", "store_ui_config", b"store_ui_config", "toggle_muted_node", b"toggle_muted_node"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["add_contact", b"add_contact", "backup_preferences", b"backup_preferences", "begin_edit_settings", b"begin_edit_settings", "commit_edit_settings", b"commit_edit_settings", "delete_file_request", b"delete_file_request", "enter_dfu_mode_request", b"enter_dfu_mode_request", "exit_simulator", b"exit_simulator", "factory_reset_config", b"factory_reset_config", "factory_reset_device", b"factory_reset_device", "get_canned_message_module_messages_request", b"get_canned_message_module_messages_request", "get_canned_message_module_messages_response", b"get_canned_message_module_messages_response", "get_channel_request", b"get_channel_request", "get_channel_response", b"get_channel_response", "get_config_request", b"get_config_request", "get_config_response", b"get_config_response", "get_device_connection_status_request", b"get_device_connection_status_request", "get_device_connection_status_response", b"get_device_connection_status_response", "get_device_metadata_request", b"get_device_metadata_request", "get_device_metadata_response", b"get_device_metadata_response", "get_module_config_request", b"get_module_config_request", "get_module_config_response", b"get_module_config_response", "get_node_remote_hardware_pins_request", b"get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", b"get_node_remote_hardware_pins_response", "get_owner_request", b"get_owner_request", "get_owner_response", b"get_owner_response", "get_ringtone_request", b"get_ringtone_request", "get_ringtone_response", b"get_ringtone_response", "get_ui_config_request", b"get_ui_config_request", "get_ui_config_response", b"get_ui_config_response", "key_verification", b"key_verification", "lockdown_auth", b"lockdown_auth", "nodedb_reset", b"nodedb_reset", "ota_request", b"ota_request", "payload_variant", b"payload_variant", "reboot_ota_seconds", b"reboot_ota_seconds", "reboot_seconds", b"reboot_seconds", "remove_backup_preferences", b"remove_backup_preferences", "remove_by_nodenum", b"remove_by_nodenum", "remove_favorite_node", b"remove_favorite_node", "remove_fixed_position", b"remove_fixed_position", "remove_ignored_node", b"remove_ignored_node", "restore_preferences", b"restore_preferences", "send_input_event", b"send_input_event", "sensor_config", b"sensor_config", "session_passkey", b"session_passkey", "set_canned_message_module_messages", b"set_canned_message_module_messages", "set_channel", b"set_channel", "set_config", b"set_config", "set_favorite_node", b"set_favorite_node", "set_fixed_position", b"set_fixed_position", "set_ham_mode", b"set_ham_mode", "set_ignored_node", b"set_ignored_node", "set_module_config", b"set_module_config", "set_owner", b"set_owner", "set_ringtone_message", b"set_ringtone_message", "set_scale", b"set_scale", "set_time_only", b"set_time_only", "shutdown_seconds", b"shutdown_seconds", "store_ui_config", b"store_ui_config", "toggle_muted_node", b"toggle_muted_node"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["get_channel_request", "get_channel_response", "get_owner_request", "get_owner_response", "get_config_request", "get_config_response", "get_module_config_request", "get_module_config_response", "get_canned_message_module_messages_request", "get_canned_message_module_messages_response", "get_device_metadata_request", "get_device_metadata_response", "get_ringtone_request", "get_ringtone_response", "get_device_connection_status_request", "get_device_connection_status_response", "set_ham_mode", "get_node_remote_hardware_pins_request", "get_node_remote_hardware_pins_response", "enter_dfu_mode_request", "delete_file_request", "set_scale", "backup_preferences", "restore_preferences", "remove_backup_preferences", "send_input_event", "set_owner", "set_channel", "set_config", "set_module_config", "set_canned_message_module_messages", "set_ringtone_message", "remove_by_nodenum", "set_favorite_node", "remove_favorite_node", "set_fixed_position", "remove_fixed_position", "set_time_only", "get_ui_config_request", "get_ui_config_response", "store_ui_config", "set_ignored_node", "remove_ignored_node", "toggle_muted_node", "begin_edit_settings", "commit_edit_settings", "add_contact", "key_verification", "factory_reset_device", "reboot_ota_seconds", "exit_simulator", "reboot_seconds", "shutdown_seconds", "factory_reset_config", "nodedb_reset", "ota_request", "sensor_config", "lockdown_auth"] | None: ...
|
||||
|
||||
global___AdminMessage = AdminMessage
|
||||
|
||||
@typing.final
|
||||
class LockdownAuth(google.protobuf.message.Message):
|
||||
"""
|
||||
Lockdown passphrase delivery payload.
|
||||
|
||||
One message handles three operations distinguished by content:
|
||||
- Provision (first-time): passphrase set, lock_now=false. Firmware
|
||||
generates DEK, wraps with passphrase-derived KEK, persists.
|
||||
- Unlock: passphrase set, lock_now=false. Firmware verifies
|
||||
passphrase against stored DEK, unlocks storage, authorizes the
|
||||
connection that delivered this packet.
|
||||
- Lock now: lock_now=true, passphrase ignored. Firmware revokes
|
||||
all client auth and reboots into the locked state.
|
||||
|
||||
Firmware decides between provision and unlock based on its own state
|
||||
(whether a DEK file already exists). Clients do not need to track
|
||||
which case applies.
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
PASSPHRASE_FIELD_NUMBER: builtins.int
|
||||
BOOTS_REMAINING_FIELD_NUMBER: builtins.int
|
||||
VALID_UNTIL_EPOCH_FIELD_NUMBER: builtins.int
|
||||
LOCK_NOW_FIELD_NUMBER: builtins.int
|
||||
passphrase: builtins.bytes
|
||||
"""
|
||||
Passphrase bytes (1-32). Empty when lock_now is true.
|
||||
Capped to 32 to match the proto cap on related security fields.
|
||||
"""
|
||||
boots_remaining: builtins.int
|
||||
"""
|
||||
Optional override of the boot-count token TTL granted on success.
|
||||
0 = use firmware default (TOKEN_DEFAULT_BOOTS).
|
||||
On reboot the firmware decrements this; when it reaches 0 the
|
||||
device boots fully locked and requires a fresh passphrase.
|
||||
"""
|
||||
valid_until_epoch: builtins.int
|
||||
"""
|
||||
Optional wall-clock expiry for the unlock token, as absolute
|
||||
Unix-epoch seconds. 0 = no time limit (only the boot-count TTL
|
||||
applies). On boot, if the device RTC is set and now > this value,
|
||||
the token is treated as expired.
|
||||
"""
|
||||
lock_now: builtins.bool
|
||||
"""
|
||||
If true, ignore passphrase fields, immediately revoke all
|
||||
connection-level admin authorization, and reboot the device into
|
||||
the locked state. Always honoured regardless of current lock state.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
passphrase: builtins.bytes = ...,
|
||||
boots_remaining: builtins.int = ...,
|
||||
valid_until_epoch: builtins.int = ...,
|
||||
lock_now: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["boots_remaining", b"boots_remaining", "lock_now", b"lock_now", "passphrase", b"passphrase", "valid_until_epoch", b"valid_until_epoch"]) -> None: ...
|
||||
|
||||
global___LockdownAuth = LockdownAuth
|
||||
|
||||
@typing.final
|
||||
class HamParameters(google.protobuf.message.Message):
|
||||
"""
|
||||
@@ -699,3 +953,352 @@ class NodeRemoteHardwarePinsResponse(google.protobuf.message.Message):
|
||||
def ClearField(self, field_name: typing.Literal["node_remote_hardware_pins", b"node_remote_hardware_pins"]) -> None: ...
|
||||
|
||||
global___NodeRemoteHardwarePinsResponse = NodeRemoteHardwarePinsResponse
|
||||
|
||||
@typing.final
|
||||
class SharedContact(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
NODE_NUM_FIELD_NUMBER: builtins.int
|
||||
USER_FIELD_NUMBER: builtins.int
|
||||
SHOULD_IGNORE_FIELD_NUMBER: builtins.int
|
||||
MANUALLY_VERIFIED_FIELD_NUMBER: builtins.int
|
||||
node_num: builtins.int
|
||||
"""
|
||||
The node number of the contact
|
||||
"""
|
||||
should_ignore: builtins.bool
|
||||
"""
|
||||
Add this contact to the blocked / ignored list
|
||||
"""
|
||||
manually_verified: builtins.bool
|
||||
"""
|
||||
Set the IS_KEY_MANUALLY_VERIFIED bit
|
||||
"""
|
||||
@property
|
||||
def user(self) -> meshtastic.protobuf.mesh_pb2.User:
|
||||
"""
|
||||
The User of the contact
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
node_num: builtins.int = ...,
|
||||
user: meshtastic.protobuf.mesh_pb2.User | None = ...,
|
||||
should_ignore: builtins.bool = ...,
|
||||
manually_verified: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["user", b"user"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["manually_verified", b"manually_verified", "node_num", b"node_num", "should_ignore", b"should_ignore", "user", b"user"]) -> None: ...
|
||||
|
||||
global___SharedContact = SharedContact
|
||||
|
||||
@typing.final
|
||||
class KeyVerificationAdmin(google.protobuf.message.Message):
|
||||
"""
|
||||
This message is used by a client to initiate or complete a key verification
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _MessageType:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _MessageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[KeyVerificationAdmin._MessageType.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
INITIATE_VERIFICATION: KeyVerificationAdmin._MessageType.ValueType # 0
|
||||
"""
|
||||
This is the first stage, where a client initiates
|
||||
"""
|
||||
PROVIDE_SECURITY_NUMBER: KeyVerificationAdmin._MessageType.ValueType # 1
|
||||
"""
|
||||
After the nonce has been returned over the mesh, the client prompts for the security number
|
||||
And uses this message to provide it to the node.
|
||||
"""
|
||||
DO_VERIFY: KeyVerificationAdmin._MessageType.ValueType # 2
|
||||
"""
|
||||
Once the user has compared the verification message, this message notifies the node.
|
||||
"""
|
||||
DO_NOT_VERIFY: KeyVerificationAdmin._MessageType.ValueType # 3
|
||||
"""
|
||||
This is the cancel path, can be taken at any point
|
||||
"""
|
||||
|
||||
class MessageType(_MessageType, metaclass=_MessageTypeEnumTypeWrapper):
|
||||
"""
|
||||
Three stages of this request.
|
||||
"""
|
||||
|
||||
INITIATE_VERIFICATION: KeyVerificationAdmin.MessageType.ValueType # 0
|
||||
"""
|
||||
This is the first stage, where a client initiates
|
||||
"""
|
||||
PROVIDE_SECURITY_NUMBER: KeyVerificationAdmin.MessageType.ValueType # 1
|
||||
"""
|
||||
After the nonce has been returned over the mesh, the client prompts for the security number
|
||||
And uses this message to provide it to the node.
|
||||
"""
|
||||
DO_VERIFY: KeyVerificationAdmin.MessageType.ValueType # 2
|
||||
"""
|
||||
Once the user has compared the verification message, this message notifies the node.
|
||||
"""
|
||||
DO_NOT_VERIFY: KeyVerificationAdmin.MessageType.ValueType # 3
|
||||
"""
|
||||
This is the cancel path, can be taken at any point
|
||||
"""
|
||||
|
||||
MESSAGE_TYPE_FIELD_NUMBER: builtins.int
|
||||
REMOTE_NODENUM_FIELD_NUMBER: builtins.int
|
||||
NONCE_FIELD_NUMBER: builtins.int
|
||||
SECURITY_NUMBER_FIELD_NUMBER: builtins.int
|
||||
message_type: global___KeyVerificationAdmin.MessageType.ValueType
|
||||
remote_nodenum: builtins.int
|
||||
"""
|
||||
The nodenum we're requesting
|
||||
"""
|
||||
nonce: builtins.int
|
||||
"""
|
||||
The nonce is used to track the connection
|
||||
"""
|
||||
security_number: builtins.int
|
||||
"""
|
||||
The 4 digit code generated by the remote node, and communicated outside the mesh
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
message_type: global___KeyVerificationAdmin.MessageType.ValueType = ...,
|
||||
remote_nodenum: builtins.int = ...,
|
||||
nonce: builtins.int = ...,
|
||||
security_number: builtins.int | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_security_number", b"_security_number", "security_number", b"security_number"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_security_number", b"_security_number", "message_type", b"message_type", "nonce", b"nonce", "remote_nodenum", b"remote_nodenum", "security_number", b"security_number"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_security_number", b"_security_number"]) -> typing.Literal["security_number"] | None: ...
|
||||
|
||||
global___KeyVerificationAdmin = KeyVerificationAdmin
|
||||
|
||||
@typing.final
|
||||
class SensorConfig(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
SCD4X_CONFIG_FIELD_NUMBER: builtins.int
|
||||
SEN5X_CONFIG_FIELD_NUMBER: builtins.int
|
||||
SCD30_CONFIG_FIELD_NUMBER: builtins.int
|
||||
SHTXX_CONFIG_FIELD_NUMBER: builtins.int
|
||||
@property
|
||||
def scd4x_config(self) -> global___SCD4X_config:
|
||||
"""
|
||||
SCD4X CO2 Sensor configuration
|
||||
"""
|
||||
|
||||
@property
|
||||
def sen5x_config(self) -> global___SEN5X_config:
|
||||
"""
|
||||
SEN5X PM Sensor configuration
|
||||
"""
|
||||
|
||||
@property
|
||||
def scd30_config(self) -> global___SCD30_config:
|
||||
"""
|
||||
SCD30 CO2 Sensor configuration
|
||||
"""
|
||||
|
||||
@property
|
||||
def shtxx_config(self) -> global___SHTXX_config:
|
||||
"""
|
||||
SHTXX temperature and relative humidity sensor configuration
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
scd4x_config: global___SCD4X_config | None = ...,
|
||||
sen5x_config: global___SEN5X_config | None = ...,
|
||||
scd30_config: global___SCD30_config | None = ...,
|
||||
shtxx_config: global___SHTXX_config | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "shtxx_config", b"shtxx_config"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "shtxx_config", b"shtxx_config"]) -> None: ...
|
||||
|
||||
global___SensorConfig = SensorConfig
|
||||
|
||||
@typing.final
|
||||
class SCD4X_config(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
SET_ASC_FIELD_NUMBER: builtins.int
|
||||
SET_TARGET_CO2_CONC_FIELD_NUMBER: builtins.int
|
||||
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
SET_ALTITUDE_FIELD_NUMBER: builtins.int
|
||||
SET_AMBIENT_PRESSURE_FIELD_NUMBER: builtins.int
|
||||
FACTORY_RESET_FIELD_NUMBER: builtins.int
|
||||
SET_POWER_MODE_FIELD_NUMBER: builtins.int
|
||||
set_asc: builtins.bool
|
||||
"""
|
||||
Set Automatic self-calibration enabled
|
||||
"""
|
||||
set_target_co2_conc: builtins.int
|
||||
"""
|
||||
Recalibration target CO2 concentration in ppm (FRC or ASC)
|
||||
"""
|
||||
set_temperature: builtins.float
|
||||
"""
|
||||
Reference temperature in degC
|
||||
"""
|
||||
set_altitude: builtins.int
|
||||
"""
|
||||
Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure)
|
||||
"""
|
||||
set_ambient_pressure: builtins.int
|
||||
"""
|
||||
Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude)
|
||||
"""
|
||||
factory_reset: builtins.bool
|
||||
"""
|
||||
Perform a factory reset of the sensor
|
||||
"""
|
||||
set_power_mode: builtins.bool
|
||||
"""
|
||||
Power mode for sensor (true for low power, false for normal)
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
set_asc: builtins.bool | None = ...,
|
||||
set_target_co2_conc: builtins.int | None = ...,
|
||||
set_temperature: builtins.float | None = ...,
|
||||
set_altitude: builtins.int | None = ...,
|
||||
set_ambient_pressure: builtins.int | None = ...,
|
||||
factory_reset: builtins.bool | None = ...,
|
||||
set_power_mode: builtins.bool | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_factory_reset", b"_factory_reset", "_set_altitude", b"_set_altitude", "_set_ambient_pressure", b"_set_ambient_pressure", "_set_asc", b"_set_asc", "_set_power_mode", b"_set_power_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_power_mode", b"set_power_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_factory_reset", b"_factory_reset", "_set_altitude", b"_set_altitude", "_set_ambient_pressure", b"_set_ambient_pressure", "_set_asc", b"_set_asc", "_set_power_mode", b"_set_power_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_power_mode", b"set_power_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_factory_reset", b"_factory_reset"]) -> typing.Literal["factory_reset"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_altitude", b"_set_altitude"]) -> typing.Literal["set_altitude"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_ambient_pressure", b"_set_ambient_pressure"]) -> typing.Literal["set_ambient_pressure"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_asc", b"_set_asc"]) -> typing.Literal["set_asc"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_power_mode", b"_set_power_mode"]) -> typing.Literal["set_power_mode"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_target_co2_conc", b"_set_target_co2_conc"]) -> typing.Literal["set_target_co2_conc"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ...
|
||||
|
||||
global___SCD4X_config = SCD4X_config
|
||||
|
||||
@typing.final
|
||||
class SEN5X_config(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
SET_ONE_SHOT_MODE_FIELD_NUMBER: builtins.int
|
||||
set_temperature: builtins.float
|
||||
"""
|
||||
Reference temperature in degC
|
||||
"""
|
||||
set_one_shot_mode: builtins.bool
|
||||
"""
|
||||
One-shot mode (true for low power - one-shot mode, false for normal - continuous mode)
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
set_temperature: builtins.float | None = ...,
|
||||
set_one_shot_mode: builtins.bool | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode", "_set_temperature", b"_set_temperature", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode", "_set_temperature", b"_set_temperature", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode"]) -> typing.Literal["set_one_shot_mode"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ...
|
||||
|
||||
global___SEN5X_config = SEN5X_config
|
||||
|
||||
@typing.final
|
||||
class SCD30_config(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
SET_ASC_FIELD_NUMBER: builtins.int
|
||||
SET_TARGET_CO2_CONC_FIELD_NUMBER: builtins.int
|
||||
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
SET_ALTITUDE_FIELD_NUMBER: builtins.int
|
||||
SET_MEASUREMENT_INTERVAL_FIELD_NUMBER: builtins.int
|
||||
SOFT_RESET_FIELD_NUMBER: builtins.int
|
||||
set_asc: builtins.bool
|
||||
"""
|
||||
Set Automatic self-calibration enabled
|
||||
"""
|
||||
set_target_co2_conc: builtins.int
|
||||
"""
|
||||
Recalibration target CO2 concentration in ppm (FRC or ASC)
|
||||
"""
|
||||
set_temperature: builtins.float
|
||||
"""
|
||||
Reference temperature in degC
|
||||
"""
|
||||
set_altitude: builtins.int
|
||||
"""
|
||||
Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure)
|
||||
"""
|
||||
set_measurement_interval: builtins.int
|
||||
"""
|
||||
Power mode for sensor (true for low power, false for normal)
|
||||
"""
|
||||
soft_reset: builtins.bool
|
||||
"""
|
||||
Perform a factory reset of the sensor
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
set_asc: builtins.bool | None = ...,
|
||||
set_target_co2_conc: builtins.int | None = ...,
|
||||
set_temperature: builtins.float | None = ...,
|
||||
set_altitude: builtins.int | None = ...,
|
||||
set_measurement_interval: builtins.int | None = ...,
|
||||
soft_reset: builtins.bool | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_set_altitude", b"_set_altitude", "_set_asc", b"_set_asc", "_set_measurement_interval", b"_set_measurement_interval", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_soft_reset", b"_soft_reset", "set_altitude", b"set_altitude", "set_asc", b"set_asc", "set_measurement_interval", b"set_measurement_interval", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "soft_reset", b"soft_reset"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_set_altitude", b"_set_altitude", "_set_asc", b"_set_asc", "_set_measurement_interval", b"_set_measurement_interval", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_soft_reset", b"_soft_reset", "set_altitude", b"set_altitude", "set_asc", b"set_asc", "set_measurement_interval", b"set_measurement_interval", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "soft_reset", b"soft_reset"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_altitude", b"_set_altitude"]) -> typing.Literal["set_altitude"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_asc", b"_set_asc"]) -> typing.Literal["set_asc"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_measurement_interval", b"_set_measurement_interval"]) -> typing.Literal["set_measurement_interval"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_target_co2_conc", b"_set_target_co2_conc"]) -> typing.Literal["set_target_co2_conc"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_temperature", b"_set_temperature"]) -> typing.Literal["set_temperature"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_soft_reset", b"_soft_reset"]) -> typing.Literal["soft_reset"] | None: ...
|
||||
|
||||
global___SCD30_config = SCD30_config
|
||||
|
||||
@typing.final
|
||||
class SHTXX_config(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
SET_ACCURACY_FIELD_NUMBER: builtins.int
|
||||
set_accuracy: builtins.int
|
||||
"""
|
||||
Accuracy mode (0 = low, 1 = medium, 2 = high)
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
set_accuracy: builtins.int | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_set_accuracy", b"_set_accuracy", "set_accuracy", b"set_accuracy"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_set_accuracy", b"_set_accuracy", "set_accuracy", b"set_accuracy"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_set_accuracy", b"_set_accuracy"]) -> typing.Literal["set_accuracy"] | None: ...
|
||||
|
||||
global___SHTXX_config = SHTXX_config
|
||||
|
||||
11
meshtastic/protobuf/apponly_pb2.py
generated
11
meshtastic/protobuf/apponly_pb2.py
generated
@@ -13,16 +13,19 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
from meshtastic.protobuf import channel_pb2 as meshtastic_dot_protobuf_dot_channel__pb2
|
||||
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/apponly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\"\x81\x01\n\nChannelSet\x12\x36\n\x08settings\x18\x01 \x03(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12;\n\x0blora_config\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfigBb\n\x13\x63om.geeksville.meshB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/apponly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a meshtastic/protobuf/nanopb.proto\"\x88\x01\n\nChannelSet\x12=\n\x08settings\x18\x01 \x03(\x0b\x32$.meshtastic.protobuf.ChannelSettingsB\x05\x92?\x02\x10\x08\x12;\n\x0blora_config\x18\x02 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfigBc\n\x14org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.apponly_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_CHANNELSET']._serialized_start=128
|
||||
_globals['_CHANNELSET']._serialized_end=257
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_CHANNELSET.fields_by_name['settings']._options = None
|
||||
_CHANNELSET.fields_by_name['settings']._serialized_options = b'\222?\002\020\010'
|
||||
_globals['_CHANNELSET']._serialized_start=162
|
||||
_globals['_CHANNELSET']._serialized_end=298
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
249
meshtastic/protobuf/atak_pb2.py
generated
249
meshtastic/protobuf/atak_pb2.py
generated
File diff suppressed because one or more lines are too long
3382
meshtastic/protobuf/atak_pb2.pyi
generated
3382
meshtastic/protobuf/atak_pb2.pyi
generated
File diff suppressed because it is too large
Load Diff
11
meshtastic/protobuf/cannedmessages_pb2.py
generated
11
meshtastic/protobuf/cannedmessages_pb2.py
generated
@@ -11,16 +11,19 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(meshtastic/protobuf/cannedmessages.proto\x12\x13meshtastic.protobuf\"-\n\x19\x43\x61nnedMessageModuleConfig\x12\x10\n\x08messages\x18\x01 \x01(\tBn\n\x13\x63om.geeksville.meshB\x19\x43\x61nnedMessageConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(meshtastic/protobuf/cannedmessages.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"5\n\x19\x43\x61nnedMessageModuleConfig\x12\x18\n\x08messages\x18\x01 \x01(\tB\x06\x92?\x03\x08\xc9\x01\x42o\n\x14org.meshtastic.protoB\x19\x43\x61nnedMessageConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cannedmessages_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=65
|
||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=110
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_CANNEDMESSAGEMODULECONFIG.fields_by_name['messages']._options = None
|
||||
_CANNEDMESSAGEMODULECONFIG.fields_by_name['messages']._serialized_options = b'\222?\003\010\311\001'
|
||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=99
|
||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=152
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
27
meshtastic/protobuf/channel_pb2.py
generated
27
meshtastic/protobuf/channel_pb2.py
generated
@@ -11,24 +11,31 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/channel.proto\x12\x13meshtastic.protobuf\"\xc1\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x0b\n\x03psk\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12<\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32#.meshtastic.protobuf.ModuleSettings\"E\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x17\n\x0fis_client_muted\x18\x02 \x01(\x08\"\xb3\x01\n\x07\x43hannel\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x36\n\x08settings\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12/\n\x04role\x18\x03 \x01(\x0e\x32!.meshtastic.protobuf.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x62\n\x13\x63om.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!meshtastic/protobuf/channel.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xcf\x01\n\x0f\x43hannelSettings\x12\x17\n\x0b\x63hannel_num\x18\x01 \x01(\rB\x02\x18\x01\x12\x12\n\x03psk\x18\x02 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x13\n\x04name\x18\x03 \x01(\tB\x05\x92?\x02\x08\x0c\x12\n\n\x02id\x18\x04 \x01(\x07\x12\x16\n\x0euplink_enabled\x18\x05 \x01(\x08\x12\x18\n\x10\x64ownlink_enabled\x18\x06 \x01(\x08\x12<\n\x0fmodule_settings\x18\x07 \x01(\x0b\x32#.meshtastic.protobuf.ModuleSettings\">\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x10\n\x08is_muted\x18\x02 \x01(\x08\"\xba\x01\n\x07\x43hannel\x12\x14\n\x05index\x18\x01 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x36\n\x08settings\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12/\n\x04role\x18\x03 \x01(\x0e\x32!.meshtastic.protobuf.Channel.Role\"0\n\x04Role\x12\x0c\n\x08\x44ISABLED\x10\x00\x12\x0b\n\x07PRIMARY\x10\x01\x12\r\n\tSECONDARY\x10\x02\x42\x63\n\x14org.meshtastic.protoB\rChannelProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.channel_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\rChannelProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rChannelProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_CHANNELSETTINGS.fields_by_name['channel_num']._options = None
|
||||
_CHANNELSETTINGS.fields_by_name['channel_num']._serialized_options = b'\030\001'
|
||||
_globals['_CHANNELSETTINGS']._serialized_start=59
|
||||
_globals['_CHANNELSETTINGS']._serialized_end=252
|
||||
_globals['_MODULESETTINGS']._serialized_start=254
|
||||
_globals['_MODULESETTINGS']._serialized_end=323
|
||||
_globals['_CHANNEL']._serialized_start=326
|
||||
_globals['_CHANNEL']._serialized_end=505
|
||||
_globals['_CHANNEL_ROLE']._serialized_start=457
|
||||
_globals['_CHANNEL_ROLE']._serialized_end=505
|
||||
_CHANNELSETTINGS.fields_by_name['psk']._options = None
|
||||
_CHANNELSETTINGS.fields_by_name['psk']._serialized_options = b'\222?\002\010 '
|
||||
_CHANNELSETTINGS.fields_by_name['name']._options = None
|
||||
_CHANNELSETTINGS.fields_by_name['name']._serialized_options = b'\222?\002\010\014'
|
||||
_CHANNEL.fields_by_name['index']._options = None
|
||||
_CHANNEL.fields_by_name['index']._serialized_options = b'\222?\0028\010'
|
||||
_globals['_CHANNELSETTINGS']._serialized_start=93
|
||||
_globals['_CHANNELSETTINGS']._serialized_end=300
|
||||
_globals['_MODULESETTINGS']._serialized_start=302
|
||||
_globals['_MODULESETTINGS']._serialized_end=364
|
||||
_globals['_CHANNEL']._serialized_start=367
|
||||
_globals['_CHANNEL']._serialized_end=553
|
||||
_globals['_CHANNEL_ROLE']._serialized_start=505
|
||||
_globals['_CHANNEL_ROLE']._serialized_end=553
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
12
meshtastic/protobuf/channel_pb2.pyi
generated
12
meshtastic/protobuf/channel_pb2.pyi
generated
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
@generated by mypy-protobuf. Do not edit manually!
|
||||
isort:skip_file
|
||||
"""
|
||||
trunk-ignore(buf-lint/PACKAGE_DIRECTORY_MATCH)"""
|
||||
|
||||
import builtins
|
||||
import google.protobuf.descriptor
|
||||
@@ -127,23 +127,23 @@ class ModuleSettings(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
POSITION_PRECISION_FIELD_NUMBER: builtins.int
|
||||
IS_CLIENT_MUTED_FIELD_NUMBER: builtins.int
|
||||
IS_MUTED_FIELD_NUMBER: builtins.int
|
||||
position_precision: builtins.int
|
||||
"""
|
||||
Bits of precision for the location sent in position packets.
|
||||
"""
|
||||
is_client_muted: builtins.bool
|
||||
is_muted: builtins.bool
|
||||
"""
|
||||
Controls whether or not the phone / clients should mute the current channel
|
||||
Controls whether or not the client / device should mute the current channel
|
||||
Useful for noisy public channels you don't necessarily want to disable
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
position_precision: builtins.int = ...,
|
||||
is_client_muted: builtins.bool = ...,
|
||||
is_muted: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["is_client_muted", b"is_client_muted", "position_precision", b"position_precision"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["is_muted", b"is_muted", "position_precision", b"position_precision"]) -> None: ...
|
||||
|
||||
global___ModuleSettings = ModuleSettings
|
||||
|
||||
|
||||
17
meshtastic/protobuf/clientonly_pb2.py
generated
17
meshtastic/protobuf/clientonly_pb2.py
generated
@@ -13,16 +13,25 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
from meshtastic.protobuf import localonly_pb2 as meshtastic_dot_protobuf_dot_localonly__pb2
|
||||
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/clientonly.proto\x12\x13meshtastic.protobuf\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"\xc4\x03\n\rDeviceProfile\x12\x16\n\tlong_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x17\n\nshort_name\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x18\n\x0b\x63hannel_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x06\x63onfig\x18\x04 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfigH\x03\x88\x01\x01\x12\x42\n\rmodule_config\x18\x05 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfigH\x04\x88\x01\x01\x12:\n\x0e\x66ixed_position\x18\x06 \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x05\x88\x01\x01\x12\x15\n\x08ringtone\x18\x07 \x01(\tH\x06\x88\x01\x01\x12\x1c\n\x0f\x63\x61nned_messages\x18\x08 \x01(\tH\x07\x88\x01\x01\x42\x0c\n\n_long_nameB\r\n\x0b_short_nameB\x0e\n\x0c_channel_urlB\t\n\x07_configB\x10\n\x0e_module_configB\x11\n\x0f_fixed_positionB\x0b\n\t_ringtoneB\x12\n\x10_canned_messagesBe\n\x13\x63om.geeksville.meshB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/clientonly.proto\x12\x13meshtastic.protobuf\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a meshtastic/protobuf/nanopb.proto\"\xe2\x03\n\rDeviceProfile\x12\x1d\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08(H\x00\x88\x01\x01\x12\x1e\n\nshort_name\x18\x02 \x01(\tB\x05\x92?\x02\x08\x05H\x01\x88\x01\x01\x12\x18\n\x0b\x63hannel_url\x18\x03 \x01(\tH\x02\x88\x01\x01\x12\x35\n\x06\x63onfig\x18\x04 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfigH\x03\x88\x01\x01\x12\x42\n\rmodule_config\x18\x05 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfigH\x04\x88\x01\x01\x12:\n\x0e\x66ixed_position\x18\x06 \x01(\x0b\x32\x1d.meshtastic.protobuf.PositionH\x05\x88\x01\x01\x12\x1d\n\x08ringtone\x18\x07 \x01(\tB\x06\x92?\x03\x08\xe7\x01H\x06\x88\x01\x01\x12$\n\x0f\x63\x61nned_messages\x18\x08 \x01(\tB\x06\x92?\x03\x08\xc9\x01H\x07\x88\x01\x01\x42\x0c\n\n_long_nameB\r\n\x0b_short_nameB\x0e\n\x0c_channel_urlB\t\n\x07_configB\x10\n\x0e_module_configB\x11\n\x0f_fixed_positionB\x0b\n\t_ringtoneB\x12\n\x10_canned_messagesBf\n\x14org.meshtastic.protoB\x10\x43lientOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.clientonly_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_DEVICEPROFILE']._serialized_start=131
|
||||
_globals['_DEVICEPROFILE']._serialized_end=583
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_DEVICEPROFILE.fields_by_name['long_name']._options = None
|
||||
_DEVICEPROFILE.fields_by_name['long_name']._serialized_options = b'\222?\002\010('
|
||||
_DEVICEPROFILE.fields_by_name['short_name']._options = None
|
||||
_DEVICEPROFILE.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
|
||||
_DEVICEPROFILE.fields_by_name['ringtone']._options = None
|
||||
_DEVICEPROFILE.fields_by_name['ringtone']._serialized_options = b'\222?\003\010\347\001'
|
||||
_DEVICEPROFILE.fields_by_name['canned_messages']._options = None
|
||||
_DEVICEPROFILE.fields_by_name['canned_messages']._serialized_options = b'\222?\003\010\311\001'
|
||||
_globals['_DEVICEPROFILE']._serialized_start=165
|
||||
_globals['_DEVICEPROFILE']._serialized_end=647
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
147
meshtastic/protobuf/config_pb2.py
generated
147
meshtastic/protobuf/config_pb2.py
generated
File diff suppressed because one or more lines are too long
421
meshtastic/protobuf/config_pb2.pyi
generated
421
meshtastic/protobuf/config_pb2.pyi
generated
@@ -64,6 +64,7 @@ class Config(google.protobuf.message.Message):
|
||||
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
|
||||
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
||||
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
|
||||
Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain.
|
||||
"""
|
||||
TRACKER: Config.DeviceConfig._Role.ValueType # 5
|
||||
"""
|
||||
@@ -116,6 +117,13 @@ class Config(google.protobuf.message.Message):
|
||||
but should not be given priority over other routers in order to avoid unnecessaraily
|
||||
consuming hops.
|
||||
"""
|
||||
CLIENT_BASE: Config.DeviceConfig._Role.ValueType # 12
|
||||
"""
|
||||
Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.
|
||||
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
|
||||
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
|
||||
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node.
|
||||
"""
|
||||
|
||||
class Role(_Role, metaclass=_RoleEnumTypeWrapper):
|
||||
"""
|
||||
@@ -148,6 +156,7 @@ class Config(google.protobuf.message.Message):
|
||||
Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
|
||||
Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
||||
or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
|
||||
Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain.
|
||||
"""
|
||||
TRACKER: Config.DeviceConfig.Role.ValueType # 5
|
||||
"""
|
||||
@@ -200,6 +209,13 @@ class Config(google.protobuf.message.Message):
|
||||
but should not be given priority over other routers in order to avoid unnecessaraily
|
||||
consuming hops.
|
||||
"""
|
||||
CLIENT_BASE: Config.DeviceConfig.Role.ValueType # 12
|
||||
"""
|
||||
Description: Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.
|
||||
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
|
||||
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
|
||||
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node.
|
||||
"""
|
||||
|
||||
class _RebroadcastMode:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
@@ -272,6 +288,73 @@ class Config(google.protobuf.message.Message):
|
||||
Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing.
|
||||
"""
|
||||
|
||||
class _BuzzerMode:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _BuzzerModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DeviceConfig._BuzzerMode.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
ALL_ENABLED: Config.DeviceConfig._BuzzerMode.ValueType # 0
|
||||
"""
|
||||
Default behavior.
|
||||
Buzzer is enabled for all audio feedback including button presses and alerts.
|
||||
"""
|
||||
DISABLED: Config.DeviceConfig._BuzzerMode.ValueType # 1
|
||||
"""
|
||||
Disabled.
|
||||
All buzzer audio feedback is disabled.
|
||||
"""
|
||||
NOTIFICATIONS_ONLY: Config.DeviceConfig._BuzzerMode.ValueType # 2
|
||||
"""
|
||||
Notifications Only.
|
||||
Buzzer is enabled only for notifications and alerts, but not for button presses.
|
||||
External notification config determines the specifics of the notification behavior.
|
||||
"""
|
||||
SYSTEM_ONLY: Config.DeviceConfig._BuzzerMode.ValueType # 3
|
||||
"""
|
||||
Non-notification system buzzer tones only.
|
||||
Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts.
|
||||
"""
|
||||
DIRECT_MSG_ONLY: Config.DeviceConfig._BuzzerMode.ValueType # 4
|
||||
"""
|
||||
Direct Message notifications only.
|
||||
Buzzer is enabled only for direct messages and alerts, but not for button presses.
|
||||
External notification config determines the specifics of the notification behavior.
|
||||
"""
|
||||
|
||||
class BuzzerMode(_BuzzerMode, metaclass=_BuzzerModeEnumTypeWrapper):
|
||||
"""
|
||||
Defines buzzer behavior for audio feedback
|
||||
"""
|
||||
|
||||
ALL_ENABLED: Config.DeviceConfig.BuzzerMode.ValueType # 0
|
||||
"""
|
||||
Default behavior.
|
||||
Buzzer is enabled for all audio feedback including button presses and alerts.
|
||||
"""
|
||||
DISABLED: Config.DeviceConfig.BuzzerMode.ValueType # 1
|
||||
"""
|
||||
Disabled.
|
||||
All buzzer audio feedback is disabled.
|
||||
"""
|
||||
NOTIFICATIONS_ONLY: Config.DeviceConfig.BuzzerMode.ValueType # 2
|
||||
"""
|
||||
Notifications Only.
|
||||
Buzzer is enabled only for notifications and alerts, but not for button presses.
|
||||
External notification config determines the specifics of the notification behavior.
|
||||
"""
|
||||
SYSTEM_ONLY: Config.DeviceConfig.BuzzerMode.ValueType # 3
|
||||
"""
|
||||
Non-notification system buzzer tones only.
|
||||
Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts.
|
||||
"""
|
||||
DIRECT_MSG_ONLY: Config.DeviceConfig.BuzzerMode.ValueType # 4
|
||||
"""
|
||||
Direct Message notifications only.
|
||||
Buzzer is enabled only for direct messages and alerts, but not for button presses.
|
||||
External notification config determines the specifics of the notification behavior.
|
||||
"""
|
||||
|
||||
ROLE_FIELD_NUMBER: builtins.int
|
||||
SERIAL_ENABLED_FIELD_NUMBER: builtins.int
|
||||
BUTTON_GPIO_FIELD_NUMBER: builtins.int
|
||||
@@ -283,6 +366,7 @@ class Config(google.protobuf.message.Message):
|
||||
DISABLE_TRIPLE_CLICK_FIELD_NUMBER: builtins.int
|
||||
TZDEF_FIELD_NUMBER: builtins.int
|
||||
LED_HEARTBEAT_DISABLED_FIELD_NUMBER: builtins.int
|
||||
BUZZER_MODE_FIELD_NUMBER: builtins.int
|
||||
role: global___Config.DeviceConfig.Role.ValueType
|
||||
"""
|
||||
Sets the role of node
|
||||
@@ -333,6 +417,11 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
If true, disable the default blinking LED (LED_PIN) behavior on the device
|
||||
"""
|
||||
buzzer_mode: global___Config.DeviceConfig.BuzzerMode.ValueType
|
||||
"""
|
||||
Controls buzzer behavior for audio feedback
|
||||
Defaults to ENABLED
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -347,8 +436,9 @@ class Config(google.protobuf.message.Message):
|
||||
disable_triple_click: builtins.bool = ...,
|
||||
tzdef: builtins.str = ...,
|
||||
led_heartbeat_disabled: builtins.bool = ...,
|
||||
buzzer_mode: global___Config.DeviceConfig.BuzzerMode.ValueType = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "led_heartbeat_disabled", b"led_heartbeat_disabled", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["button_gpio", b"button_gpio", "buzzer_gpio", b"buzzer_gpio", "buzzer_mode", b"buzzer_mode", "disable_triple_click", b"disable_triple_click", "double_tap_as_button_press", b"double_tap_as_button_press", "is_managed", b"is_managed", "led_heartbeat_disabled", b"led_heartbeat_disabled", "node_info_broadcast_secs", b"node_info_broadcast_secs", "rebroadcast_mode", b"rebroadcast_mode", "role", b"role", "serial_enabled", b"serial_enabled", "tzdef", b"tzdef"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class PositionConfig(google.protobuf.message.Message):
|
||||
@@ -781,6 +871,7 @@ class Config(google.protobuf.message.Message):
|
||||
IPV4_CONFIG_FIELD_NUMBER: builtins.int
|
||||
RSYSLOG_SERVER_FIELD_NUMBER: builtins.int
|
||||
ENABLED_PROTOCOLS_FIELD_NUMBER: builtins.int
|
||||
IPV6_ENABLED_FIELD_NUMBER: builtins.int
|
||||
wifi_enabled: builtins.bool
|
||||
"""
|
||||
Enable WiFi (disables Bluetooth)
|
||||
@@ -814,6 +905,10 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
Flags for enabling/disabling network protocols
|
||||
"""
|
||||
ipv6_enabled: builtins.bool
|
||||
"""
|
||||
Enable/Disable ipv6 support
|
||||
"""
|
||||
@property
|
||||
def ipv4_config(self) -> global___Config.NetworkConfig.IpV4Config:
|
||||
"""
|
||||
@@ -832,9 +927,10 @@ class Config(google.protobuf.message.Message):
|
||||
ipv4_config: global___Config.NetworkConfig.IpV4Config | None = ...,
|
||||
rsyslog_server: builtins.str = ...,
|
||||
enabled_protocols: builtins.int = ...,
|
||||
ipv6_enabled: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["ipv4_config", b"ipv4_config"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["address_mode", b"address_mode", "enabled_protocols", b"enabled_protocols", "eth_enabled", b"eth_enabled", "ipv4_config", b"ipv4_config", "ntp_server", b"ntp_server", "rsyslog_server", b"rsyslog_server", "wifi_enabled", b"wifi_enabled", "wifi_psk", b"wifi_psk", "wifi_ssid", b"wifi_ssid"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["address_mode", b"address_mode", "enabled_protocols", b"enabled_protocols", "eth_enabled", b"eth_enabled", "ipv4_config", b"ipv4_config", "ipv6_enabled", b"ipv6_enabled", "ntp_server", b"ntp_server", "rsyslog_server", b"rsyslog_server", "wifi_enabled", b"wifi_enabled", "wifi_psk", b"wifi_psk", "wifi_ssid", b"wifi_ssid"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class DisplayConfig(google.protobuf.message.Message):
|
||||
@@ -844,80 +940,20 @@ class Config(google.protobuf.message.Message):
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _GpsCoordinateFormat:
|
||||
class _DeprecatedGpsCoordinateFormat:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _GpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DisplayConfig._GpsCoordinateFormat.ValueType], builtins.type):
|
||||
class _DeprecatedGpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.DisplayConfig._DeprecatedGpsCoordinateFormat.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
DEC: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 0
|
||||
UNUSED: Config.DisplayConfig._DeprecatedGpsCoordinateFormat.ValueType # 0
|
||||
|
||||
class DeprecatedGpsCoordinateFormat(_DeprecatedGpsCoordinateFormat, metaclass=_DeprecatedGpsCoordinateFormatEnumTypeWrapper):
|
||||
"""
|
||||
GPS coordinates are displayed in the normal decimal degrees format:
|
||||
DD.DDDDDD DDD.DDDDDD
|
||||
"""
|
||||
DMS: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 1
|
||||
"""
|
||||
GPS coordinates are displayed in the degrees minutes seconds format:
|
||||
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
||||
"""
|
||||
UTM: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 2
|
||||
"""
|
||||
Universal Transverse Mercator format:
|
||||
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
||||
"""
|
||||
MGRS: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 3
|
||||
"""
|
||||
Military Grid Reference System format:
|
||||
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
||||
E is easting, N is northing
|
||||
"""
|
||||
OLC: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 4
|
||||
"""
|
||||
Open Location Code (aka Plus Codes).
|
||||
"""
|
||||
OSGR: Config.DisplayConfig._GpsCoordinateFormat.ValueType # 5
|
||||
"""
|
||||
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
||||
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
||||
E is the easting, N is the northing
|
||||
Deprecated in 2.7.4: Unused
|
||||
"""
|
||||
|
||||
class GpsCoordinateFormat(_GpsCoordinateFormat, metaclass=_GpsCoordinateFormatEnumTypeWrapper):
|
||||
"""
|
||||
How the GPS coordinates are displayed on the OLED screen.
|
||||
"""
|
||||
|
||||
DEC: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 0
|
||||
"""
|
||||
GPS coordinates are displayed in the normal decimal degrees format:
|
||||
DD.DDDDDD DDD.DDDDDD
|
||||
"""
|
||||
DMS: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 1
|
||||
"""
|
||||
GPS coordinates are displayed in the degrees minutes seconds format:
|
||||
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
||||
"""
|
||||
UTM: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 2
|
||||
"""
|
||||
Universal Transverse Mercator format:
|
||||
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
||||
"""
|
||||
MGRS: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 3
|
||||
"""
|
||||
Military Grid Reference System format:
|
||||
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
||||
E is easting, N is northing
|
||||
"""
|
||||
OLC: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 4
|
||||
"""
|
||||
Open Location Code (aka Plus Codes).
|
||||
"""
|
||||
OSGR: Config.DisplayConfig.GpsCoordinateFormat.ValueType # 5
|
||||
"""
|
||||
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
||||
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
||||
E is the easting, N is the northing
|
||||
"""
|
||||
UNUSED: Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType # 0
|
||||
|
||||
class _DisplayUnits:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
@@ -956,20 +992,28 @@ class Config(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
OLED_AUTO: Config.DisplayConfig._OledType.ValueType # 0
|
||||
"""
|
||||
Default / Auto
|
||||
Default / Autodetect
|
||||
"""
|
||||
OLED_SSD1306: Config.DisplayConfig._OledType.ValueType # 1
|
||||
"""
|
||||
Default / Auto
|
||||
Default / Autodetect
|
||||
"""
|
||||
OLED_SH1106: Config.DisplayConfig._OledType.ValueType # 2
|
||||
"""
|
||||
Default / Auto
|
||||
Default / Autodetect
|
||||
"""
|
||||
OLED_SH1107: Config.DisplayConfig._OledType.ValueType # 3
|
||||
"""
|
||||
Can not be auto detected but set by proto. Used for 128x64 screens
|
||||
"""
|
||||
OLED_SH1107_128_128: Config.DisplayConfig._OledType.ValueType # 4
|
||||
"""
|
||||
Can not be auto detected but set by proto. Used for 128x128 screens
|
||||
"""
|
||||
OLED_SH1107_ROTATED: Config.DisplayConfig._OledType.ValueType # 5
|
||||
"""
|
||||
Can not be auto detected but set by proto. Used for 64x128 rotated screens
|
||||
"""
|
||||
|
||||
class OledType(_OledType, metaclass=_OledTypeEnumTypeWrapper):
|
||||
"""
|
||||
@@ -978,20 +1022,28 @@ class Config(google.protobuf.message.Message):
|
||||
|
||||
OLED_AUTO: Config.DisplayConfig.OledType.ValueType # 0
|
||||
"""
|
||||
Default / Auto
|
||||
Default / Autodetect
|
||||
"""
|
||||
OLED_SSD1306: Config.DisplayConfig.OledType.ValueType # 1
|
||||
"""
|
||||
Default / Auto
|
||||
Default / Autodetect
|
||||
"""
|
||||
OLED_SH1106: Config.DisplayConfig.OledType.ValueType # 2
|
||||
"""
|
||||
Default / Auto
|
||||
Default / Autodetect
|
||||
"""
|
||||
OLED_SH1107: Config.DisplayConfig.OledType.ValueType # 3
|
||||
"""
|
||||
Can not be auto detected but set by proto. Used for 128x64 screens
|
||||
"""
|
||||
OLED_SH1107_128_128: Config.DisplayConfig.OledType.ValueType # 4
|
||||
"""
|
||||
Can not be auto detected but set by proto. Used for 128x128 screens
|
||||
"""
|
||||
OLED_SH1107_ROTATED: Config.DisplayConfig.OledType.ValueType # 5
|
||||
"""
|
||||
Can not be auto detected but set by proto. Used for 64x128 rotated screens
|
||||
"""
|
||||
|
||||
class _DisplayMode:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
@@ -1119,13 +1171,16 @@ class Config(google.protobuf.message.Message):
|
||||
WAKE_ON_TAP_OR_MOTION_FIELD_NUMBER: builtins.int
|
||||
COMPASS_ORIENTATION_FIELD_NUMBER: builtins.int
|
||||
USE_12H_CLOCK_FIELD_NUMBER: builtins.int
|
||||
USE_LONG_NODE_NAME_FIELD_NUMBER: builtins.int
|
||||
ENABLE_MESSAGE_BUBBLES_FIELD_NUMBER: builtins.int
|
||||
screen_on_secs: builtins.int
|
||||
"""
|
||||
Number of seconds the screen stays on after pressing the user button or receiving a message
|
||||
0 for default of one minute MAXUINT for always on
|
||||
"""
|
||||
gps_format: global___Config.DisplayConfig.GpsCoordinateFormat.ValueType
|
||||
gps_format: global___Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType
|
||||
"""
|
||||
Deprecated in 2.7.4: Unused
|
||||
How the GPS coordinates are formatted on the OLED screen.
|
||||
"""
|
||||
auto_screen_carousel_secs: builtins.int
|
||||
@@ -1171,11 +1226,20 @@ class Config(google.protobuf.message.Message):
|
||||
If false (default), the device will display the time in 24-hour format on screen.
|
||||
If true, the device will display the time in 12-hour format on screen.
|
||||
"""
|
||||
use_long_node_name: builtins.bool
|
||||
"""
|
||||
If false (default), the device will use short names for various display screens.
|
||||
If true, node names will show in long format
|
||||
"""
|
||||
enable_message_bubbles: builtins.bool
|
||||
"""
|
||||
If true, the device will display message bubbles on screen.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
screen_on_secs: builtins.int = ...,
|
||||
gps_format: global___Config.DisplayConfig.GpsCoordinateFormat.ValueType = ...,
|
||||
gps_format: global___Config.DisplayConfig.DeprecatedGpsCoordinateFormat.ValueType = ...,
|
||||
auto_screen_carousel_secs: builtins.int = ...,
|
||||
compass_north_top: builtins.bool = ...,
|
||||
flip_screen: builtins.bool = ...,
|
||||
@@ -1186,8 +1250,10 @@ class Config(google.protobuf.message.Message):
|
||||
wake_on_tap_or_motion: builtins.bool = ...,
|
||||
compass_orientation: global___Config.DisplayConfig.CompassOrientation.ValueType = ...,
|
||||
use_12h_clock: builtins.bool = ...,
|
||||
use_long_node_name: builtins.bool = ...,
|
||||
enable_message_bubbles: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "use_12h_clock", b"use_12h_clock", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["auto_screen_carousel_secs", b"auto_screen_carousel_secs", "compass_north_top", b"compass_north_top", "compass_orientation", b"compass_orientation", "displaymode", b"displaymode", "enable_message_bubbles", b"enable_message_bubbles", "flip_screen", b"flip_screen", "gps_format", b"gps_format", "heading_bold", b"heading_bold", "oled", b"oled", "screen_on_secs", b"screen_on_secs", "units", b"units", "use_12h_clock", b"use_12h_clock", "use_long_node_name", b"use_long_node_name", "wake_on_tap_or_motion", b"wake_on_tap_or_motion"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class LoRaConfig(google.protobuf.message.Message):
|
||||
@@ -1291,6 +1357,51 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
Philippines 915mhz
|
||||
"""
|
||||
ANZ_433: Config.LoRaConfig._RegionCode.ValueType # 22
|
||||
"""
|
||||
Australia / New Zealand 433MHz
|
||||
"""
|
||||
KZ_433: Config.LoRaConfig._RegionCode.ValueType # 23
|
||||
"""
|
||||
Kazakhstan 433MHz
|
||||
"""
|
||||
KZ_863: Config.LoRaConfig._RegionCode.ValueType # 24
|
||||
"""
|
||||
Kazakhstan 863MHz
|
||||
"""
|
||||
NP_865: Config.LoRaConfig._RegionCode.ValueType # 25
|
||||
"""
|
||||
Nepal 865MHz
|
||||
"""
|
||||
BR_902: Config.LoRaConfig._RegionCode.ValueType # 26
|
||||
"""
|
||||
Brazil 902MHz
|
||||
"""
|
||||
ITU1_2M: Config.LoRaConfig._RegionCode.ValueType # 27
|
||||
"""
|
||||
ITU Region 1 Amateur Radio 2m band (144-146 MHz)
|
||||
"""
|
||||
ITU2_2M: Config.LoRaConfig._RegionCode.ValueType # 28
|
||||
"""
|
||||
ITU Region 2 Amateur Radio 2m band (144-148 MHz)
|
||||
"""
|
||||
EU_866: Config.LoRaConfig._RegionCode.ValueType # 29
|
||||
"""
|
||||
EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
|
||||
"""
|
||||
EU_874: Config.LoRaConfig._RegionCode.ValueType # 30
|
||||
"""
|
||||
EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD)
|
||||
"""
|
||||
EU_917: Config.LoRaConfig._RegionCode.ValueType # 31
|
||||
EU_N_868: Config.LoRaConfig._RegionCode.ValueType # 32
|
||||
"""
|
||||
EU 868MHz band, with narrow presets
|
||||
"""
|
||||
ITU3_2M: Config.LoRaConfig._RegionCode.ValueType # 33
|
||||
"""
|
||||
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
|
||||
"""
|
||||
|
||||
class RegionCode(_RegionCode, metaclass=_RegionCodeEnumTypeWrapper): ...
|
||||
UNSET: Config.LoRaConfig.RegionCode.ValueType # 0
|
||||
@@ -1381,6 +1492,51 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
Philippines 915mhz
|
||||
"""
|
||||
ANZ_433: Config.LoRaConfig.RegionCode.ValueType # 22
|
||||
"""
|
||||
Australia / New Zealand 433MHz
|
||||
"""
|
||||
KZ_433: Config.LoRaConfig.RegionCode.ValueType # 23
|
||||
"""
|
||||
Kazakhstan 433MHz
|
||||
"""
|
||||
KZ_863: Config.LoRaConfig.RegionCode.ValueType # 24
|
||||
"""
|
||||
Kazakhstan 863MHz
|
||||
"""
|
||||
NP_865: Config.LoRaConfig.RegionCode.ValueType # 25
|
||||
"""
|
||||
Nepal 865MHz
|
||||
"""
|
||||
BR_902: Config.LoRaConfig.RegionCode.ValueType # 26
|
||||
"""
|
||||
Brazil 902MHz
|
||||
"""
|
||||
ITU1_2M: Config.LoRaConfig.RegionCode.ValueType # 27
|
||||
"""
|
||||
ITU Region 1 Amateur Radio 2m band (144-146 MHz)
|
||||
"""
|
||||
ITU2_2M: Config.LoRaConfig.RegionCode.ValueType # 28
|
||||
"""
|
||||
ITU Region 2 Amateur Radio 2m band (144-148 MHz)
|
||||
"""
|
||||
EU_866: Config.LoRaConfig.RegionCode.ValueType # 29
|
||||
"""
|
||||
EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD)
|
||||
"""
|
||||
EU_874: Config.LoRaConfig.RegionCode.ValueType # 30
|
||||
"""
|
||||
EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD)
|
||||
"""
|
||||
EU_917: Config.LoRaConfig.RegionCode.ValueType # 31
|
||||
EU_N_868: Config.LoRaConfig.RegionCode.ValueType # 32
|
||||
"""
|
||||
EU 868MHz band, with narrow presets
|
||||
"""
|
||||
ITU3_2M: Config.LoRaConfig.RegionCode.ValueType # 33
|
||||
"""
|
||||
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
|
||||
"""
|
||||
|
||||
class _ModemPreset:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
@@ -1395,6 +1551,7 @@ class Config(google.protobuf.message.Message):
|
||||
LONG_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 1
|
||||
"""
|
||||
Long Range - Slow
|
||||
Deprecated in 2.7: Unpopular slow preset.
|
||||
"""
|
||||
VERY_LONG_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 2
|
||||
"""
|
||||
@@ -1427,6 +1584,36 @@ class Config(google.protobuf.message.Message):
|
||||
This is the fastest preset and the only one with 500kHz bandwidth.
|
||||
It is not legal to use in all regions due to this wider bandwidth.
|
||||
"""
|
||||
LONG_TURBO: Config.LoRaConfig._ModemPreset.ValueType # 9
|
||||
"""
|
||||
Long Range - Turbo
|
||||
This preset performs similarly to LongFast, but with 500Khz bandwidth.
|
||||
"""
|
||||
LITE_FAST: Config.LoRaConfig._ModemPreset.ValueType # 10
|
||||
"""
|
||||
Lite Fast
|
||||
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
||||
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC.
|
||||
"""
|
||||
LITE_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 11
|
||||
"""
|
||||
Lite Slow
|
||||
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
||||
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC.
|
||||
"""
|
||||
NARROW_FAST: Config.LoRaConfig._ModemPreset.ValueType # 12
|
||||
"""
|
||||
Narrow Fast
|
||||
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
||||
Comparable link budget to SHORT_SLOW, but with half the data rate.
|
||||
Intended to avoid interference with other devices.
|
||||
"""
|
||||
NARROW_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 13
|
||||
"""
|
||||
Narrow Slow
|
||||
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
||||
Comparable link budget and data rate to LONG_FAST.
|
||||
"""
|
||||
|
||||
class ModemPreset(_ModemPreset, metaclass=_ModemPresetEnumTypeWrapper):
|
||||
"""
|
||||
@@ -1441,6 +1628,7 @@ class Config(google.protobuf.message.Message):
|
||||
LONG_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 1
|
||||
"""
|
||||
Long Range - Slow
|
||||
Deprecated in 2.7: Unpopular slow preset.
|
||||
"""
|
||||
VERY_LONG_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 2
|
||||
"""
|
||||
@@ -1473,6 +1661,69 @@ class Config(google.protobuf.message.Message):
|
||||
This is the fastest preset and the only one with 500kHz bandwidth.
|
||||
It is not legal to use in all regions due to this wider bandwidth.
|
||||
"""
|
||||
LONG_TURBO: Config.LoRaConfig.ModemPreset.ValueType # 9
|
||||
"""
|
||||
Long Range - Turbo
|
||||
This preset performs similarly to LongFast, but with 500Khz bandwidth.
|
||||
"""
|
||||
LITE_FAST: Config.LoRaConfig.ModemPreset.ValueType # 10
|
||||
"""
|
||||
Lite Fast
|
||||
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
||||
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC.
|
||||
"""
|
||||
LITE_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 11
|
||||
"""
|
||||
Lite Slow
|
||||
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
||||
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC.
|
||||
"""
|
||||
NARROW_FAST: Config.LoRaConfig.ModemPreset.ValueType # 12
|
||||
"""
|
||||
Narrow Fast
|
||||
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
||||
Comparable link budget to SHORT_SLOW, but with half the data rate.
|
||||
Intended to avoid interference with other devices.
|
||||
"""
|
||||
NARROW_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 13
|
||||
"""
|
||||
Narrow Slow
|
||||
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
||||
Comparable link budget and data rate to LONG_FAST.
|
||||
"""
|
||||
|
||||
class _FEM_LNA_Mode:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _FEM_LNA_ModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.LoRaConfig._FEM_LNA_Mode.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
DISABLED: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 0
|
||||
"""
|
||||
FEM_LNA is present but disabled
|
||||
"""
|
||||
ENABLED: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 1
|
||||
"""
|
||||
FEM_LNA is present and enabled
|
||||
"""
|
||||
NOT_PRESENT: Config.LoRaConfig._FEM_LNA_Mode.ValueType # 2
|
||||
"""
|
||||
FEM_LNA is not present on the device
|
||||
"""
|
||||
|
||||
class FEM_LNA_Mode(_FEM_LNA_Mode, metaclass=_FEM_LNA_ModeEnumTypeWrapper): ...
|
||||
DISABLED: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 0
|
||||
"""
|
||||
FEM_LNA is present but disabled
|
||||
"""
|
||||
ENABLED: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 1
|
||||
"""
|
||||
FEM_LNA is present and enabled
|
||||
"""
|
||||
NOT_PRESENT: Config.LoRaConfig.FEM_LNA_Mode.ValueType # 2
|
||||
"""
|
||||
FEM_LNA is not present on the device
|
||||
"""
|
||||
|
||||
USE_PRESET_FIELD_NUMBER: builtins.int
|
||||
MODEM_PRESET_FIELD_NUMBER: builtins.int
|
||||
@@ -1492,6 +1743,8 @@ class Config(google.protobuf.message.Message):
|
||||
IGNORE_INCOMING_FIELD_NUMBER: builtins.int
|
||||
IGNORE_MQTT_FIELD_NUMBER: builtins.int
|
||||
CONFIG_OK_TO_MQTT_FIELD_NUMBER: builtins.int
|
||||
FEM_LNA_MODE_FIELD_NUMBER: builtins.int
|
||||
SERIAL_HAL_ONLY_FIELD_NUMBER: builtins.int
|
||||
use_preset: builtins.bool
|
||||
"""
|
||||
When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate`
|
||||
@@ -1589,6 +1842,14 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
Sets the ok_to_mqtt bit on outgoing packets
|
||||
"""
|
||||
fem_lna_mode: global___Config.LoRaConfig.FEM_LNA_Mode.ValueType
|
||||
"""
|
||||
Set where LORA FEM is enabled, disabled, or not present
|
||||
"""
|
||||
serial_hal_only: builtins.bool
|
||||
"""
|
||||
Don't use radiolib to initialize the radio, instead listen for a serialHal connection
|
||||
"""
|
||||
@property
|
||||
def ignore_incoming(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
|
||||
"""
|
||||
@@ -1618,8 +1879,10 @@ class Config(google.protobuf.message.Message):
|
||||
ignore_incoming: collections.abc.Iterable[builtins.int] | None = ...,
|
||||
ignore_mqtt: builtins.bool = ...,
|
||||
config_ok_to_mqtt: builtins.bool = ...,
|
||||
fem_lna_mode: global___Config.LoRaConfig.FEM_LNA_Mode.ValueType = ...,
|
||||
serial_hal_only: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["bandwidth", b"bandwidth", "channel_num", b"channel_num", "coding_rate", b"coding_rate", "config_ok_to_mqtt", b"config_ok_to_mqtt", "frequency_offset", b"frequency_offset", "hop_limit", b"hop_limit", "ignore_incoming", b"ignore_incoming", "ignore_mqtt", b"ignore_mqtt", "modem_preset", b"modem_preset", "override_duty_cycle", b"override_duty_cycle", "override_frequency", b"override_frequency", "pa_fan_disabled", b"pa_fan_disabled", "region", b"region", "spread_factor", b"spread_factor", "sx126x_rx_boosted_gain", b"sx126x_rx_boosted_gain", "tx_enabled", b"tx_enabled", "tx_power", b"tx_power", "use_preset", b"use_preset"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["bandwidth", b"bandwidth", "channel_num", b"channel_num", "coding_rate", b"coding_rate", "config_ok_to_mqtt", b"config_ok_to_mqtt", "fem_lna_mode", b"fem_lna_mode", "frequency_offset", b"frequency_offset", "hop_limit", b"hop_limit", "ignore_incoming", b"ignore_incoming", "ignore_mqtt", b"ignore_mqtt", "modem_preset", b"modem_preset", "override_duty_cycle", b"override_duty_cycle", "override_frequency", b"override_frequency", "pa_fan_disabled", b"pa_fan_disabled", "region", b"region", "serial_hal_only", b"serial_hal_only", "spread_factor", b"spread_factor", "sx126x_rx_boosted_gain", b"sx126x_rx_boosted_gain", "tx_enabled", b"tx_enabled", "tx_power", b"tx_power", "use_preset", b"use_preset"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class BluetoothConfig(google.protobuf.message.Message):
|
||||
|
||||
31
meshtastic/protobuf/connection_status_pb2.py
generated
31
meshtastic/protobuf/connection_status_pb2.py
generated
@@ -11,26 +11,29 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/connection_status.proto\x12\x13meshtastic.protobuf\"\xd5\x02\n\x16\x44\x65viceConnectionStatus\x12<\n\x04wifi\x18\x01 \x01(\x0b\x32).meshtastic.protobuf.WifiConnectionStatusH\x00\x88\x01\x01\x12\x44\n\x08\x65thernet\x18\x02 \x01(\x0b\x32-.meshtastic.protobuf.EthernetConnectionStatusH\x01\x88\x01\x01\x12\x46\n\tbluetooth\x18\x03 \x01(\x0b\x32..meshtastic.protobuf.BluetoothConnectionStatusH\x02\x88\x01\x01\x12@\n\x06serial\x18\x04 \x01(\x0b\x32+.meshtastic.protobuf.SerialConnectionStatusH\x03\x88\x01\x01\x42\x07\n\x05_wifiB\x0b\n\t_ethernetB\x0c\n\n_bluetoothB\t\n\x07_serial\"p\n\x14WifiConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\x12\x0c\n\x04ssid\x18\x02 \x01(\t\x12\x0c\n\x04rssi\x18\x03 \x01(\x05\"X\n\x18\x45thernetConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\"{\n\x17NetworkConnectionStatus\x12\x12\n\nip_address\x18\x01 \x01(\x07\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x12\x19\n\x11is_mqtt_connected\x18\x03 \x01(\x08\x12\x1b\n\x13is_syslog_connected\x18\x04 \x01(\x08\"L\n\x19\x42luetoothConnectionStatus\x12\x0b\n\x03pin\x18\x01 \x01(\r\x12\x0c\n\x04rssi\x18\x02 \x01(\x05\x12\x14\n\x0cis_connected\x18\x03 \x01(\x08\"<\n\x16SerialConnectionStatus\x12\x0c\n\x04\x62\x61ud\x18\x01 \x01(\r\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x42\x65\n\x13\x63om.geeksville.meshB\x10\x43onnStatusProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/connection_status.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xd5\x02\n\x16\x44\x65viceConnectionStatus\x12<\n\x04wifi\x18\x01 \x01(\x0b\x32).meshtastic.protobuf.WifiConnectionStatusH\x00\x88\x01\x01\x12\x44\n\x08\x65thernet\x18\x02 \x01(\x0b\x32-.meshtastic.protobuf.EthernetConnectionStatusH\x01\x88\x01\x01\x12\x46\n\tbluetooth\x18\x03 \x01(\x0b\x32..meshtastic.protobuf.BluetoothConnectionStatusH\x02\x88\x01\x01\x12@\n\x06serial\x18\x04 \x01(\x0b\x32+.meshtastic.protobuf.SerialConnectionStatusH\x03\x88\x01\x01\x42\x07\n\x05_wifiB\x0b\n\t_ethernetB\x0c\n\n_bluetoothB\t\n\x07_serial\"w\n\x14WifiConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\x12\x13\n\x04ssid\x18\x02 \x01(\tB\x05\x92?\x02\x08!\x12\x0c\n\x04rssi\x18\x03 \x01(\x05\"X\n\x18\x45thernetConnectionStatus\x12<\n\x06status\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.NetworkConnectionStatus\"{\n\x17NetworkConnectionStatus\x12\x12\n\nip_address\x18\x01 \x01(\x07\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x12\x19\n\x11is_mqtt_connected\x18\x03 \x01(\x08\x12\x1b\n\x13is_syslog_connected\x18\x04 \x01(\x08\"L\n\x19\x42luetoothConnectionStatus\x12\x0b\n\x03pin\x18\x01 \x01(\r\x12\x0c\n\x04rssi\x18\x02 \x01(\x05\x12\x14\n\x0cis_connected\x18\x03 \x01(\x08\"<\n\x16SerialConnectionStatus\x12\x0c\n\x04\x62\x61ud\x18\x01 \x01(\r\x12\x14\n\x0cis_connected\x18\x02 \x01(\x08\x42\x66\n\x14org.meshtastic.protoB\x10\x43onnStatusProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.connection_status_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\020ConnStatusProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_DEVICECONNECTIONSTATUS']._serialized_start=69
|
||||
_globals['_DEVICECONNECTIONSTATUS']._serialized_end=410
|
||||
_globals['_WIFICONNECTIONSTATUS']._serialized_start=412
|
||||
_globals['_WIFICONNECTIONSTATUS']._serialized_end=524
|
||||
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_start=526
|
||||
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_end=614
|
||||
_globals['_NETWORKCONNECTIONSTATUS']._serialized_start=616
|
||||
_globals['_NETWORKCONNECTIONSTATUS']._serialized_end=739
|
||||
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_start=741
|
||||
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_end=817
|
||||
_globals['_SERIALCONNECTIONSTATUS']._serialized_start=819
|
||||
_globals['_SERIALCONNECTIONSTATUS']._serialized_end=879
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ConnStatusProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_WIFICONNECTIONSTATUS.fields_by_name['ssid']._options = None
|
||||
_WIFICONNECTIONSTATUS.fields_by_name['ssid']._serialized_options = b'\222?\002\010!'
|
||||
_globals['_DEVICECONNECTIONSTATUS']._serialized_start=103
|
||||
_globals['_DEVICECONNECTIONSTATUS']._serialized_end=444
|
||||
_globals['_WIFICONNECTIONSTATUS']._serialized_start=446
|
||||
_globals['_WIFICONNECTIONSTATUS']._serialized_end=565
|
||||
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_start=567
|
||||
_globals['_ETHERNETCONNECTIONSTATUS']._serialized_end=655
|
||||
_globals['_NETWORKCONNECTIONSTATUS']._serialized_start=657
|
||||
_globals['_NETWORKCONNECTIONSTATUS']._serialized_end=780
|
||||
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_start=782
|
||||
_globals['_BLUETOOTHCONNECTIONSTATUS']._serialized_end=858
|
||||
_globals['_SERIALCONNECTIONSTATUS']._serialized_start=860
|
||||
_globals['_SERIALCONNECTIONSTATUS']._serialized_end=920
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
61
meshtastic/protobuf/device_ui_pb2.py
generated
61
meshtastic/protobuf/device_ui_pb2.py
generated
@@ -11,28 +11,57 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\"\xeb\x03\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x19\n\x11screen_brightness\x18\x02 \x01(\r\x12\x16\n\x0escreen_timeout\x18\x03 \x01(\r\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x14\n\x0cring_tone_id\x18\n \x01(\r\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x18\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\"\xa7\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x11\n\thops_away\x18\x04 \x01(\x05\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x11\n\tnode_name\x18\x06 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\"~\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x11\n\tnode_name\x18\x05 \x01(\t\"=\n\x08GeoPoint\x12\x0c\n\x04zoom\x18\x01 \x01(\x05\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"U\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\r\n\x05style\x18\x02 \x01(\t\x12\x12\n\nfollow_gps\x18\x03 \x01(\x08*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\x9a\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\r\n\tUKRAINIAN\x10\x10\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x63\n\x13\x63om.geeksville.meshB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/device_ui.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xa9\x06\n\x0e\x44\x65viceUIConfig\x12\x0f\n\x07version\x18\x01 \x01(\r\x12 \n\x11screen_brightness\x18\x02 \x01(\rB\x05\x92?\x02\x38\x08\x12\x1d\n\x0escreen_timeout\x18\x03 \x01(\rB\x05\x92?\x02\x38\x10\x12\x13\n\x0bscreen_lock\x18\x04 \x01(\x08\x12\x15\n\rsettings_lock\x18\x05 \x01(\x08\x12\x10\n\x08pin_code\x18\x06 \x01(\r\x12)\n\x05theme\x18\x07 \x01(\x0e\x32\x1a.meshtastic.protobuf.Theme\x12\x15\n\ralert_enabled\x18\x08 \x01(\x08\x12\x16\n\x0e\x62\x61nner_enabled\x18\t \x01(\x08\x12\x1b\n\x0cring_tone_id\x18\n \x01(\rB\x05\x92?\x02\x38\x08\x12/\n\x08language\x18\x0b \x01(\x0e\x32\x1d.meshtastic.protobuf.Language\x12\x34\n\x0bnode_filter\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.NodeFilter\x12:\n\x0enode_highlight\x18\r \x01(\x0b\x32\".meshtastic.protobuf.NodeHighlight\x12\x1f\n\x10\x63\x61libration_data\x18\x0e \x01(\x0c\x42\x05\x92?\x02\x08\x10\x12*\n\x08map_data\x18\x0f \x01(\x0b\x32\x18.meshtastic.protobuf.Map\x12=\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassModeB\x05\x92?\x02\x38\x08\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\x12R\n\ngps_format\x18\x13 \x01(\x0e\x32\x37.meshtastic.protobuf.DeviceUIConfig.GpsCoordinateFormatB\x05\x92?\x02\x38\x08\"V\n\x13GpsCoordinateFormat\x12\x07\n\x03\x44\x45\x43\x10\x00\x12\x07\n\x03\x44MS\x10\x01\x12\x07\n\x03UTM\x10\x02\x12\x08\n\x04MGRS\x10\x03\x12\x07\n\x03OLC\x10\x04\x12\x08\n\x04OSGR\x10\x05\x12\x07\n\x03MLS\x10\x06\"\xbc\x01\n\nNodeFilter\x12\x16\n\x0eunknown_switch\x18\x01 \x01(\x08\x12\x16\n\x0eoffline_switch\x18\x02 \x01(\x08\x12\x19\n\x11public_key_switch\x18\x03 \x01(\x08\x12\x18\n\thops_away\x18\x04 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x17\n\x0fposition_switch\x18\x05 \x01(\x08\x12\x18\n\tnode_name\x18\x06 \x01(\tB\x05\x92?\x02\x08\x10\x12\x16\n\x07\x63hannel\x18\x07 \x01(\x05\x42\x05\x92?\x02\x38\x08\"\x85\x01\n\rNodeHighlight\x12\x13\n\x0b\x63hat_switch\x18\x01 \x01(\x08\x12\x17\n\x0fposition_switch\x18\x02 \x01(\x08\x12\x18\n\x10telemetry_switch\x18\x03 \x01(\x08\x12\x12\n\niaq_switch\x18\x04 \x01(\x08\x12\x18\n\tnode_name\x18\x05 \x01(\tB\x05\x92?\x02\x08\x10\"D\n\x08GeoPoint\x12\x13\n\x04zoom\x18\x01 \x01(\x05\x42\x05\x92?\x02\x38\x08\x12\x10\n\x08latitude\x18\x02 \x01(\x05\x12\x11\n\tlongitude\x18\x03 \x01(\x05\"\\\n\x03Map\x12+\n\x04home\x18\x01 \x01(\x0b\x32\x1d.meshtastic.protobuf.GeoPoint\x12\x14\n\x05style\x18\x02 \x01(\tB\x05\x92?\x02\x08\x14\x12\x12\n\nfollow_gps\x18\x03 \x01(\x08*>\n\x0b\x43ompassMode\x12\x0b\n\x07\x44YNAMIC\x10\x00\x12\x0e\n\nFIXED_RING\x10\x01\x12\x12\n\x0e\x46REEZE_HEADING\x10\x02*%\n\x05Theme\x12\x08\n\x04\x44\x41RK\x10\x00\x12\t\n\x05LIGHT\x10\x01\x12\x07\n\x03RED\x10\x02*\xc0\x02\n\x08Language\x12\x0b\n\x07\x45NGLISH\x10\x00\x12\n\n\x06\x46RENCH\x10\x01\x12\n\n\x06GERMAN\x10\x02\x12\x0b\n\x07ITALIAN\x10\x03\x12\x0e\n\nPORTUGUESE\x10\x04\x12\x0b\n\x07SPANISH\x10\x05\x12\x0b\n\x07SWEDISH\x10\x06\x12\x0b\n\x07\x46INNISH\x10\x07\x12\n\n\x06POLISH\x10\x08\x12\x0b\n\x07TURKISH\x10\t\x12\x0b\n\x07SERBIAN\x10\n\x12\x0b\n\x07RUSSIAN\x10\x0b\x12\t\n\x05\x44UTCH\x10\x0c\x12\t\n\x05GREEK\x10\r\x12\r\n\tNORWEGIAN\x10\x0e\x12\r\n\tSLOVENIAN\x10\x0f\x12\r\n\tUKRAINIAN\x10\x10\x12\r\n\tBULGARIAN\x10\x11\x12\t\n\x05\x43ZECH\x10\x12\x12\n\n\x06\x44\x41NISH\x10\x13\x12\x16\n\x12SIMPLIFIED_CHINESE\x10\x1e\x12\x17\n\x13TRADITIONAL_CHINESE\x10\x1f\x42\x64\n\x14org.meshtastic.protoB\x0e\x44\x65viceUIProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.device_ui_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016DeviceUIProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_THEME']._serialized_start=1002
|
||||
_globals['_THEME']._serialized_end=1039
|
||||
_globals['_LANGUAGE']._serialized_start=1042
|
||||
_globals['_LANGUAGE']._serialized_end=1324
|
||||
_globals['_DEVICEUICONFIG']._serialized_start=61
|
||||
_globals['_DEVICEUICONFIG']._serialized_end=552
|
||||
_globals['_NODEFILTER']._serialized_start=555
|
||||
_globals['_NODEFILTER']._serialized_end=722
|
||||
_globals['_NODEHIGHLIGHT']._serialized_start=724
|
||||
_globals['_NODEHIGHLIGHT']._serialized_end=850
|
||||
_globals['_GEOPOINT']._serialized_start=852
|
||||
_globals['_GEOPOINT']._serialized_end=913
|
||||
_globals['_MAP']._serialized_start=915
|
||||
_globals['_MAP']._serialized_end=1000
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016DeviceUIProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_DEVICEUICONFIG.fields_by_name['screen_brightness']._options = None
|
||||
_DEVICEUICONFIG.fields_by_name['screen_brightness']._serialized_options = b'\222?\0028\010'
|
||||
_DEVICEUICONFIG.fields_by_name['screen_timeout']._options = None
|
||||
_DEVICEUICONFIG.fields_by_name['screen_timeout']._serialized_options = b'\222?\0028\020'
|
||||
_DEVICEUICONFIG.fields_by_name['ring_tone_id']._options = None
|
||||
_DEVICEUICONFIG.fields_by_name['ring_tone_id']._serialized_options = b'\222?\0028\010'
|
||||
_DEVICEUICONFIG.fields_by_name['calibration_data']._options = None
|
||||
_DEVICEUICONFIG.fields_by_name['calibration_data']._serialized_options = b'\222?\002\010\020'
|
||||
_DEVICEUICONFIG.fields_by_name['compass_mode']._options = None
|
||||
_DEVICEUICONFIG.fields_by_name['compass_mode']._serialized_options = b'\222?\0028\010'
|
||||
_DEVICEUICONFIG.fields_by_name['gps_format']._options = None
|
||||
_DEVICEUICONFIG.fields_by_name['gps_format']._serialized_options = b'\222?\0028\010'
|
||||
_NODEFILTER.fields_by_name['hops_away']._options = None
|
||||
_NODEFILTER.fields_by_name['hops_away']._serialized_options = b'\222?\0028\010'
|
||||
_NODEFILTER.fields_by_name['node_name']._options = None
|
||||
_NODEFILTER.fields_by_name['node_name']._serialized_options = b'\222?\002\010\020'
|
||||
_NODEFILTER.fields_by_name['channel']._options = None
|
||||
_NODEFILTER.fields_by_name['channel']._serialized_options = b'\222?\0028\010'
|
||||
_NODEHIGHLIGHT.fields_by_name['node_name']._options = None
|
||||
_NODEHIGHLIGHT.fields_by_name['node_name']._serialized_options = b'\222?\002\010\020'
|
||||
_GEOPOINT.fields_by_name['zoom']._options = None
|
||||
_GEOPOINT.fields_by_name['zoom']._serialized_options = b'\222?\0028\010'
|
||||
_MAP.fields_by_name['style']._options = None
|
||||
_MAP.fields_by_name['style']._serialized_options = b'\222?\002\010\024'
|
||||
_globals['_COMPASSMODE']._serialized_start=1397
|
||||
_globals['_COMPASSMODE']._serialized_end=1459
|
||||
_globals['_THEME']._serialized_start=1461
|
||||
_globals['_THEME']._serialized_end=1498
|
||||
_globals['_LANGUAGE']._serialized_start=1501
|
||||
_globals['_LANGUAGE']._serialized_end=1821
|
||||
_globals['_DEVICEUICONFIG']._serialized_start=95
|
||||
_globals['_DEVICEUICONFIG']._serialized_end=904
|
||||
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_start=818
|
||||
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_end=904
|
||||
_globals['_NODEFILTER']._serialized_start=907
|
||||
_globals['_NODEFILTER']._serialized_end=1095
|
||||
_globals['_NODEHIGHLIGHT']._serialized_start=1098
|
||||
_globals['_NODEHIGHLIGHT']._serialized_end=1231
|
||||
_globals['_GEOPOINT']._serialized_start=1233
|
||||
_globals['_GEOPOINT']._serialized_end=1301
|
||||
_globals['_MAP']._serialized_start=1303
|
||||
_globals['_MAP']._serialized_end=1395
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
172
meshtastic/protobuf/device_ui_pb2.pyi
generated
172
meshtastic/protobuf/device_ui_pb2.pyi
generated
@@ -17,6 +17,41 @@ else:
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
|
||||
|
||||
class _CompassMode:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _CompassModeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_CompassMode.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
DYNAMIC: _CompassMode.ValueType # 0
|
||||
"""
|
||||
Compass with dynamic ring and heading
|
||||
"""
|
||||
FIXED_RING: _CompassMode.ValueType # 1
|
||||
"""
|
||||
Compass with fixed ring and heading
|
||||
"""
|
||||
FREEZE_HEADING: _CompassMode.ValueType # 2
|
||||
"""
|
||||
Compass with heading and freeze option
|
||||
"""
|
||||
|
||||
class CompassMode(_CompassMode, metaclass=_CompassModeEnumTypeWrapper): ...
|
||||
|
||||
DYNAMIC: CompassMode.ValueType # 0
|
||||
"""
|
||||
Compass with dynamic ring and heading
|
||||
"""
|
||||
FIXED_RING: CompassMode.ValueType # 1
|
||||
"""
|
||||
Compass with fixed ring and heading
|
||||
"""
|
||||
FREEZE_HEADING: CompassMode.ValueType # 2
|
||||
"""
|
||||
Compass with heading and freeze option
|
||||
"""
|
||||
global___CompassMode = CompassMode
|
||||
|
||||
class _Theme:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
@@ -126,6 +161,18 @@ class _LanguageEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumT
|
||||
"""
|
||||
Ukrainian
|
||||
"""
|
||||
BULGARIAN: _Language.ValueType # 17
|
||||
"""
|
||||
Bulgarian
|
||||
"""
|
||||
CZECH: _Language.ValueType # 18
|
||||
"""
|
||||
Czech
|
||||
"""
|
||||
DANISH: _Language.ValueType # 19
|
||||
"""
|
||||
Danish
|
||||
"""
|
||||
SIMPLIFIED_CHINESE: _Language.ValueType # 30
|
||||
"""
|
||||
Simplified Chinese (experimental)
|
||||
@@ -208,6 +255,18 @@ UKRAINIAN: Language.ValueType # 16
|
||||
"""
|
||||
Ukrainian
|
||||
"""
|
||||
BULGARIAN: Language.ValueType # 17
|
||||
"""
|
||||
Bulgarian
|
||||
"""
|
||||
CZECH: Language.ValueType # 18
|
||||
"""
|
||||
Czech
|
||||
"""
|
||||
DANISH: Language.ValueType # 19
|
||||
"""
|
||||
Danish
|
||||
"""
|
||||
SIMPLIFIED_CHINESE: Language.ValueType # 30
|
||||
"""
|
||||
Simplified Chinese (experimental)
|
||||
@@ -226,6 +285,91 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _GpsCoordinateFormat:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _GpsCoordinateFormatEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DeviceUIConfig._GpsCoordinateFormat.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
DEC: DeviceUIConfig._GpsCoordinateFormat.ValueType # 0
|
||||
"""
|
||||
GPS coordinates are displayed in the normal decimal degrees format:
|
||||
DD.DDDDDD DDD.DDDDDD
|
||||
"""
|
||||
DMS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 1
|
||||
"""
|
||||
GPS coordinates are displayed in the degrees minutes seconds format:
|
||||
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
||||
"""
|
||||
UTM: DeviceUIConfig._GpsCoordinateFormat.ValueType # 2
|
||||
"""
|
||||
Universal Transverse Mercator format:
|
||||
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
||||
"""
|
||||
MGRS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 3
|
||||
"""
|
||||
Military Grid Reference System format:
|
||||
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
||||
E is easting, N is northing
|
||||
"""
|
||||
OLC: DeviceUIConfig._GpsCoordinateFormat.ValueType # 4
|
||||
"""
|
||||
Open Location Code (aka Plus Codes).
|
||||
"""
|
||||
OSGR: DeviceUIConfig._GpsCoordinateFormat.ValueType # 5
|
||||
"""
|
||||
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
||||
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
||||
E is the easting, N is the northing
|
||||
"""
|
||||
MLS: DeviceUIConfig._GpsCoordinateFormat.ValueType # 6
|
||||
"""
|
||||
Maidenhead Locator System
|
||||
Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
|
||||
"""
|
||||
|
||||
class GpsCoordinateFormat(_GpsCoordinateFormat, metaclass=_GpsCoordinateFormatEnumTypeWrapper):
|
||||
"""
|
||||
How the GPS coordinates are displayed on the OLED screen.
|
||||
"""
|
||||
|
||||
DEC: DeviceUIConfig.GpsCoordinateFormat.ValueType # 0
|
||||
"""
|
||||
GPS coordinates are displayed in the normal decimal degrees format:
|
||||
DD.DDDDDD DDD.DDDDDD
|
||||
"""
|
||||
DMS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 1
|
||||
"""
|
||||
GPS coordinates are displayed in the degrees minutes seconds format:
|
||||
DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
|
||||
"""
|
||||
UTM: DeviceUIConfig.GpsCoordinateFormat.ValueType # 2
|
||||
"""
|
||||
Universal Transverse Mercator format:
|
||||
ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
|
||||
"""
|
||||
MGRS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 3
|
||||
"""
|
||||
Military Grid Reference System format:
|
||||
ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
|
||||
E is easting, N is northing
|
||||
"""
|
||||
OLC: DeviceUIConfig.GpsCoordinateFormat.ValueType # 4
|
||||
"""
|
||||
Open Location Code (aka Plus Codes).
|
||||
"""
|
||||
OSGR: DeviceUIConfig.GpsCoordinateFormat.ValueType # 5
|
||||
"""
|
||||
Ordnance Survey Grid Reference (the National Grid System of the UK).
|
||||
Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
|
||||
E is the easting, N is the northing
|
||||
"""
|
||||
MLS: DeviceUIConfig.GpsCoordinateFormat.ValueType # 6
|
||||
"""
|
||||
Maidenhead Locator System
|
||||
Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
|
||||
"""
|
||||
|
||||
VERSION_FIELD_NUMBER: builtins.int
|
||||
SCREEN_BRIGHTNESS_FIELD_NUMBER: builtins.int
|
||||
SCREEN_TIMEOUT_FIELD_NUMBER: builtins.int
|
||||
@@ -241,6 +385,10 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
||||
NODE_HIGHLIGHT_FIELD_NUMBER: builtins.int
|
||||
CALIBRATION_DATA_FIELD_NUMBER: builtins.int
|
||||
MAP_DATA_FIELD_NUMBER: builtins.int
|
||||
COMPASS_MODE_FIELD_NUMBER: builtins.int
|
||||
SCREEN_RGB_COLOR_FIELD_NUMBER: builtins.int
|
||||
IS_CLOCKFACE_ANALOG_FIELD_NUMBER: builtins.int
|
||||
GPS_FORMAT_FIELD_NUMBER: builtins.int
|
||||
version: builtins.int
|
||||
"""
|
||||
A version integer used to invalidate saved files when we make incompatible changes.
|
||||
@@ -277,6 +425,24 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
8 integers for screen calibration data
|
||||
"""
|
||||
compass_mode: global___CompassMode.ValueType
|
||||
"""
|
||||
Compass mode
|
||||
"""
|
||||
screen_rgb_color: builtins.int
|
||||
"""
|
||||
RGB color for BaseUI
|
||||
0xRRGGBB format, e.g. 0xFF0000 for red
|
||||
"""
|
||||
is_clockface_analog: builtins.bool
|
||||
"""
|
||||
Clockface analog style
|
||||
true for analog clockface, false for digital clockface
|
||||
"""
|
||||
gps_format: global___DeviceUIConfig.GpsCoordinateFormat.ValueType
|
||||
"""
|
||||
How the GPS coordinates are formatted on the OLED screen.
|
||||
"""
|
||||
@property
|
||||
def node_filter(self) -> global___NodeFilter:
|
||||
"""
|
||||
@@ -313,9 +479,13 @@ class DeviceUIConfig(google.protobuf.message.Message):
|
||||
node_highlight: global___NodeHighlight | None = ...,
|
||||
calibration_data: builtins.bytes = ...,
|
||||
map_data: global___Map | None = ...,
|
||||
compass_mode: global___CompassMode.ValueType = ...,
|
||||
screen_rgb_color: builtins.int = ...,
|
||||
is_clockface_analog: builtins.bool = ...,
|
||||
gps_format: global___DeviceUIConfig.GpsCoordinateFormat.ValueType = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["alert_enabled", b"alert_enabled", "banner_enabled", b"banner_enabled", "calibration_data", b"calibration_data", "language", b"language", "map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight", "pin_code", b"pin_code", "ring_tone_id", b"ring_tone_id", "screen_brightness", b"screen_brightness", "screen_lock", b"screen_lock", "screen_timeout", b"screen_timeout", "settings_lock", b"settings_lock", "theme", b"theme", "version", b"version"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["alert_enabled", b"alert_enabled", "banner_enabled", b"banner_enabled", "calibration_data", b"calibration_data", "compass_mode", b"compass_mode", "gps_format", b"gps_format", "is_clockface_analog", b"is_clockface_analog", "language", b"language", "map_data", b"map_data", "node_filter", b"node_filter", "node_highlight", b"node_highlight", "pin_code", b"pin_code", "ring_tone_id", b"ring_tone_id", "screen_brightness", b"screen_brightness", "screen_lock", b"screen_lock", "screen_rgb_color", b"screen_rgb_color", "screen_timeout", b"screen_timeout", "settings_lock", b"settings_lock", "theme", b"theme", "version", b"version"]) -> None: ...
|
||||
|
||||
global___DeviceUIConfig = DeviceUIConfig
|
||||
|
||||
|
||||
50
meshtastic/protobuf/deviceonly_pb2.py
generated
50
meshtastic/protobuf/deviceonly_pb2.py
generated
@@ -12,41 +12,59 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import channel_pb2 as meshtastic_dot_protobuf_dot_channel__pb2
|
||||
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
||||
from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_telemetry__pb2
|
||||
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
|
||||
from meshtastic.protobuf import localonly_pb2 as meshtastic_dot_protobuf_dot_localonly__pb2
|
||||
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
||||
from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_telemetry__pb2
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/config.proto\x1a#meshtastic/protobuf/localonly.proto\x1a meshtastic/protobuf/nanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\xe2\x01\n\x08UserLite\x12\x13\n\x07macaddr\x18\x01 \x01(\x0c\x42\x02\x18\x01\x12\x11\n\tlong_name\x18\x02 \x01(\t\x12\x12\n\nshort_name\x18\x03 \x01(\t\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x05 \x01(\x08\x12;\n\x04role\x18\x06 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x12\n\npublic_key\x18\x07 \x01(\x0c\"\xde\x02\n\x0cNodeInfoLite\x12\x0b\n\x03num\x18\x01 \x01(\r\x12+\n\x04user\x18\x02 \x01(\x0b\x32\x1d.meshtastic.protobuf.UserLite\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\r\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x16\n\thops_away\x18\t \x01(\rH\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12\x10\n\x08next_hop\x18\x0c \x01(\rB\x0c\n\n_hops_away\"\xa1\x03\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x36\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x19\n\rdid_gps_reset\x18\x0b \x01(\x08\x42\x02\x18\x01\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12M\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\"}\n\x0cNodeDatabase\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\\\n\x05nodes\x18\x02 \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"N\n\x0b\x43hannelFile\x12.\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.Channel\x12\x0f\n\x07version\x18\x02 \x01(\r\"\x86\x02\n\x11\x42\x61\x63kupPreferences\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x11\n\ttimestamp\x18\x02 \x01(\x07\x12\x30\n\x06\x63onfig\x18\x03 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12=\n\rmodule_config\x18\x04 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig\x12\x32\n\x08\x63hannels\x18\x05 \x01(\x0b\x32 .meshtastic.protobuf.ChannelFile\x12(\n\x05owner\x18\x06 \x01(\x0b\x32\x19.meshtastic.protobuf.UserBm\n\x13\x63om.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/nanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\xb0\x02\n\x08UserLite\x12\x1a\n\x07macaddr\x18\x01 \x01(\x0c\x42\t\x18\x01\x92?\x04\x08\x06x\x01\x12\x18\n\tlong_name\x18\x02 \x01(\tB\x05\x92?\x02\x08(\x12\x19\n\nshort_name\x18\x03 \x01(\tB\x05\x92?\x02\x08\x05\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x13\n\x0bis_licensed\x18\x05 \x01(\x08\x12;\n\x04role\x18\x06 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x19\n\npublic_key\x18\x07 \x01(\x0c\x42\x05\x92?\x02\x08 \x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x00\x88\x01\x01\x42\x12\n\x10_is_unmessagable\"\x85\x03\n\x0cNodeInfoLite\x12\x0b\n\x03num\x18\x01 \x01(\r\x12+\n\x04user\x18\x02 \x01(\x0b\x32\x1d.meshtastic.protobuf.UserLite\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x16\n\x07\x63hannel\x18\x07 \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x1d\n\thops_away\x18\t \x01(\rB\x05\x92?\x02\x38\x08H\x00\x88\x01\x01\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\x12\x12\n\nis_ignored\x18\x0b \x01(\x08\x12\x17\n\x08next_hop\x18\x0c \x01(\rB\x05\x92?\x02\x38\x08\x12\x10\n\x08\x62itfield\x18\r \x01(\rB\x0c\n\n_hops_away\"\xaf\x03\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12=\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketB\x05\x92?\x02\x10\x01\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x19\n\rdid_gps_reset\x18\x0b \x01(\x08\x42\x02\x18\x01\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12T\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePinB\x05\x92?\x02\x10\x0c\"}\n\x0cNodeDatabase\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\\\n\x05nodes\x18\x02 \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"U\n\x0b\x43hannelFile\x12\x35\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.ChannelB\x05\x92?\x02\x10\x08\x12\x0f\n\x07version\x18\x02 \x01(\r\"\x86\x02\n\x11\x42\x61\x63kupPreferences\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x11\n\ttimestamp\x18\x02 \x01(\x07\x12\x30\n\x06\x63onfig\x18\x03 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12=\n\rmodule_config\x18\x04 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig\x12\x32\n\x08\x63hannels\x18\x05 \x01(\x0b\x32 .meshtastic.protobuf.ChannelFile\x12(\n\x05owner\x18\x06 \x01(\x0b\x32\x19.meshtastic.protobuf.UserBn\n\x14org.meshtastic.protoB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.deviceonly_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
|
||||
_USERLITE.fields_by_name['macaddr']._options = None
|
||||
_USERLITE.fields_by_name['macaddr']._serialized_options = b'\030\001'
|
||||
_USERLITE.fields_by_name['macaddr']._serialized_options = b'\030\001\222?\004\010\006x\001'
|
||||
_USERLITE.fields_by_name['long_name']._options = None
|
||||
_USERLITE.fields_by_name['long_name']._serialized_options = b'\222?\002\010('
|
||||
_USERLITE.fields_by_name['short_name']._options = None
|
||||
_USERLITE.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
|
||||
_USERLITE.fields_by_name['public_key']._options = None
|
||||
_USERLITE.fields_by_name['public_key']._serialized_options = b'\222?\002\010 '
|
||||
_NODEINFOLITE.fields_by_name['channel']._options = None
|
||||
_NODEINFOLITE.fields_by_name['channel']._serialized_options = b'\222?\0028\010'
|
||||
_NODEINFOLITE.fields_by_name['hops_away']._options = None
|
||||
_NODEINFOLITE.fields_by_name['hops_away']._serialized_options = b'\222?\0028\010'
|
||||
_NODEINFOLITE.fields_by_name['next_hop']._options = None
|
||||
_NODEINFOLITE.fields_by_name['next_hop']._serialized_options = b'\222?\0028\010'
|
||||
_DEVICESTATE.fields_by_name['receive_queue']._options = None
|
||||
_DEVICESTATE.fields_by_name['receive_queue']._serialized_options = b'\222?\002\020\001'
|
||||
_DEVICESTATE.fields_by_name['no_save']._options = None
|
||||
_DEVICESTATE.fields_by_name['no_save']._serialized_options = b'\030\001'
|
||||
_DEVICESTATE.fields_by_name['did_gps_reset']._options = None
|
||||
_DEVICESTATE.fields_by_name['did_gps_reset']._serialized_options = b'\030\001'
|
||||
_DEVICESTATE.fields_by_name['node_remote_hardware_pins']._options = None
|
||||
_DEVICESTATE.fields_by_name['node_remote_hardware_pins']._serialized_options = b'\222?\002\020\014'
|
||||
_NODEDATABASE.fields_by_name['nodes']._options = None
|
||||
_NODEDATABASE.fields_by_name['nodes']._serialized_options = b'\222?\'\222\001$std::vector<meshtastic_NodeInfoLite>'
|
||||
_CHANNELFILE.fields_by_name['channels']._options = None
|
||||
_CHANNELFILE.fields_by_name['channels']._serialized_options = b'\222?\002\020\010'
|
||||
_globals['_POSITIONLITE']._serialized_start=271
|
||||
_globals['_POSITIONLITE']._serialized_end=424
|
||||
_globals['_USERLITE']._serialized_start=427
|
||||
_globals['_USERLITE']._serialized_end=653
|
||||
_globals['_NODEINFOLITE']._serialized_start=656
|
||||
_globals['_NODEINFOLITE']._serialized_end=1006
|
||||
_globals['_DEVICESTATE']._serialized_start=1009
|
||||
_globals['_DEVICESTATE']._serialized_end=1426
|
||||
_globals['_NODEDATABASE']._serialized_start=1428
|
||||
_globals['_NODEDATABASE']._serialized_end=1553
|
||||
_globals['_CHANNELFILE']._serialized_start=1555
|
||||
_globals['_CHANNELFILE']._serialized_end=1633
|
||||
_globals['_BACKUPPREFERENCES']._serialized_start=1636
|
||||
_globals['_BACKUPPREFERENCES']._serialized_end=1898
|
||||
_globals['_USERLITE']._serialized_end=731
|
||||
_globals['_NODEINFOLITE']._serialized_start=734
|
||||
_globals['_NODEINFOLITE']._serialized_end=1123
|
||||
_globals['_DEVICESTATE']._serialized_start=1126
|
||||
_globals['_DEVICESTATE']._serialized_end=1557
|
||||
_globals['_NODEDATABASE']._serialized_start=1559
|
||||
_globals['_NODEDATABASE']._serialized_end=1684
|
||||
_globals['_CHANNELFILE']._serialized_start=1686
|
||||
_globals['_CHANNELFILE']._serialized_end=1771
|
||||
_globals['_BACKUPPREFERENCES']._serialized_start=1774
|
||||
_globals['_BACKUPPREFERENCES']._serialized_end=2036
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
20
meshtastic/protobuf/deviceonly_pb2.pyi
generated
20
meshtastic/protobuf/deviceonly_pb2.pyi
generated
@@ -79,6 +79,7 @@ class UserLite(google.protobuf.message.Message):
|
||||
IS_LICENSED_FIELD_NUMBER: builtins.int
|
||||
ROLE_FIELD_NUMBER: builtins.int
|
||||
PUBLIC_KEY_FIELD_NUMBER: builtins.int
|
||||
IS_UNMESSAGABLE_FIELD_NUMBER: builtins.int
|
||||
macaddr: builtins.bytes
|
||||
"""
|
||||
This is the addr of the radio.
|
||||
@@ -114,6 +115,10 @@ class UserLite(google.protobuf.message.Message):
|
||||
The public key of the user's device.
|
||||
This is sent out to other nodes on the mesh to allow them to compute a shared secret key.
|
||||
"""
|
||||
is_unmessagable: builtins.bool
|
||||
"""
|
||||
Whether or not the node can be messaged
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -124,8 +129,11 @@ class UserLite(google.protobuf.message.Message):
|
||||
is_licensed: builtins.bool = ...,
|
||||
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType = ...,
|
||||
public_key: builtins.bytes = ...,
|
||||
is_unmessagable: builtins.bool | None = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["hw_model", b"hw_model", "is_licensed", b"is_licensed", "long_name", b"long_name", "macaddr", b"macaddr", "public_key", b"public_key", "role", b"role", "short_name", b"short_name"]) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_is_unmessagable", b"_is_unmessagable", "is_unmessagable", b"is_unmessagable"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_is_unmessagable", b"_is_unmessagable", "hw_model", b"hw_model", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "macaddr", b"macaddr", "public_key", b"public_key", "role", b"role", "short_name", b"short_name"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_is_unmessagable", b"_is_unmessagable"]) -> typing.Literal["is_unmessagable"] | None: ...
|
||||
|
||||
global___UserLite = UserLite
|
||||
|
||||
@@ -145,6 +153,7 @@ class NodeInfoLite(google.protobuf.message.Message):
|
||||
IS_FAVORITE_FIELD_NUMBER: builtins.int
|
||||
IS_IGNORED_FIELD_NUMBER: builtins.int
|
||||
NEXT_HOP_FIELD_NUMBER: builtins.int
|
||||
BITFIELD_FIELD_NUMBER: builtins.int
|
||||
num: builtins.int
|
||||
"""
|
||||
The node number
|
||||
@@ -184,6 +193,12 @@ class NodeInfoLite(google.protobuf.message.Message):
|
||||
"""
|
||||
Last byte of the node number of the node that should be used as the next hop to reach this node.
|
||||
"""
|
||||
bitfield: builtins.int
|
||||
"""
|
||||
Bitfield for storing booleans.
|
||||
LSB 0 is_key_manually_verified
|
||||
LSB 1 is_muted
|
||||
"""
|
||||
@property
|
||||
def user(self) -> global___UserLite:
|
||||
"""
|
||||
@@ -218,9 +233,10 @@ class NodeInfoLite(google.protobuf.message.Message):
|
||||
is_favorite: builtins.bool = ...,
|
||||
is_ignored: builtins.bool = ...,
|
||||
next_hop: builtins.int = ...,
|
||||
bitfield: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "position", b"position", "user", b"user"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "last_heard", b"last_heard", "next_hop", b"next_hop", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "last_heard", b"last_heard", "next_hop", b"next_hop", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_hops_away", b"_hops_away"]) -> typing.Literal["hops_away"] | None: ...
|
||||
|
||||
global___NodeInfoLite = NodeInfoLite
|
||||
|
||||
19
meshtastic/protobuf/interdevice_pb2.py
generated
19
meshtastic/protobuf/interdevice_pb2.py
generated
@@ -11,20 +11,23 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/interdevice.proto\x12\x13meshtastic.protobuf\"s\n\nSensorData\x12.\n\x04type\x18\x01 \x01(\x0e\x32 .meshtastic.protobuf.MessageType\x12\x15\n\x0b\x66loat_value\x18\x02 \x01(\x02H\x00\x12\x16\n\x0cuint32_value\x18\x03 \x01(\rH\x00\x42\x06\n\x04\x64\x61ta\"_\n\x12InterdeviceMessage\x12\x0e\n\x04nmea\x18\x01 \x01(\tH\x00\x12\x31\n\x06sensor\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.SensorDataH\x00\x42\x06\n\x04\x64\x61ta*\xd5\x01\n\x0bMessageType\x12\x07\n\x03\x41\x43K\x10\x00\x12\x15\n\x10\x43OLLECT_INTERVAL\x10\xa0\x01\x12\x0c\n\x07\x42\x45\x45P_ON\x10\xa1\x01\x12\r\n\x08\x42\x45\x45P_OFF\x10\xa2\x01\x12\r\n\x08SHUTDOWN\x10\xa3\x01\x12\r\n\x08POWER_ON\x10\xa4\x01\x12\x0f\n\nSCD41_TEMP\x10\xb0\x01\x12\x13\n\x0eSCD41_HUMIDITY\x10\xb1\x01\x12\x0e\n\tSCD41_CO2\x10\xb2\x01\x12\x0f\n\nAHT20_TEMP\x10\xb3\x01\x12\x13\n\x0e\x41HT20_HUMIDITY\x10\xb4\x01\x12\x0f\n\nTVOC_INDEX\x10\xb5\x01\x42\x66\n\x13\x63om.geeksville.meshB\x11InterdeviceProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/interdevice.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"s\n\nSensorData\x12.\n\x04type\x18\x01 \x01(\x0e\x32 .meshtastic.protobuf.MessageType\x12\x15\n\x0b\x66loat_value\x18\x02 \x01(\x02H\x00\x12\x16\n\x0cuint32_value\x18\x03 \x01(\rH\x00\x42\x06\n\x04\x64\x61ta\"g\n\x12InterdeviceMessage\x12\x16\n\x04nmea\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x08H\x00\x12\x31\n\x06sensor\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.SensorDataH\x00\x42\x06\n\x04\x64\x61ta*\xd5\x01\n\x0bMessageType\x12\x07\n\x03\x41\x43K\x10\x00\x12\x15\n\x10\x43OLLECT_INTERVAL\x10\xa0\x01\x12\x0c\n\x07\x42\x45\x45P_ON\x10\xa1\x01\x12\r\n\x08\x42\x45\x45P_OFF\x10\xa2\x01\x12\r\n\x08SHUTDOWN\x10\xa3\x01\x12\r\n\x08POWER_ON\x10\xa4\x01\x12\x0f\n\nSCD41_TEMP\x10\xb0\x01\x12\x13\n\x0eSCD41_HUMIDITY\x10\xb1\x01\x12\x0e\n\tSCD41_CO2\x10\xb2\x01\x12\x0f\n\nAHT20_TEMP\x10\xb3\x01\x12\x13\n\x0e\x41HT20_HUMIDITY\x10\xb4\x01\x12\x0f\n\nTVOC_INDEX\x10\xb5\x01\x42g\n\x14org.meshtastic.protoB\x11InterdeviceProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.interdevice_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\021InterdeviceProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_MESSAGETYPE']._serialized_start=277
|
||||
_globals['_MESSAGETYPE']._serialized_end=490
|
||||
_globals['_SENSORDATA']._serialized_start=62
|
||||
_globals['_SENSORDATA']._serialized_end=177
|
||||
_globals['_INTERDEVICEMESSAGE']._serialized_start=179
|
||||
_globals['_INTERDEVICEMESSAGE']._serialized_end=274
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021InterdeviceProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_INTERDEVICEMESSAGE.fields_by_name['nmea']._options = None
|
||||
_INTERDEVICEMESSAGE.fields_by_name['nmea']._serialized_options = b'\222?\003\010\200\010'
|
||||
_globals['_MESSAGETYPE']._serialized_start=319
|
||||
_globals['_MESSAGETYPE']._serialized_end=532
|
||||
_globals['_SENSORDATA']._serialized_start=96
|
||||
_globals['_SENSORDATA']._serialized_end=211
|
||||
_globals['_INTERDEVICEMESSAGE']._serialized_start=213
|
||||
_globals['_INTERDEVICEMESSAGE']._serialized_end=316
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
6
meshtastic/protobuf/localonly_pb2.py
generated
6
meshtastic/protobuf/localonly_pb2.py
generated
@@ -15,16 +15,16 @@ from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config
|
||||
from meshtastic.protobuf import module_config_pb2 as meshtastic_dot_protobuf_dot_module__config__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/localonly.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xfa\x03\n\x0bLocalConfig\x12\x38\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32(.meshtastic.protobuf.Config.DeviceConfig\x12<\n\x08position\x18\x02 \x01(\x0b\x32*.meshtastic.protobuf.Config.PositionConfig\x12\x36\n\x05power\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.Config.PowerConfig\x12:\n\x07network\x18\x04 \x01(\x0b\x32).meshtastic.protobuf.Config.NetworkConfig\x12:\n\x07\x64isplay\x18\x05 \x01(\x0b\x32).meshtastic.protobuf.Config.DisplayConfig\x12\x34\n\x04lora\x18\x06 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfig\x12>\n\tbluetooth\x18\x07 \x01(\x0b\x32+.meshtastic.protobuf.Config.BluetoothConfig\x12\x0f\n\x07version\x18\x08 \x01(\r\x12<\n\x08security\x18\t \x01(\x0b\x32*.meshtastic.protobuf.Config.SecurityConfig\"\xf0\x07\n\x11LocalModuleConfig\x12:\n\x04mqtt\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.ModuleConfig.MQTTConfig\x12>\n\x06serial\x18\x02 \x01(\x0b\x32..meshtastic.protobuf.ModuleConfig.SerialConfig\x12[\n\x15\x65xternal_notification\x18\x03 \x01(\x0b\x32<.meshtastic.protobuf.ModuleConfig.ExternalNotificationConfig\x12K\n\rstore_forward\x18\x04 \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.StoreForwardConfig\x12\x45\n\nrange_test\x18\x05 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.RangeTestConfig\x12\x44\n\ttelemetry\x18\x06 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.TelemetryConfig\x12M\n\x0e\x63\x61nned_message\x18\x07 \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.CannedMessageConfig\x12<\n\x05\x61udio\x18\t \x01(\x0b\x32-.meshtastic.protobuf.ModuleConfig.AudioConfig\x12O\n\x0fremote_hardware\x18\n \x01(\x0b\x32\x36.meshtastic.protobuf.ModuleConfig.RemoteHardwareConfig\x12K\n\rneighbor_info\x18\x0b \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.NeighborInfoConfig\x12Q\n\x10\x61mbient_lighting\x18\x0c \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.AmbientLightingConfig\x12Q\n\x10\x64\x65tection_sensor\x18\r \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.DetectionSensorConfig\x12\x46\n\npaxcounter\x18\x0e \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.PaxcounterConfig\x12\x0f\n\x07version\x18\x08 \x01(\rBd\n\x13\x63om.geeksville.meshB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#meshtastic/protobuf/localonly.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\'meshtastic/protobuf/module_config.proto\"\xfa\x03\n\x0bLocalConfig\x12\x38\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32(.meshtastic.protobuf.Config.DeviceConfig\x12<\n\x08position\x18\x02 \x01(\x0b\x32*.meshtastic.protobuf.Config.PositionConfig\x12\x36\n\x05power\x18\x03 \x01(\x0b\x32\'.meshtastic.protobuf.Config.PowerConfig\x12:\n\x07network\x18\x04 \x01(\x0b\x32).meshtastic.protobuf.Config.NetworkConfig\x12:\n\x07\x64isplay\x18\x05 \x01(\x0b\x32).meshtastic.protobuf.Config.DisplayConfig\x12\x34\n\x04lora\x18\x06 \x01(\x0b\x32&.meshtastic.protobuf.Config.LoRaConfig\x12>\n\tbluetooth\x18\x07 \x01(\x0b\x32+.meshtastic.protobuf.Config.BluetoothConfig\x12\x0f\n\x07version\x18\x08 \x01(\r\x12<\n\x08security\x18\t \x01(\x0b\x32*.meshtastic.protobuf.Config.SecurityConfig\"\xcf\t\n\x11LocalModuleConfig\x12:\n\x04mqtt\x18\x01 \x01(\x0b\x32,.meshtastic.protobuf.ModuleConfig.MQTTConfig\x12>\n\x06serial\x18\x02 \x01(\x0b\x32..meshtastic.protobuf.ModuleConfig.SerialConfig\x12[\n\x15\x65xternal_notification\x18\x03 \x01(\x0b\x32<.meshtastic.protobuf.ModuleConfig.ExternalNotificationConfig\x12K\n\rstore_forward\x18\x04 \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.StoreForwardConfig\x12\x45\n\nrange_test\x18\x05 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.RangeTestConfig\x12\x44\n\ttelemetry\x18\x06 \x01(\x0b\x32\x31.meshtastic.protobuf.ModuleConfig.TelemetryConfig\x12M\n\x0e\x63\x61nned_message\x18\x07 \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.CannedMessageConfig\x12<\n\x05\x61udio\x18\t \x01(\x0b\x32-.meshtastic.protobuf.ModuleConfig.AudioConfig\x12O\n\x0fremote_hardware\x18\n \x01(\x0b\x32\x36.meshtastic.protobuf.ModuleConfig.RemoteHardwareConfig\x12K\n\rneighbor_info\x18\x0b \x01(\x0b\x32\x34.meshtastic.protobuf.ModuleConfig.NeighborInfoConfig\x12Q\n\x10\x61mbient_lighting\x18\x0c \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.AmbientLightingConfig\x12Q\n\x10\x64\x65tection_sensor\x18\r \x01(\x0b\x32\x37.meshtastic.protobuf.ModuleConfig.DetectionSensorConfig\x12\x46\n\npaxcounter\x18\x0e \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.PaxcounterConfig\x12L\n\rstatusmessage\x18\x0f \x01(\x0b\x32\x35.meshtastic.protobuf.ModuleConfig.StatusMessageConfig\x12U\n\x12traffic_management\x18\x10 \x01(\x0b\x32\x39.meshtastic.protobuf.ModuleConfig.TrafficManagementConfig\x12\x38\n\x03tak\x18\x11 \x01(\x0b\x32+.meshtastic.protobuf.ModuleConfig.TAKConfig\x12\x0f\n\x07version\x18\x08 \x01(\rBe\n\x14org.meshtastic.protoB\x0fLocalOnlyProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.localonly_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\017LocalOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\017LocalOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_LOCALCONFIG']._serialized_start=136
|
||||
_globals['_LOCALCONFIG']._serialized_end=642
|
||||
_globals['_LOCALMODULECONFIG']._serialized_start=645
|
||||
_globals['_LOCALMODULECONFIG']._serialized_end=1653
|
||||
_globals['_LOCALMODULECONFIG']._serialized_end=1876
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
28
meshtastic/protobuf/localonly_pb2.pyi
generated
28
meshtastic/protobuf/localonly_pb2.pyi
generated
@@ -119,6 +119,9 @@ class LocalModuleConfig(google.protobuf.message.Message):
|
||||
AMBIENT_LIGHTING_FIELD_NUMBER: builtins.int
|
||||
DETECTION_SENSOR_FIELD_NUMBER: builtins.int
|
||||
PAXCOUNTER_FIELD_NUMBER: builtins.int
|
||||
STATUSMESSAGE_FIELD_NUMBER: builtins.int
|
||||
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
|
||||
TAK_FIELD_NUMBER: builtins.int
|
||||
VERSION_FIELD_NUMBER: builtins.int
|
||||
version: builtins.int
|
||||
"""
|
||||
@@ -204,6 +207,24 @@ class LocalModuleConfig(google.protobuf.message.Message):
|
||||
Paxcounter Config
|
||||
"""
|
||||
|
||||
@property
|
||||
def statusmessage(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.StatusMessageConfig:
|
||||
"""
|
||||
StatusMessage Config
|
||||
"""
|
||||
|
||||
@property
|
||||
def traffic_management(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.TrafficManagementConfig:
|
||||
"""
|
||||
The part of the config that is specific to the Traffic Management module
|
||||
"""
|
||||
|
||||
@property
|
||||
def tak(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.TAKConfig:
|
||||
"""
|
||||
TAK Config
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -220,9 +241,12 @@ class LocalModuleConfig(google.protobuf.message.Message):
|
||||
ambient_lighting: meshtastic.protobuf.module_config_pb2.ModuleConfig.AmbientLightingConfig | None = ...,
|
||||
detection_sensor: meshtastic.protobuf.module_config_pb2.ModuleConfig.DetectionSensorConfig | None = ...,
|
||||
paxcounter: meshtastic.protobuf.module_config_pb2.ModuleConfig.PaxcounterConfig | None = ...,
|
||||
statusmessage: meshtastic.protobuf.module_config_pb2.ModuleConfig.StatusMessageConfig | None = ...,
|
||||
traffic_management: meshtastic.protobuf.module_config_pb2.ModuleConfig.TrafficManagementConfig | None = ...,
|
||||
tak: meshtastic.protobuf.module_config_pb2.ModuleConfig.TAKConfig | None = ...,
|
||||
version: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry", "version", b"version"]) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management", "version", b"version"]) -> None: ...
|
||||
|
||||
global___LocalModuleConfig = LocalModuleConfig
|
||||
|
||||
293
meshtastic/protobuf/mesh_pb2.py
generated
293
meshtastic/protobuf/mesh_pb2.py
generated
File diff suppressed because one or more lines are too long
1158
meshtastic/protobuf/mesh_pb2.pyi
generated
1158
meshtastic/protobuf/mesh_pb2.pyi
generated
File diff suppressed because it is too large
Load Diff
152
meshtastic/protobuf/module_config_pb2.py
generated
152
meshtastic/protobuf/module_config_pb2.py
generated
File diff suppressed because one or more lines are too long
240
meshtastic/protobuf/module_config_pb2.pyi
generated
240
meshtastic/protobuf/module_config_pb2.pyi
generated
@@ -9,6 +9,7 @@ import google.protobuf.descriptor
|
||||
import google.protobuf.internal.containers
|
||||
import google.protobuf.internal.enum_type_wrapper
|
||||
import google.protobuf.message
|
||||
import meshtastic.protobuf.atak_pb2
|
||||
import sys
|
||||
import typing
|
||||
|
||||
@@ -112,7 +113,7 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
json_enabled: builtins.bool
|
||||
"""
|
||||
Whether to send / consume json packets on MQTT
|
||||
Deprecated: JSON packet support on MQTT was removed, and this field is ignored.
|
||||
"""
|
||||
tls_enabled: builtins.bool
|
||||
"""
|
||||
@@ -165,6 +166,7 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
|
||||
PUBLISH_INTERVAL_SECS_FIELD_NUMBER: builtins.int
|
||||
POSITION_PRECISION_FIELD_NUMBER: builtins.int
|
||||
SHOULD_REPORT_LOCATION_FIELD_NUMBER: builtins.int
|
||||
publish_interval_secs: builtins.int
|
||||
"""
|
||||
How often we should report our info to the map (in seconds)
|
||||
@@ -173,13 +175,18 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
Bits of precision for the location sent (default of 32 is full precision).
|
||||
"""
|
||||
should_report_location: builtins.bool
|
||||
"""
|
||||
Whether we have opted-in to report our location to the map
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
publish_interval_secs: builtins.int = ...,
|
||||
position_precision: builtins.int = ...,
|
||||
should_report_location: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["position_precision", b"position_precision", "publish_interval_secs", b"publish_interval_secs"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["position_precision", b"position_precision", "publish_interval_secs", b"publish_interval_secs", "should_report_location", b"should_report_location"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class RemoteHardwareConfig(google.protobuf.message.Message):
|
||||
@@ -486,6 +493,105 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["ble_threshold", b"ble_threshold", "enabled", b"enabled", "paxcounter_update_interval", b"paxcounter_update_interval", "wifi_threshold", b"wifi_threshold"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class TrafficManagementConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
Config for the Traffic Management module.
|
||||
Provides packet inspection and traffic shaping to help reduce channel utilization
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
ENABLED_FIELD_NUMBER: builtins.int
|
||||
POSITION_DEDUP_ENABLED_FIELD_NUMBER: builtins.int
|
||||
POSITION_PRECISION_BITS_FIELD_NUMBER: builtins.int
|
||||
POSITION_MIN_INTERVAL_SECS_FIELD_NUMBER: builtins.int
|
||||
NODEINFO_DIRECT_RESPONSE_FIELD_NUMBER: builtins.int
|
||||
NODEINFO_DIRECT_RESPONSE_MAX_HOPS_FIELD_NUMBER: builtins.int
|
||||
RATE_LIMIT_ENABLED_FIELD_NUMBER: builtins.int
|
||||
RATE_LIMIT_WINDOW_SECS_FIELD_NUMBER: builtins.int
|
||||
RATE_LIMIT_MAX_PACKETS_FIELD_NUMBER: builtins.int
|
||||
DROP_UNKNOWN_ENABLED_FIELD_NUMBER: builtins.int
|
||||
UNKNOWN_PACKET_THRESHOLD_FIELD_NUMBER: builtins.int
|
||||
EXHAUST_HOP_TELEMETRY_FIELD_NUMBER: builtins.int
|
||||
EXHAUST_HOP_POSITION_FIELD_NUMBER: builtins.int
|
||||
ROUTER_PRESERVE_HOPS_FIELD_NUMBER: builtins.int
|
||||
enabled: builtins.bool
|
||||
"""
|
||||
Master enable for traffic management module
|
||||
"""
|
||||
position_dedup_enabled: builtins.bool
|
||||
"""
|
||||
Enable position deduplication to drop redundant position broadcasts
|
||||
"""
|
||||
position_precision_bits: builtins.int
|
||||
"""
|
||||
Number of bits of precision for position deduplication (0-32)
|
||||
"""
|
||||
position_min_interval_secs: builtins.int
|
||||
"""
|
||||
Minimum interval in seconds between position updates from the same node
|
||||
"""
|
||||
nodeinfo_direct_response: builtins.bool
|
||||
"""
|
||||
Enable direct response to NodeInfo requests from local cache
|
||||
"""
|
||||
nodeinfo_direct_response_max_hops: builtins.int
|
||||
"""
|
||||
Minimum hop distance from requestor before responding to NodeInfo requests
|
||||
"""
|
||||
rate_limit_enabled: builtins.bool
|
||||
"""
|
||||
Enable per-node rate limiting to throttle chatty nodes
|
||||
"""
|
||||
rate_limit_window_secs: builtins.int
|
||||
"""
|
||||
Time window in seconds for rate limiting calculations
|
||||
"""
|
||||
rate_limit_max_packets: builtins.int
|
||||
"""
|
||||
Maximum packets allowed per node within the rate limit window
|
||||
"""
|
||||
drop_unknown_enabled: builtins.bool
|
||||
"""
|
||||
Enable dropping of unknown/undecryptable packets per rate_limit_window_secs
|
||||
"""
|
||||
unknown_packet_threshold: builtins.int
|
||||
"""
|
||||
Number of unknown packets before dropping from a node
|
||||
"""
|
||||
exhaust_hop_telemetry: builtins.bool
|
||||
"""
|
||||
Set hop_limit to 0 for relayed telemetry broadcasts (own packets unaffected)
|
||||
"""
|
||||
exhaust_hop_position: builtins.bool
|
||||
"""
|
||||
Set hop_limit to 0 for relayed position broadcasts (own packets unaffected)
|
||||
"""
|
||||
router_preserve_hops: builtins.bool
|
||||
"""
|
||||
Preserve hop_limit for router-to-router traffic
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
enabled: builtins.bool = ...,
|
||||
position_dedup_enabled: builtins.bool = ...,
|
||||
position_precision_bits: builtins.int = ...,
|
||||
position_min_interval_secs: builtins.int = ...,
|
||||
nodeinfo_direct_response: builtins.bool = ...,
|
||||
nodeinfo_direct_response_max_hops: builtins.int = ...,
|
||||
rate_limit_enabled: builtins.bool = ...,
|
||||
rate_limit_window_secs: builtins.int = ...,
|
||||
rate_limit_max_packets: builtins.int = ...,
|
||||
drop_unknown_enabled: builtins.bool = ...,
|
||||
unknown_packet_threshold: builtins.int = ...,
|
||||
exhaust_hop_telemetry: builtins.bool = ...,
|
||||
exhaust_hop_position: builtins.bool = ...,
|
||||
router_preserve_hops: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["drop_unknown_enabled", b"drop_unknown_enabled", "enabled", b"enabled", "exhaust_hop_position", b"exhaust_hop_position", "exhaust_hop_telemetry", b"exhaust_hop_telemetry", "nodeinfo_direct_response", b"nodeinfo_direct_response", "nodeinfo_direct_response_max_hops", b"nodeinfo_direct_response_max_hops", "position_dedup_enabled", b"position_dedup_enabled", "position_min_interval_secs", b"position_min_interval_secs", "position_precision_bits", b"position_precision_bits", "rate_limit_enabled", b"rate_limit_enabled", "rate_limit_max_packets", b"rate_limit_max_packets", "rate_limit_window_secs", b"rate_limit_window_secs", "router_preserve_hops", b"router_preserve_hops", "unknown_packet_threshold", b"unknown_packet_threshold"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class SerialConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
@@ -554,6 +660,20 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
"""NMEA messages specifically tailored for CalTopo"""
|
||||
WS85: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 6
|
||||
"""Ecowitt WS85 weather station"""
|
||||
VE_DIRECT: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 7
|
||||
"""VE.Direct is a serial protocol used by Victron Energy products
|
||||
https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
|
||||
"""
|
||||
MS_CONFIG: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 8
|
||||
"""Used to configure and view some parameters of MeshSolar.
|
||||
https://heltec.org/project/meshsolar/
|
||||
"""
|
||||
LOG: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 9
|
||||
"""Logs mesh traffic to the serial pins, ideal for logging via openLog or similar.
|
||||
includes other packets
|
||||
"""
|
||||
LOGTEXT: ModuleConfig.SerialConfig._Serial_Mode.ValueType # 10
|
||||
"""only text (channel & DM)"""
|
||||
|
||||
class Serial_Mode(_Serial_Mode, metaclass=_Serial_ModeEnumTypeWrapper):
|
||||
"""
|
||||
@@ -569,6 +689,20 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
"""NMEA messages specifically tailored for CalTopo"""
|
||||
WS85: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 6
|
||||
"""Ecowitt WS85 weather station"""
|
||||
VE_DIRECT: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 7
|
||||
"""VE.Direct is a serial protocol used by Victron Energy products
|
||||
https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
|
||||
"""
|
||||
MS_CONFIG: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 8
|
||||
"""Used to configure and view some parameters of MeshSolar.
|
||||
https://heltec.org/project/meshsolar/
|
||||
"""
|
||||
LOG: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 9
|
||||
"""Logs mesh traffic to the serial pins, ideal for logging via openLog or similar.
|
||||
includes other packets
|
||||
"""
|
||||
LOGTEXT: ModuleConfig.SerialConfig.Serial_Mode.ValueType # 10
|
||||
"""only text (channel & DM)"""
|
||||
|
||||
ENABLED_FIELD_NUMBER: builtins.int
|
||||
ECHO_FIELD_NUMBER: builtins.int
|
||||
@@ -802,6 +936,7 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
ENABLED_FIELD_NUMBER: builtins.int
|
||||
SENDER_FIELD_NUMBER: builtins.int
|
||||
SAVE_FIELD_NUMBER: builtins.int
|
||||
CLEAR_ON_REBOOT_FIELD_NUMBER: builtins.int
|
||||
enabled: builtins.bool
|
||||
"""
|
||||
Enable the Range Test Module
|
||||
@@ -815,14 +950,20 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
Bool value indicating that this node should save a RangeTest.csv file.
|
||||
ESP32 Only
|
||||
"""
|
||||
clear_on_reboot: builtins.bool
|
||||
"""
|
||||
Bool indicating that the node should cleanup / destroy it's RangeTest.csv file.
|
||||
ESP32 Only
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
enabled: builtins.bool = ...,
|
||||
sender: builtins.int = ...,
|
||||
save: builtins.bool = ...,
|
||||
clear_on_reboot: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["enabled", b"enabled", "save", b"save", "sender", b"sender"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["clear_on_reboot", b"clear_on_reboot", "enabled", b"enabled", "save", b"save", "sender", b"sender"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class TelemetryConfig(google.protobuf.message.Message):
|
||||
@@ -845,6 +986,8 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
HEALTH_MEASUREMENT_ENABLED_FIELD_NUMBER: builtins.int
|
||||
HEALTH_UPDATE_INTERVAL_FIELD_NUMBER: builtins.int
|
||||
HEALTH_SCREEN_ENABLED_FIELD_NUMBER: builtins.int
|
||||
DEVICE_TELEMETRY_ENABLED_FIELD_NUMBER: builtins.int
|
||||
AIR_QUALITY_SCREEN_ENABLED_FIELD_NUMBER: builtins.int
|
||||
device_update_interval: builtins.int
|
||||
"""
|
||||
Interval in seconds of how often we should try to send our
|
||||
@@ -905,6 +1048,15 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
Enable/Disable the health telemetry module on-device display
|
||||
"""
|
||||
device_telemetry_enabled: builtins.bool
|
||||
"""
|
||||
Enable/Disable the device telemetry module to send metrics to the mesh
|
||||
Note: We will still send telemtry to the connected phone / client every minute over the API
|
||||
"""
|
||||
air_quality_screen_enabled: builtins.bool
|
||||
"""
|
||||
Enable/Disable the air quality telemetry measurement module on-device display
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -921,8 +1073,10 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
health_measurement_enabled: builtins.bool = ...,
|
||||
health_update_interval: builtins.int = ...,
|
||||
health_screen_enabled: builtins.bool = ...,
|
||||
device_telemetry_enabled: builtins.bool = ...,
|
||||
air_quality_screen_enabled: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["air_quality_enabled", b"air_quality_enabled", "air_quality_interval", b"air_quality_interval", "device_update_interval", b"device_update_interval", "environment_display_fahrenheit", b"environment_display_fahrenheit", "environment_measurement_enabled", b"environment_measurement_enabled", "environment_screen_enabled", b"environment_screen_enabled", "environment_update_interval", b"environment_update_interval", "health_measurement_enabled", b"health_measurement_enabled", "health_screen_enabled", b"health_screen_enabled", "health_update_interval", b"health_update_interval", "power_measurement_enabled", b"power_measurement_enabled", "power_screen_enabled", b"power_screen_enabled", "power_update_interval", b"power_update_interval"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["air_quality_enabled", b"air_quality_enabled", "air_quality_interval", b"air_quality_interval", "air_quality_screen_enabled", b"air_quality_screen_enabled", "device_telemetry_enabled", b"device_telemetry_enabled", "device_update_interval", b"device_update_interval", "environment_display_fahrenheit", b"environment_display_fahrenheit", "environment_measurement_enabled", b"environment_measurement_enabled", "environment_screen_enabled", b"environment_screen_enabled", "environment_update_interval", b"environment_update_interval", "health_measurement_enabled", b"health_measurement_enabled", "health_screen_enabled", b"health_screen_enabled", "health_update_interval", b"health_update_interval", "power_measurement_enabled", b"power_measurement_enabled", "power_screen_enabled", b"power_screen_enabled", "power_update_interval", b"power_update_interval"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class CannedMessageConfig(google.protobuf.message.Message):
|
||||
@@ -1128,6 +1282,54 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["blue", b"blue", "current", b"current", "green", b"green", "led_state", b"led_state", "red", b"red"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class StatusMessageConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
StatusMessage config - Allows setting a status message for a node to periodically rebroadcast
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
NODE_STATUS_FIELD_NUMBER: builtins.int
|
||||
node_status: builtins.str
|
||||
"""
|
||||
The actual status string
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
node_status: builtins.str = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["node_status", b"node_status"]) -> None: ...
|
||||
|
||||
@typing.final
|
||||
class TAKConfig(google.protobuf.message.Message):
|
||||
"""
|
||||
TAK team/role configuration
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
TEAM_FIELD_NUMBER: builtins.int
|
||||
ROLE_FIELD_NUMBER: builtins.int
|
||||
team: meshtastic.protobuf.atak_pb2.Team.ValueType
|
||||
"""
|
||||
Team color.
|
||||
Default Unspecifed_Color -> firmware uses Cyan
|
||||
"""
|
||||
role: meshtastic.protobuf.atak_pb2.MemberRole.ValueType
|
||||
"""
|
||||
Member role.
|
||||
Default Unspecifed -> firmware uses TeamMember
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
team: meshtastic.protobuf.atak_pb2.Team.ValueType = ...,
|
||||
role: meshtastic.protobuf.atak_pb2.MemberRole.ValueType = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["role", b"role", "team", b"team"]) -> None: ...
|
||||
|
||||
MQTT_FIELD_NUMBER: builtins.int
|
||||
SERIAL_FIELD_NUMBER: builtins.int
|
||||
EXTERNAL_NOTIFICATION_FIELD_NUMBER: builtins.int
|
||||
@@ -1141,6 +1343,9 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
AMBIENT_LIGHTING_FIELD_NUMBER: builtins.int
|
||||
DETECTION_SENSOR_FIELD_NUMBER: builtins.int
|
||||
PAXCOUNTER_FIELD_NUMBER: builtins.int
|
||||
STATUSMESSAGE_FIELD_NUMBER: builtins.int
|
||||
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
|
||||
TAK_FIELD_NUMBER: builtins.int
|
||||
@property
|
||||
def mqtt(self) -> global___ModuleConfig.MQTTConfig:
|
||||
"""
|
||||
@@ -1219,6 +1424,24 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
TODO: REPLACE
|
||||
"""
|
||||
|
||||
@property
|
||||
def statusmessage(self) -> global___ModuleConfig.StatusMessageConfig:
|
||||
"""
|
||||
TODO: REPLACE
|
||||
"""
|
||||
|
||||
@property
|
||||
def traffic_management(self) -> global___ModuleConfig.TrafficManagementConfig:
|
||||
"""
|
||||
Traffic management module config for mesh network optimization
|
||||
"""
|
||||
|
||||
@property
|
||||
def tak(self) -> global___ModuleConfig.TAKConfig:
|
||||
"""
|
||||
TAK team/role configuration for TAK_TRACKER
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -1235,10 +1458,13 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
ambient_lighting: global___ModuleConfig.AmbientLightingConfig | None = ...,
|
||||
detection_sensor: global___ModuleConfig.DetectionSensorConfig | None = ...,
|
||||
paxcounter: global___ModuleConfig.PaxcounterConfig | None = ...,
|
||||
statusmessage: global___ModuleConfig.StatusMessageConfig | None = ...,
|
||||
traffic_management: global___ModuleConfig.TrafficManagementConfig | None = ...,
|
||||
tak: global___ModuleConfig.TAKConfig | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "store_forward", b"store_forward", "telemetry", b"telemetry"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["mqtt", "serial", "external_notification", "store_forward", "range_test", "telemetry", "canned_message", "audio", "remote_hardware", "neighbor_info", "ambient_lighting", "detection_sensor", "paxcounter"] | None: ...
|
||||
def HasField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["ambient_lighting", b"ambient_lighting", "audio", b"audio", "canned_message", b"canned_message", "detection_sensor", b"detection_sensor", "external_notification", b"external_notification", "mqtt", b"mqtt", "neighbor_info", b"neighbor_info", "paxcounter", b"paxcounter", "payload_variant", b"payload_variant", "range_test", b"range_test", "remote_hardware", b"remote_hardware", "serial", b"serial", "statusmessage", b"statusmessage", "store_forward", b"store_forward", "tak", b"tak", "telemetry", b"telemetry", "traffic_management", b"traffic_management"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["mqtt", "serial", "external_notification", "store_forward", "range_test", "telemetry", "canned_message", "audio", "remote_hardware", "neighbor_info", "ambient_lighting", "detection_sensor", "paxcounter", "statusmessage", "traffic_management", "tak"] | None: ...
|
||||
|
||||
global___ModuleConfig = ModuleConfig
|
||||
|
||||
|
||||
21
meshtastic/protobuf/mqtt_pb2.py
generated
21
meshtastic/protobuf/mqtt_pb2.py
generated
@@ -13,18 +13,27 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
from meshtastic.protobuf import config_pb2 as meshtastic_dot_protobuf_dot_config__pb2
|
||||
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mqtt.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\x1emeshtastic/protobuf/mesh.proto\"j\n\x0fServiceEnvelope\x12/\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x12\n\nchannel_id\x18\x02 \x01(\t\x12\x12\n\ngateway_id\x18\x03 \x01(\t\"\xe0\x03\n\tMapReport\x12\x11\n\tlong_name\x18\x01 \x01(\t\x12\x12\n\nshort_name\x18\x02 \x01(\t\x12;\n\x04role\x18\x03 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x18\n\x10\x66irmware_version\x18\x05 \x01(\t\x12\x41\n\x06region\x18\x06 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12H\n\x0cmodem_preset\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPreset\x12\x1b\n\x13has_default_channel\x18\x08 \x01(\x08\x12\x12\n\nlatitude_i\x18\t \x01(\x0f\x12\x13\n\x0blongitude_i\x18\n \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x0b \x01(\x05\x12\x1a\n\x12position_precision\x18\x0c \x01(\r\x12\x1e\n\x16num_online_local_nodes\x18\r \x01(\rB_\n\x13\x63om.geeksville.meshB\nMQTTProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1emeshtastic/protobuf/mqtt.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/config.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a meshtastic/protobuf/nanopb.proto\"j\n\x0fServiceEnvelope\x12/\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x12\n\nchannel_id\x18\x02 \x01(\t\x12\x12\n\ngateway_id\x18\x03 \x01(\t\"\x9f\x04\n\tMapReport\x12\x18\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08(\x12\x19\n\nshort_name\x18\x02 \x01(\tB\x05\x92?\x02\x08\x05\x12;\n\x04role\x18\x03 \x01(\x0e\x32-.meshtastic.protobuf.Config.DeviceConfig.Role\x12\x34\n\x08hw_model\x18\x04 \x01(\x0e\x32\".meshtastic.protobuf.HardwareModel\x12\x1f\n\x10\x66irmware_version\x18\x05 \x01(\tB\x05\x92?\x02\x08\x12\x12\x41\n\x06region\x18\x06 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12H\n\x0cmodem_preset\x18\x07 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPreset\x12\x1b\n\x13has_default_channel\x18\x08 \x01(\x08\x12\x12\n\nlatitude_i\x18\t \x01(\x0f\x12\x13\n\x0blongitude_i\x18\n \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x0b \x01(\x05\x12\x1a\n\x12position_precision\x18\x0c \x01(\r\x12%\n\x16num_online_local_nodes\x18\r \x01(\rB\x05\x92?\x02\x38\x10\x12!\n\x19has_opted_report_location\x18\x0e \x01(\x08\x42`\n\x14org.meshtastic.protoB\nMQTTProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mqtt_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\nMQTTProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_SERVICEENVELOPE']._serialized_start=121
|
||||
_globals['_SERVICEENVELOPE']._serialized_end=227
|
||||
_globals['_MAPREPORT']._serialized_start=230
|
||||
_globals['_MAPREPORT']._serialized_end=710
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\nMQTTProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_MAPREPORT.fields_by_name['long_name']._options = None
|
||||
_MAPREPORT.fields_by_name['long_name']._serialized_options = b'\222?\002\010('
|
||||
_MAPREPORT.fields_by_name['short_name']._options = None
|
||||
_MAPREPORT.fields_by_name['short_name']._serialized_options = b'\222?\002\010\005'
|
||||
_MAPREPORT.fields_by_name['firmware_version']._options = None
|
||||
_MAPREPORT.fields_by_name['firmware_version']._serialized_options = b'\222?\002\010\022'
|
||||
_MAPREPORT.fields_by_name['num_online_local_nodes']._options = None
|
||||
_MAPREPORT.fields_by_name['num_online_local_nodes']._serialized_options = b'\222?\0028\020'
|
||||
_globals['_SERVICEENVELOPE']._serialized_start=155
|
||||
_globals['_SERVICEENVELOPE']._serialized_end=261
|
||||
_globals['_MAPREPORT']._serialized_start=264
|
||||
_globals['_MAPREPORT']._serialized_end=807
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
9
meshtastic/protobuf/mqtt_pb2.pyi
generated
9
meshtastic/protobuf/mqtt_pb2.pyi
generated
@@ -72,6 +72,7 @@ class MapReport(google.protobuf.message.Message):
|
||||
ALTITUDE_FIELD_NUMBER: builtins.int
|
||||
POSITION_PRECISION_FIELD_NUMBER: builtins.int
|
||||
NUM_ONLINE_LOCAL_NODES_FIELD_NUMBER: builtins.int
|
||||
HAS_OPTED_REPORT_LOCATION_FIELD_NUMBER: builtins.int
|
||||
long_name: builtins.str
|
||||
"""
|
||||
A full name for this user, i.e. "Kevin Hester"
|
||||
@@ -126,6 +127,11 @@ class MapReport(google.protobuf.message.Message):
|
||||
"""
|
||||
Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT)
|
||||
"""
|
||||
has_opted_report_location: builtins.bool
|
||||
"""
|
||||
User has opted in to share their location (map report) with the mqtt server
|
||||
Controlled by map_report.should_report_location
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -142,7 +148,8 @@ class MapReport(google.protobuf.message.Message):
|
||||
altitude: builtins.int = ...,
|
||||
position_precision: builtins.int = ...,
|
||||
num_online_local_nodes: builtins.int = ...,
|
||||
has_opted_report_location: builtins.bool = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "firmware_version", b"firmware_version", "has_default_channel", b"has_default_channel", "hw_model", b"hw_model", "latitude_i", b"latitude_i", "long_name", b"long_name", "longitude_i", b"longitude_i", "modem_preset", b"modem_preset", "num_online_local_nodes", b"num_online_local_nodes", "position_precision", b"position_precision", "region", b"region", "role", b"role", "short_name", b"short_name"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "firmware_version", b"firmware_version", "has_default_channel", b"has_default_channel", "has_opted_report_location", b"has_opted_report_location", "hw_model", b"hw_model", "latitude_i", b"latitude_i", "long_name", b"long_name", "longitude_i", b"longitude_i", "modem_preset", b"modem_preset", "num_online_local_nodes", b"num_online_local_nodes", "position_precision", b"position_precision", "region", b"region", "role", b"role", "short_name", b"short_name"]) -> None: ...
|
||||
|
||||
global___MapReport = MapReport
|
||||
|
||||
4
meshtastic/protobuf/paxcount_pb2.py
generated
4
meshtastic/protobuf/paxcount_pb2.py
generated
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/paxcount.proto\x12\x13meshtastic.protobuf\"5\n\x08Paxcount\x12\x0c\n\x04wifi\x18\x01 \x01(\r\x12\x0b\n\x03\x62le\x18\x02 \x01(\r\x12\x0e\n\x06uptime\x18\x03 \x01(\rBc\n\x13\x63om.geeksville.meshB\x0ePaxcountProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/paxcount.proto\x12\x13meshtastic.protobuf\"5\n\x08Paxcount\x12\x0c\n\x04wifi\x18\x01 \x01(\r\x12\x0b\n\x03\x62le\x18\x02 \x01(\r\x12\x0e\n\x06uptime\x18\x03 \x01(\rBd\n\x14org.meshtastic.protoB\x0ePaxcountProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.paxcount_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016PaxcountProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016PaxcountProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_PAXCOUNT']._serialized_start=59
|
||||
_globals['_PAXCOUNT']._serialized_end=112
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
6
meshtastic/protobuf/portnums_pb2.py
generated
6
meshtastic/protobuf/portnums_pb2.py
generated
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xcb\x04\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x18\n\x14RETICULUM_TUNNEL_APP\x10L\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42]\n\x13\x63om.geeksville.meshB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xfd\x05\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tALERT_APP\x10\x0b\x12\x18\n\x14KEY_VERIFICATION_APP\x10\x0c\x12\x14\n\x10REMOTE_SHELL_APP\x10\r\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x1e\n\x1aSTORE_FORWARD_PLUSPLUS_APP\x10#\x12\x13\n\x0fNODE_STATUS_APP\x10$\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x12\n\x0eLORAWAN_BRIDGE\x10K\x12\x18\n\x14RETICULUM_TUNNEL_APP\x10L\x12\x0f\n\x0b\x43\x41YENNE_APP\x10M\x12\x12\n\x0e\x41TAK_PLUGIN_V2\x10N\x12\x12\n\x0eGROUPALARM_APP\x10p\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42^\n\x14org.meshtastic.protoB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.portnums_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\010PortnumsZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\010PortnumsZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_PORTNUM']._serialized_start=60
|
||||
_globals['_PORTNUM']._serialized_end=647
|
||||
_globals['_PORTNUM']._serialized_end=825
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
90
meshtastic/protobuf/portnums_pb2.pyi
generated
90
meshtastic/protobuf/portnums_pb2.pyi
generated
@@ -97,6 +97,14 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
|
||||
"""
|
||||
Same as Text Message but used for critical alerts.
|
||||
"""
|
||||
KEY_VERIFICATION_APP: _PortNum.ValueType # 12
|
||||
"""
|
||||
Module/port for handling key verification requests.
|
||||
"""
|
||||
REMOTE_SHELL_APP: _PortNum.ValueType # 13
|
||||
"""
|
||||
Module/port for handling primitive remote shell access.
|
||||
"""
|
||||
REPLY_APP: _PortNum.ValueType # 32
|
||||
"""
|
||||
Provides a 'ping' service that replies to any packet it receives.
|
||||
@@ -113,6 +121,20 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
|
||||
Paxcounter lib included in the firmware
|
||||
ENCODING: protobuf
|
||||
"""
|
||||
STORE_FORWARD_PLUSPLUS_APP: _PortNum.ValueType # 35
|
||||
"""
|
||||
Store and Forward++ module included in the firmware
|
||||
ENCODING: protobuf
|
||||
This module is specifically for Native Linux nodes, and provides a Git-style
|
||||
chain of messages.
|
||||
"""
|
||||
NODE_STATUS_APP: _PortNum.ValueType # 36
|
||||
"""
|
||||
Node Status module
|
||||
ENCODING: protobuf
|
||||
This module allows setting an extra string of status for a node.
|
||||
Broadcasts on change and on a timer, possibly once a day.
|
||||
"""
|
||||
SERIAL_APP: _PortNum.ValueType # 64
|
||||
"""
|
||||
Provides a hardware serial interface to send and receive from the Meshtastic network.
|
||||
@@ -179,11 +201,34 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
|
||||
"""
|
||||
PowerStress based monitoring support (for automated power consumption testing)
|
||||
"""
|
||||
LORAWAN_BRIDGE: _PortNum.ValueType # 75
|
||||
"""
|
||||
LoraWAN Payload Transport
|
||||
ENCODING: compact binary LoRaWAN uplink (10-byte RF metadata + PHY payload) - see LoRaWANBridgeModule
|
||||
"""
|
||||
RETICULUM_TUNNEL_APP: _PortNum.ValueType # 76
|
||||
"""
|
||||
Reticulum Network Stack Tunnel App
|
||||
ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface
|
||||
"""
|
||||
CAYENNE_APP: _PortNum.ValueType # 77
|
||||
"""
|
||||
App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send
|
||||
arbitrary telemetry over meshtastic that is not covered by telemetry.proto
|
||||
ENCODING: CayenneLLP
|
||||
"""
|
||||
ATAK_PLUGIN_V2: _PortNum.ValueType # 78
|
||||
"""
|
||||
ATAK Plugin V2
|
||||
Portnum for payloads from the official Meshtastic ATAK plugin using
|
||||
TAKPacketV2 with zstd dictionary compression.
|
||||
"""
|
||||
GROUPALARM_APP: _PortNum.ValueType # 112
|
||||
"""
|
||||
GroupAlarm integration
|
||||
Used for transporting GroupAlarm-related messages between Meshtastic nodes
|
||||
and companion applications/services.
|
||||
"""
|
||||
PRIVATE_APP: _PortNum.ValueType # 256
|
||||
"""
|
||||
Private applications should use portnums >= 256.
|
||||
@@ -291,6 +336,14 @@ ALERT_APP: PortNum.ValueType # 11
|
||||
"""
|
||||
Same as Text Message but used for critical alerts.
|
||||
"""
|
||||
KEY_VERIFICATION_APP: PortNum.ValueType # 12
|
||||
"""
|
||||
Module/port for handling key verification requests.
|
||||
"""
|
||||
REMOTE_SHELL_APP: PortNum.ValueType # 13
|
||||
"""
|
||||
Module/port for handling primitive remote shell access.
|
||||
"""
|
||||
REPLY_APP: PortNum.ValueType # 32
|
||||
"""
|
||||
Provides a 'ping' service that replies to any packet it receives.
|
||||
@@ -307,6 +360,20 @@ PAXCOUNTER_APP: PortNum.ValueType # 34
|
||||
Paxcounter lib included in the firmware
|
||||
ENCODING: protobuf
|
||||
"""
|
||||
STORE_FORWARD_PLUSPLUS_APP: PortNum.ValueType # 35
|
||||
"""
|
||||
Store and Forward++ module included in the firmware
|
||||
ENCODING: protobuf
|
||||
This module is specifically for Native Linux nodes, and provides a Git-style
|
||||
chain of messages.
|
||||
"""
|
||||
NODE_STATUS_APP: PortNum.ValueType # 36
|
||||
"""
|
||||
Node Status module
|
||||
ENCODING: protobuf
|
||||
This module allows setting an extra string of status for a node.
|
||||
Broadcasts on change and on a timer, possibly once a day.
|
||||
"""
|
||||
SERIAL_APP: PortNum.ValueType # 64
|
||||
"""
|
||||
Provides a hardware serial interface to send and receive from the Meshtastic network.
|
||||
@@ -373,11 +440,34 @@ POWERSTRESS_APP: PortNum.ValueType # 74
|
||||
"""
|
||||
PowerStress based monitoring support (for automated power consumption testing)
|
||||
"""
|
||||
LORAWAN_BRIDGE: PortNum.ValueType # 75
|
||||
"""
|
||||
LoraWAN Payload Transport
|
||||
ENCODING: compact binary LoRaWAN uplink (10-byte RF metadata + PHY payload) - see LoRaWANBridgeModule
|
||||
"""
|
||||
RETICULUM_TUNNEL_APP: PortNum.ValueType # 76
|
||||
"""
|
||||
Reticulum Network Stack Tunnel App
|
||||
ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface
|
||||
"""
|
||||
CAYENNE_APP: PortNum.ValueType # 77
|
||||
"""
|
||||
App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send
|
||||
arbitrary telemetry over meshtastic that is not covered by telemetry.proto
|
||||
ENCODING: CayenneLLP
|
||||
"""
|
||||
ATAK_PLUGIN_V2: PortNum.ValueType # 78
|
||||
"""
|
||||
ATAK Plugin V2
|
||||
Portnum for payloads from the official Meshtastic ATAK plugin using
|
||||
TAKPacketV2 with zstd dictionary compression.
|
||||
"""
|
||||
GROUPALARM_APP: PortNum.ValueType # 112
|
||||
"""
|
||||
GroupAlarm integration
|
||||
Used for transporting GroupAlarm-related messages between Meshtastic nodes
|
||||
and companion applications/services.
|
||||
"""
|
||||
PRIVATE_APP: PortNum.ValueType # 256
|
||||
"""
|
||||
Private applications should use portnums >= 256.
|
||||
|
||||
4
meshtastic/protobuf/powermon_pb2.py
generated
4
meshtastic/protobuf/powermon_pb2.py
generated
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\"\x88\x03\n\x12PowerStressMessage\x12;\n\x03\x63md\x18\x01 \x01(\x0e\x32..meshtastic.protobuf.PowerStressMessage.Opcode\x12\x13\n\x0bnum_seconds\x18\x02 \x01(\x02\"\x9f\x02\n\x06Opcode\x12\t\n\x05UNSET\x10\x00\x12\x0e\n\nPRINT_INFO\x10\x01\x12\x0f\n\x0b\x46ORCE_QUIET\x10\x02\x12\r\n\tEND_QUIET\x10\x03\x12\r\n\tSCREEN_ON\x10\x10\x12\x0e\n\nSCREEN_OFF\x10\x11\x12\x0c\n\x08\x43PU_IDLE\x10 \x12\x11\n\rCPU_DEEPSLEEP\x10!\x12\x0e\n\nCPU_FULLON\x10\"\x12\n\n\x06LED_ON\x10\x30\x12\x0b\n\x07LED_OFF\x10\x31\x12\x0c\n\x08LORA_OFF\x10@\x12\x0b\n\x07LORA_TX\x10\x41\x12\x0b\n\x07LORA_RX\x10\x42\x12\n\n\x06\x42T_OFF\x10P\x12\t\n\x05\x42T_ON\x10Q\x12\x0c\n\x08WIFI_OFF\x10`\x12\x0b\n\x07WIFI_ON\x10\x61\x12\x0b\n\x07GPS_OFF\x10p\x12\n\n\x06GPS_ON\x10qBc\n\x13\x63om.geeksville.meshB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\"\x88\x03\n\x12PowerStressMessage\x12;\n\x03\x63md\x18\x01 \x01(\x0e\x32..meshtastic.protobuf.PowerStressMessage.Opcode\x12\x13\n\x0bnum_seconds\x18\x02 \x01(\x02\"\x9f\x02\n\x06Opcode\x12\t\n\x05UNSET\x10\x00\x12\x0e\n\nPRINT_INFO\x10\x01\x12\x0f\n\x0b\x46ORCE_QUIET\x10\x02\x12\r\n\tEND_QUIET\x10\x03\x12\r\n\tSCREEN_ON\x10\x10\x12\x0e\n\nSCREEN_OFF\x10\x11\x12\x0c\n\x08\x43PU_IDLE\x10 \x12\x11\n\rCPU_DEEPSLEEP\x10!\x12\x0e\n\nCPU_FULLON\x10\"\x12\n\n\x06LED_ON\x10\x30\x12\x0b\n\x07LED_OFF\x10\x31\x12\x0c\n\x08LORA_OFF\x10@\x12\x0b\n\x07LORA_TX\x10\x41\x12\x0b\n\x07LORA_RX\x10\x42\x12\n\n\x06\x42T_OFF\x10P\x12\t\n\x05\x42T_ON\x10Q\x12\x0c\n\x08WIFI_OFF\x10`\x12\x0b\n\x07WIFI_ON\x10\x61\x12\x0b\n\x07GPS_OFF\x10p\x12\n\n\x06GPS_ON\x10qBd\n\x14org.meshtastic.protoB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.powermon_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016PowerMonProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016PowerMonProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_POWERMON']._serialized_start=60
|
||||
_globals['_POWERMON']._serialized_end=284
|
||||
_globals['_POWERMON_STATE']._serialized_start=73
|
||||
|
||||
4
meshtastic/protobuf/remote_hardware_pb2.py
generated
4
meshtastic/protobuf/remote_hardware_pb2.py
generated
@@ -13,14 +13,14 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)meshtastic/protobuf/remote_hardware.proto\x12\x13meshtastic.protobuf\"\xdf\x01\n\x0fHardwareMessage\x12\x37\n\x04type\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.HardwareMessage.Type\x12\x11\n\tgpio_mask\x18\x02 \x01(\x04\x12\x12\n\ngpio_value\x18\x03 \x01(\x04\"l\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0f\n\x0bWRITE_GPIOS\x10\x01\x12\x0f\n\x0bWATCH_GPIOS\x10\x02\x12\x11\n\rGPIOS_CHANGED\x10\x03\x12\x0e\n\nREAD_GPIOS\x10\x04\x12\x14\n\x10READ_GPIOS_REPLY\x10\x05\x42\x63\n\x13\x63om.geeksville.meshB\x0eRemoteHardwareZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)meshtastic/protobuf/remote_hardware.proto\x12\x13meshtastic.protobuf\"\xdf\x01\n\x0fHardwareMessage\x12\x37\n\x04type\x18\x01 \x01(\x0e\x32).meshtastic.protobuf.HardwareMessage.Type\x12\x11\n\tgpio_mask\x18\x02 \x01(\x04\x12\x12\n\ngpio_value\x18\x03 \x01(\x04\"l\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0f\n\x0bWRITE_GPIOS\x10\x01\x12\x0f\n\x0bWATCH_GPIOS\x10\x02\x12\x11\n\rGPIOS_CHANGED\x10\x03\x12\x0e\n\nREAD_GPIOS\x10\x04\x12\x14\n\x10READ_GPIOS_REPLY\x10\x05\x42\x64\n\x14org.meshtastic.protoB\x0eRemoteHardwareZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.remote_hardware_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016RemoteHardwareZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016RemoteHardwareZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_HARDWAREMESSAGE']._serialized_start=67
|
||||
_globals['_HARDWAREMESSAGE']._serialized_end=290
|
||||
_globals['_HARDWAREMESSAGE_TYPE']._serialized_start=182
|
||||
|
||||
11
meshtastic/protobuf/rtttl_pb2.py
generated
11
meshtastic/protobuf/rtttl_pb2.py
generated
@@ -11,16 +11,19 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/rtttl.proto\x12\x13meshtastic.protobuf\"\x1f\n\x0bRTTTLConfig\x12\x10\n\x08ringtone\x18\x01 \x01(\tBf\n\x13\x63om.geeksville.meshB\x11RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fmeshtastic/protobuf/rtttl.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\'\n\x0bRTTTLConfig\x12\x18\n\x08ringtone\x18\x01 \x01(\tB\x06\x92?\x03\x08\xe7\x01\x42g\n\x14org.meshtastic.protoB\x11RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.rtttl_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_RTTTLCONFIG']._serialized_start=56
|
||||
_globals['_RTTTLCONFIG']._serialized_end=87
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_RTTTLCONFIG.fields_by_name['ringtone']._options = None
|
||||
_RTTTLCONFIG.fields_by_name['ringtone']._serialized_options = b'\222?\003\010\347\001'
|
||||
_globals['_RTTTLCONFIG']._serialized_start=90
|
||||
_globals['_RTTTLCONFIG']._serialized_end=129
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
39
meshtastic/protobuf/serial_hal_pb2.py
generated
Normal file
39
meshtastic/protobuf/serial_hal_pb2.py
generated
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: meshtastic/protobuf/serial_hal.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/serial_hal.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xb3\x02\n\x10SerialHalCommand\x12\x16\n\x0etransaction_id\x18\x01 \x01(\r\x12\x38\n\x04type\x18\x02 \x01(\x0e\x32*.meshtastic.protobuf.SerialHalCommand.Type\x12\x0b\n\x03pin\x18\x03 \x01(\r\x12\r\n\x05value\x18\x04 \x01(\r\x12\x0c\n\x04mode\x18\x05 \x01(\r\x12\x14\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x04\"\x8c\x01\n\x04Type\x12\t\n\x05UNSET\x10\x00\x12\x0c\n\x08PIN_MODE\x10\x01\x12\x11\n\rDIGITAL_WRITE\x10\x02\x12\x10\n\x0c\x44IGITAL_READ\x10\x03\x12\x14\n\x10\x41TTACH_INTERRUPT\x10\x04\x12\x14\n\x10\x44\x45TACH_INTERRUPT\x10\x05\x12\x10\n\x0cSPI_TRANSFER\x10\x06\x12\x08\n\x04NOOP\x10\x07\"\xe4\x01\n\x11SerialHalResponse\x12\x16\n\x0etransaction_id\x18\x01 \x01(\r\x12=\n\x06result\x18\x02 \x01(\x0e\x32-.meshtastic.protobuf.SerialHalResponse.Result\x12\r\n\x05value\x18\x03 \x01(\r\x12\x14\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x04\x12\x14\n\x05\x65rror\x18\x05 \x01(\tB\x05\x92?\x02\x08P\"=\n\x06Result\x12\x06\n\x02OK\x10\x00\x12\t\n\x05\x45RROR\x10\x01\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10\x02\x12\x0f\n\x0bUNSUPPORTED\x10\x03\x42_\n\x14org.meshtastic.protoB\tSerialHalZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.serial_hal_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\tSerialHalZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_SERIALHALCOMMAND.fields_by_name['data']._options = None
|
||||
_SERIALHALCOMMAND.fields_by_name['data']._serialized_options = b'\222?\003\010\200\004'
|
||||
_SERIALHALRESPONSE.fields_by_name['data']._options = None
|
||||
_SERIALHALRESPONSE.fields_by_name['data']._serialized_options = b'\222?\003\010\200\004'
|
||||
_SERIALHALRESPONSE.fields_by_name['error']._options = None
|
||||
_SERIALHALRESPONSE.fields_by_name['error']._serialized_options = b'\222?\002\010P'
|
||||
_globals['_SERIALHALCOMMAND']._serialized_start=96
|
||||
_globals['_SERIALHALCOMMAND']._serialized_end=403
|
||||
_globals['_SERIALHALCOMMAND_TYPE']._serialized_start=263
|
||||
_globals['_SERIALHALCOMMAND_TYPE']._serialized_end=403
|
||||
_globals['_SERIALHALRESPONSE']._serialized_start=406
|
||||
_globals['_SERIALHALRESPONSE']._serialized_end=634
|
||||
_globals['_SERIALHALRESPONSE_RESULT']._serialized_start=573
|
||||
_globals['_SERIALHALRESPONSE_RESULT']._serialized_end=634
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
140
meshtastic/protobuf/serial_hal_pb2.pyi
generated
Normal file
140
meshtastic/protobuf/serial_hal_pb2.pyi
generated
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
@generated by mypy-protobuf. Do not edit manually!
|
||||
isort:skip_file
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import google.protobuf.descriptor
|
||||
import google.protobuf.internal.enum_type_wrapper
|
||||
import google.protobuf.message
|
||||
import sys
|
||||
import typing
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
import typing as typing_extensions
|
||||
else:
|
||||
import typing_extensions
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
|
||||
|
||||
@typing.final
|
||||
class SerialHalCommand(google.protobuf.message.Message):
|
||||
"""SerialHalCommand messages are sent from host to device over the SerialHal
|
||||
framing stream. Responses normally come back as SerialHalResponse with the
|
||||
same transaction_id.
|
||||
|
||||
Interrupt notifications are the one asynchronous exception: the device emits
|
||||
an unsolicited SerialHalResponse with transaction_id == 0 and value == pin.
|
||||
Host implementations should treat those frames as interrupt events rather
|
||||
than replies to an outstanding request.
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _Type:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _TypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SerialHalCommand._Type.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
UNSET: SerialHalCommand._Type.ValueType # 0
|
||||
PIN_MODE: SerialHalCommand._Type.ValueType # 1
|
||||
DIGITAL_WRITE: SerialHalCommand._Type.ValueType # 2
|
||||
DIGITAL_READ: SerialHalCommand._Type.ValueType # 3
|
||||
ATTACH_INTERRUPT: SerialHalCommand._Type.ValueType # 4
|
||||
DETACH_INTERRUPT: SerialHalCommand._Type.ValueType # 5
|
||||
SPI_TRANSFER: SerialHalCommand._Type.ValueType # 6
|
||||
NOOP: SerialHalCommand._Type.ValueType # 7
|
||||
|
||||
class Type(_Type, metaclass=_TypeEnumTypeWrapper): ...
|
||||
UNSET: SerialHalCommand.Type.ValueType # 0
|
||||
PIN_MODE: SerialHalCommand.Type.ValueType # 1
|
||||
DIGITAL_WRITE: SerialHalCommand.Type.ValueType # 2
|
||||
DIGITAL_READ: SerialHalCommand.Type.ValueType # 3
|
||||
ATTACH_INTERRUPT: SerialHalCommand.Type.ValueType # 4
|
||||
DETACH_INTERRUPT: SerialHalCommand.Type.ValueType # 5
|
||||
SPI_TRANSFER: SerialHalCommand.Type.ValueType # 6
|
||||
NOOP: SerialHalCommand.Type.ValueType # 7
|
||||
|
||||
TRANSACTION_ID_FIELD_NUMBER: builtins.int
|
||||
TYPE_FIELD_NUMBER: builtins.int
|
||||
PIN_FIELD_NUMBER: builtins.int
|
||||
VALUE_FIELD_NUMBER: builtins.int
|
||||
MODE_FIELD_NUMBER: builtins.int
|
||||
DATA_FIELD_NUMBER: builtins.int
|
||||
transaction_id: builtins.int
|
||||
"""Host-assigned request id. Replies echo this id back in
|
||||
SerialHalResponse.transaction_id.
|
||||
"""
|
||||
type: global___SerialHalCommand.Type.ValueType
|
||||
pin: builtins.int
|
||||
value: builtins.int
|
||||
mode: builtins.int
|
||||
data: builtins.bytes
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
transaction_id: builtins.int = ...,
|
||||
type: global___SerialHalCommand.Type.ValueType = ...,
|
||||
pin: builtins.int = ...,
|
||||
value: builtins.int = ...,
|
||||
mode: builtins.int = ...,
|
||||
data: builtins.bytes = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["data", b"data", "mode", b"mode", "pin", b"pin", "transaction_id", b"transaction_id", "type", b"type", "value", b"value"]) -> None: ...
|
||||
|
||||
global___SerialHalCommand = SerialHalCommand
|
||||
|
||||
@typing.final
|
||||
class SerialHalResponse(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _Result:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _ResultEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SerialHalResponse._Result.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
OK: SerialHalResponse._Result.ValueType # 0
|
||||
ERROR: SerialHalResponse._Result.ValueType # 1
|
||||
BAD_REQUEST: SerialHalResponse._Result.ValueType # 2
|
||||
UNSUPPORTED: SerialHalResponse._Result.ValueType # 3
|
||||
|
||||
class Result(_Result, metaclass=_ResultEnumTypeWrapper): ...
|
||||
OK: SerialHalResponse.Result.ValueType # 0
|
||||
ERROR: SerialHalResponse.Result.ValueType # 1
|
||||
BAD_REQUEST: SerialHalResponse.Result.ValueType # 2
|
||||
UNSUPPORTED: SerialHalResponse.Result.ValueType # 3
|
||||
|
||||
TRANSACTION_ID_FIELD_NUMBER: builtins.int
|
||||
RESULT_FIELD_NUMBER: builtins.int
|
||||
VALUE_FIELD_NUMBER: builtins.int
|
||||
DATA_FIELD_NUMBER: builtins.int
|
||||
ERROR_FIELD_NUMBER: builtins.int
|
||||
transaction_id: builtins.int
|
||||
"""Matches the originating SerialHalCommand.transaction_id for normal
|
||||
request/response traffic.
|
||||
|
||||
A value of 0 indicates an unsolicited interrupt notification generated by
|
||||
the device. In that case, the host should interpret value as the GPIO pin
|
||||
that triggered.
|
||||
"""
|
||||
result: global___SerialHalResponse.Result.ValueType
|
||||
value: builtins.int
|
||||
"""Used by DIGITAL_READ replies and interrupt notifications. For interrupt
|
||||
notifications (transaction_id == 0), this carries the pin number.
|
||||
"""
|
||||
data: builtins.bytes
|
||||
error: builtins.str
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
transaction_id: builtins.int = ...,
|
||||
result: global___SerialHalResponse.Result.ValueType = ...,
|
||||
value: builtins.int = ...,
|
||||
data: builtins.bytes = ...,
|
||||
error: builtins.str = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["data", b"data", "error", b"error", "result", b"result", "transaction_id", b"transaction_id", "value", b"value"]) -> None: ...
|
||||
|
||||
global___SerialHalResponse = SerialHalResponse
|
||||
27
meshtastic/protobuf/storeforward_pb2.py
generated
27
meshtastic/protobuf/storeforward_pb2.py
generated
@@ -11,24 +11,27 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&meshtastic/protobuf/storeforward.proto\x12\x13meshtastic.protobuf\"\xc0\x07\n\x0fStoreAndForward\x12@\n\x02rr\x18\x01 \x01(\x0e\x32\x34.meshtastic.protobuf.StoreAndForward.RequestResponse\x12@\n\x05stats\x18\x02 \x01(\x0b\x32/.meshtastic.protobuf.StoreAndForward.StatisticsH\x00\x12?\n\x07history\x18\x03 \x01(\x0b\x32,.meshtastic.protobuf.StoreAndForward.HistoryH\x00\x12\x43\n\theartbeat\x18\x04 \x01(\x0b\x32..meshtastic.protobuf.StoreAndForward.HeartbeatH\x00\x12\x0e\n\x04text\x18\x05 \x01(\x0cH\x00\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\"\xbc\x02\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\x0cROUTER_STATS\x10\x07\x12\x16\n\x12ROUTER_TEXT_DIRECT\x10\x08\x12\x19\n\x15ROUTER_TEXT_BROADCAST\x10\t\x12\x10\n\x0c\x43LIENT_ERROR\x10@\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x41\x12\x10\n\x0c\x43LIENT_STATS\x10\x42\x12\x0f\n\x0b\x43LIENT_PING\x10\x43\x12\x0f\n\x0b\x43LIENT_PONG\x10\x44\x12\x10\n\x0c\x43LIENT_ABORT\x10jB\t\n\x07variantBj\n\x13\x63om.geeksville.meshB\x15StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&meshtastic/protobuf/storeforward.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xc8\x07\n\x0fStoreAndForward\x12@\n\x02rr\x18\x01 \x01(\x0e\x32\x34.meshtastic.protobuf.StoreAndForward.RequestResponse\x12@\n\x05stats\x18\x02 \x01(\x0b\x32/.meshtastic.protobuf.StoreAndForward.StatisticsH\x00\x12?\n\x07history\x18\x03 \x01(\x0b\x32,.meshtastic.protobuf.StoreAndForward.HistoryH\x00\x12\x43\n\theartbeat\x18\x04 \x01(\x0b\x32..meshtastic.protobuf.StoreAndForward.HeartbeatH\x00\x12\x16\n\x04text\x18\x05 \x01(\x0c\x42\x06\x92?\x03\x08\xe9\x01H\x00\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\"\xbc\x02\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\x0cROUTER_STATS\x10\x07\x12\x16\n\x12ROUTER_TEXT_DIRECT\x10\x08\x12\x19\n\x15ROUTER_TEXT_BROADCAST\x10\t\x12\x10\n\x0c\x43LIENT_ERROR\x10@\x12\x12\n\x0e\x43LIENT_HISTORY\x10\x41\x12\x10\n\x0c\x43LIENT_STATS\x10\x42\x12\x0f\n\x0b\x43LIENT_PING\x10\x43\x12\x0f\n\x0b\x43LIENT_PONG\x10\x44\x12\x10\n\x0c\x43LIENT_ABORT\x10jB\t\n\x07variantBk\n\x14org.meshtastic.protoB\x15StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.storeforward_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\025StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_STOREANDFORWARD']._serialized_start=64
|
||||
_globals['_STOREANDFORWARD']._serialized_end=1024
|
||||
_globals['_STOREANDFORWARD_STATISTICS']._serialized_start=366
|
||||
_globals['_STOREANDFORWARD_STATISTICS']._serialized_end=571
|
||||
_globals['_STOREANDFORWARD_HISTORY']._serialized_start=573
|
||||
_globals['_STOREANDFORWARD_HISTORY']._serialized_end=646
|
||||
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_start=648
|
||||
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_end=694
|
||||
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_start=697
|
||||
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_end=1013
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\025StoreAndForwardProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_STOREANDFORWARD.fields_by_name['text']._options = None
|
||||
_STOREANDFORWARD.fields_by_name['text']._serialized_options = b'\222?\003\010\351\001'
|
||||
_globals['_STOREANDFORWARD']._serialized_start=98
|
||||
_globals['_STOREANDFORWARD']._serialized_end=1066
|
||||
_globals['_STOREANDFORWARD_STATISTICS']._serialized_start=408
|
||||
_globals['_STOREANDFORWARD_STATISTICS']._serialized_end=613
|
||||
_globals['_STOREANDFORWARD_HISTORY']._serialized_start=615
|
||||
_globals['_STOREANDFORWARD_HISTORY']._serialized_end=688
|
||||
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_start=690
|
||||
_globals['_STOREANDFORWARD_HEARTBEAT']._serialized_end=736
|
||||
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_start=739
|
||||
_globals['_STOREANDFORWARD_REQUESTRESPONSE']._serialized_end=1055
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
73
meshtastic/protobuf/telemetry_pb2.py
generated
73
meshtastic/protobuf/telemetry_pb2.py
generated
File diff suppressed because one or more lines are too long
605
meshtastic/protobuf/telemetry_pb2.pyi
generated
605
meshtastic/protobuf/telemetry_pb2.pyi
generated
@@ -4,7 +4,9 @@ isort:skip_file
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import collections.abc
|
||||
import google.protobuf.descriptor
|
||||
import google.protobuf.internal.containers
|
||||
import google.protobuf.internal.enum_type_wrapper
|
||||
import google.protobuf.message
|
||||
import sys
|
||||
@@ -53,7 +55,7 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
||||
"""
|
||||
SHTC3: _TelemetrySensorType.ValueType # 7
|
||||
"""
|
||||
High accuracy temperature and humidity
|
||||
TODO - REMOVE High accuracy temperature and humidity
|
||||
"""
|
||||
LPS22: _TelemetrySensorType.ValueType # 8
|
||||
"""
|
||||
@@ -73,7 +75,7 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
||||
"""
|
||||
SHT31: _TelemetrySensorType.ValueType # 12
|
||||
"""
|
||||
High accuracy temperature and humidity
|
||||
TODO - REMOVE High accuracy temperature and humidity
|
||||
"""
|
||||
PMSA003I: _TelemetrySensorType.ValueType # 13
|
||||
"""
|
||||
@@ -93,7 +95,7 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
||||
"""
|
||||
SHT4X: _TelemetrySensorType.ValueType # 17
|
||||
"""
|
||||
Sensirion High accuracy temperature and humidity
|
||||
TODO - REMOVE Sensirion High accuracy temperature and humidity
|
||||
"""
|
||||
VEML7700: _TelemetrySensorType.ValueType # 18
|
||||
"""
|
||||
@@ -175,6 +177,70 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
||||
"""
|
||||
RAKWireless RAK12035 Soil Moisture Sensor Module
|
||||
"""
|
||||
MAX17261: _TelemetrySensorType.ValueType # 38
|
||||
"""
|
||||
MAX17261 lipo battery gauge
|
||||
"""
|
||||
PCT2075: _TelemetrySensorType.ValueType # 39
|
||||
"""
|
||||
PCT2075 Temperature Sensor
|
||||
"""
|
||||
ADS1X15: _TelemetrySensorType.ValueType # 40
|
||||
"""
|
||||
ADS1X15 ADC
|
||||
"""
|
||||
ADS1X15_ALT: _TelemetrySensorType.ValueType # 41
|
||||
"""
|
||||
ADS1X15 ADC_ALT
|
||||
"""
|
||||
SFA30: _TelemetrySensorType.ValueType # 42
|
||||
"""
|
||||
Sensirion SFA30 Formaldehyde sensor
|
||||
"""
|
||||
SEN5X: _TelemetrySensorType.ValueType # 43
|
||||
"""
|
||||
SEN5X PM SENSORS
|
||||
"""
|
||||
TSL2561: _TelemetrySensorType.ValueType # 44
|
||||
"""
|
||||
TSL2561 light sensor
|
||||
"""
|
||||
BH1750: _TelemetrySensorType.ValueType # 45
|
||||
"""
|
||||
BH1750 light sensor
|
||||
"""
|
||||
HDC1080: _TelemetrySensorType.ValueType # 46
|
||||
"""
|
||||
HDC1080 Temperature and Humidity Sensor
|
||||
"""
|
||||
SHT21: _TelemetrySensorType.ValueType # 47
|
||||
"""
|
||||
TODO - REMOVE STH21 Temperature and R. Humidity sensor
|
||||
"""
|
||||
STC31: _TelemetrySensorType.ValueType # 48
|
||||
"""
|
||||
Sensirion STC31 CO2 sensor
|
||||
"""
|
||||
SCD30: _TelemetrySensorType.ValueType # 49
|
||||
"""
|
||||
SCD30 CO2, humidity, temperature sensor
|
||||
"""
|
||||
SHTXX: _TelemetrySensorType.ValueType # 50
|
||||
"""
|
||||
SHT family of sensors for temperature and humidity
|
||||
"""
|
||||
DS248X: _TelemetrySensorType.ValueType # 51
|
||||
"""
|
||||
DS248X Bridge for one-wire temperature sensors
|
||||
"""
|
||||
MMC5983MA: _TelemetrySensorType.ValueType # 52
|
||||
"""
|
||||
MMC5983MA 3-Axis Digital Magnetic Sensor
|
||||
"""
|
||||
ICM42607P: _TelemetrySensorType.ValueType # 53
|
||||
"""
|
||||
ICM-42607-P 6‑Axis IMU
|
||||
"""
|
||||
|
||||
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
|
||||
"""
|
||||
@@ -211,7 +277,7 @@ High accuracy temperature and pressure
|
||||
"""
|
||||
SHTC3: TelemetrySensorType.ValueType # 7
|
||||
"""
|
||||
High accuracy temperature and humidity
|
||||
TODO - REMOVE High accuracy temperature and humidity
|
||||
"""
|
||||
LPS22: TelemetrySensorType.ValueType # 8
|
||||
"""
|
||||
@@ -231,7 +297,7 @@ QMC5883L: TelemetrySensorType.ValueType # 11
|
||||
"""
|
||||
SHT31: TelemetrySensorType.ValueType # 12
|
||||
"""
|
||||
High accuracy temperature and humidity
|
||||
TODO - REMOVE High accuracy temperature and humidity
|
||||
"""
|
||||
PMSA003I: TelemetrySensorType.ValueType # 13
|
||||
"""
|
||||
@@ -251,7 +317,7 @@ RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
|
||||
"""
|
||||
SHT4X: TelemetrySensorType.ValueType # 17
|
||||
"""
|
||||
Sensirion High accuracy temperature and humidity
|
||||
TODO - REMOVE Sensirion High accuracy temperature and humidity
|
||||
"""
|
||||
VEML7700: TelemetrySensorType.ValueType # 18
|
||||
"""
|
||||
@@ -333,6 +399,70 @@ RAK12035: TelemetrySensorType.ValueType # 37
|
||||
"""
|
||||
RAKWireless RAK12035 Soil Moisture Sensor Module
|
||||
"""
|
||||
MAX17261: TelemetrySensorType.ValueType # 38
|
||||
"""
|
||||
MAX17261 lipo battery gauge
|
||||
"""
|
||||
PCT2075: TelemetrySensorType.ValueType # 39
|
||||
"""
|
||||
PCT2075 Temperature Sensor
|
||||
"""
|
||||
ADS1X15: TelemetrySensorType.ValueType # 40
|
||||
"""
|
||||
ADS1X15 ADC
|
||||
"""
|
||||
ADS1X15_ALT: TelemetrySensorType.ValueType # 41
|
||||
"""
|
||||
ADS1X15 ADC_ALT
|
||||
"""
|
||||
SFA30: TelemetrySensorType.ValueType # 42
|
||||
"""
|
||||
Sensirion SFA30 Formaldehyde sensor
|
||||
"""
|
||||
SEN5X: TelemetrySensorType.ValueType # 43
|
||||
"""
|
||||
SEN5X PM SENSORS
|
||||
"""
|
||||
TSL2561: TelemetrySensorType.ValueType # 44
|
||||
"""
|
||||
TSL2561 light sensor
|
||||
"""
|
||||
BH1750: TelemetrySensorType.ValueType # 45
|
||||
"""
|
||||
BH1750 light sensor
|
||||
"""
|
||||
HDC1080: TelemetrySensorType.ValueType # 46
|
||||
"""
|
||||
HDC1080 Temperature and Humidity Sensor
|
||||
"""
|
||||
SHT21: TelemetrySensorType.ValueType # 47
|
||||
"""
|
||||
TODO - REMOVE STH21 Temperature and R. Humidity sensor
|
||||
"""
|
||||
STC31: TelemetrySensorType.ValueType # 48
|
||||
"""
|
||||
Sensirion STC31 CO2 sensor
|
||||
"""
|
||||
SCD30: TelemetrySensorType.ValueType # 49
|
||||
"""
|
||||
SCD30 CO2, humidity, temperature sensor
|
||||
"""
|
||||
SHTXX: TelemetrySensorType.ValueType # 50
|
||||
"""
|
||||
SHT family of sensors for temperature and humidity
|
||||
"""
|
||||
DS248X: TelemetrySensorType.ValueType # 51
|
||||
"""
|
||||
DS248X Bridge for one-wire temperature sensors
|
||||
"""
|
||||
MMC5983MA: TelemetrySensorType.ValueType # 52
|
||||
"""
|
||||
MMC5983MA 3-Axis Digital Magnetic Sensor
|
||||
"""
|
||||
ICM42607P: TelemetrySensorType.ValueType # 53
|
||||
"""
|
||||
ICM-42607-P 6‑Axis IMU
|
||||
"""
|
||||
global___TelemetrySensorType = TelemetrySensorType
|
||||
|
||||
@typing.final
|
||||
@@ -422,6 +552,7 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
||||
RAINFALL_24H_FIELD_NUMBER: builtins.int
|
||||
SOIL_MOISTURE_FIELD_NUMBER: builtins.int
|
||||
SOIL_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
ONE_WIRE_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
temperature: builtins.float
|
||||
"""
|
||||
Temperature measured
|
||||
@@ -512,6 +643,12 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
||||
"""
|
||||
Soil temperature measured (*C)
|
||||
"""
|
||||
@property
|
||||
def one_wire_temperature(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
|
||||
"""
|
||||
One-wire temperature (*C)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -537,9 +674,10 @@ class EnvironmentMetrics(google.protobuf.message.Message):
|
||||
rainfall_24h: builtins.float | None = ...,
|
||||
soil_moisture: builtins.int | None = ...,
|
||||
soil_temperature: builtins.float | None = ...,
|
||||
one_wire_temperature: collections.abc.Iterable[builtins.float] | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_rainfall_1h", b"_rainfall_1h", "_rainfall_24h", b"_rainfall_24h", "_relative_humidity", b"_relative_humidity", "_soil_moisture", b"_soil_moisture", "_soil_temperature", b"_soil_temperature", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "radiation", b"radiation", "rainfall_1h", b"rainfall_1h", "rainfall_24h", b"rainfall_24h", "relative_humidity", b"relative_humidity", "soil_moisture", b"soil_moisture", "soil_temperature", b"soil_temperature", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_rainfall_1h", b"_rainfall_1h", "_rainfall_24h", b"_rainfall_24h", "_relative_humidity", b"_relative_humidity", "_soil_moisture", b"_soil_moisture", "_soil_temperature", b"_soil_temperature", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "radiation", b"radiation", "rainfall_1h", b"rainfall_1h", "rainfall_24h", b"rainfall_24h", "relative_humidity", b"relative_humidity", "soil_moisture", b"soil_moisture", "soil_temperature", b"soil_temperature", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lux", b"_lux", "_radiation", b"_radiation", "_rainfall_1h", b"_rainfall_1h", "_rainfall_24h", b"_rainfall_24h", "_relative_humidity", b"_relative_humidity", "_soil_moisture", b"_soil_moisture", "_soil_temperature", b"_soil_temperature", "_temperature", b"_temperature", "_uv_lux", b"_uv_lux", "_voltage", b"_voltage", "_weight", b"_weight", "_white_lux", b"_white_lux", "_wind_direction", b"_wind_direction", "_wind_gust", b"_wind_gust", "_wind_lull", b"_wind_lull", "_wind_speed", b"_wind_speed", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lux", b"lux", "one_wire_temperature", b"one_wire_temperature", "radiation", b"radiation", "rainfall_1h", b"rainfall_1h", "rainfall_24h", b"rainfall_24h", "relative_humidity", b"relative_humidity", "soil_moisture", b"soil_moisture", "soil_temperature", b"soil_temperature", "temperature", b"temperature", "uv_lux", b"uv_lux", "voltage", b"voltage", "weight", b"weight", "white_lux", b"white_lux", "wind_direction", b"wind_direction", "wind_gust", b"wind_gust", "wind_lull", b"wind_lull", "wind_speed", b"wind_speed"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_barometric_pressure", b"_barometric_pressure"]) -> typing.Literal["barometric_pressure"] | None: ...
|
||||
@typing.overload
|
||||
@@ -601,6 +739,16 @@ class PowerMetrics(google.protobuf.message.Message):
|
||||
CH2_CURRENT_FIELD_NUMBER: builtins.int
|
||||
CH3_VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CH3_CURRENT_FIELD_NUMBER: builtins.int
|
||||
CH4_VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CH4_CURRENT_FIELD_NUMBER: builtins.int
|
||||
CH5_VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CH5_CURRENT_FIELD_NUMBER: builtins.int
|
||||
CH6_VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CH6_CURRENT_FIELD_NUMBER: builtins.int
|
||||
CH7_VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CH7_CURRENT_FIELD_NUMBER: builtins.int
|
||||
CH8_VOLTAGE_FIELD_NUMBER: builtins.int
|
||||
CH8_CURRENT_FIELD_NUMBER: builtins.int
|
||||
ch1_voltage: builtins.float
|
||||
"""
|
||||
Voltage (Ch1)
|
||||
@@ -625,6 +773,46 @@ class PowerMetrics(google.protobuf.message.Message):
|
||||
"""
|
||||
Current (Ch3)
|
||||
"""
|
||||
ch4_voltage: builtins.float
|
||||
"""
|
||||
Voltage (Ch4)
|
||||
"""
|
||||
ch4_current: builtins.float
|
||||
"""
|
||||
Current (Ch4)
|
||||
"""
|
||||
ch5_voltage: builtins.float
|
||||
"""
|
||||
Voltage (Ch5)
|
||||
"""
|
||||
ch5_current: builtins.float
|
||||
"""
|
||||
Current (Ch5)
|
||||
"""
|
||||
ch6_voltage: builtins.float
|
||||
"""
|
||||
Voltage (Ch6)
|
||||
"""
|
||||
ch6_current: builtins.float
|
||||
"""
|
||||
Current (Ch6)
|
||||
"""
|
||||
ch7_voltage: builtins.float
|
||||
"""
|
||||
Voltage (Ch7)
|
||||
"""
|
||||
ch7_current: builtins.float
|
||||
"""
|
||||
Current (Ch7)
|
||||
"""
|
||||
ch8_voltage: builtins.float
|
||||
"""
|
||||
Voltage (Ch8)
|
||||
"""
|
||||
ch8_current: builtins.float
|
||||
"""
|
||||
Current (Ch8)
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -634,9 +822,19 @@ class PowerMetrics(google.protobuf.message.Message):
|
||||
ch2_current: builtins.float | None = ...,
|
||||
ch3_voltage: builtins.float | None = ...,
|
||||
ch3_current: builtins.float | None = ...,
|
||||
ch4_voltage: builtins.float | None = ...,
|
||||
ch4_current: builtins.float | None = ...,
|
||||
ch5_voltage: builtins.float | None = ...,
|
||||
ch5_current: builtins.float | None = ...,
|
||||
ch6_voltage: builtins.float | None = ...,
|
||||
ch6_current: builtins.float | None = ...,
|
||||
ch7_voltage: builtins.float | None = ...,
|
||||
ch7_current: builtins.float | None = ...,
|
||||
ch8_voltage: builtins.float | None = ...,
|
||||
ch8_current: builtins.float | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage"]) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "_ch4_current", b"_ch4_current", "_ch4_voltage", b"_ch4_voltage", "_ch5_current", b"_ch5_current", "_ch5_voltage", b"_ch5_voltage", "_ch6_current", b"_ch6_current", "_ch6_voltage", b"_ch6_voltage", "_ch7_current", b"_ch7_current", "_ch7_voltage", b"_ch7_voltage", "_ch8_current", b"_ch8_current", "_ch8_voltage", b"_ch8_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage", "ch4_current", b"ch4_current", "ch4_voltage", b"ch4_voltage", "ch5_current", b"ch5_current", "ch5_voltage", b"ch5_voltage", "ch6_current", b"ch6_current", "ch6_voltage", b"ch6_voltage", "ch7_current", b"ch7_current", "ch7_voltage", b"ch7_voltage", "ch8_current", b"ch8_current", "ch8_voltage", b"ch8_voltage"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_ch1_current", b"_ch1_current", "_ch1_voltage", b"_ch1_voltage", "_ch2_current", b"_ch2_current", "_ch2_voltage", b"_ch2_voltage", "_ch3_current", b"_ch3_current", "_ch3_voltage", b"_ch3_voltage", "_ch4_current", b"_ch4_current", "_ch4_voltage", b"_ch4_voltage", "_ch5_current", b"_ch5_current", "_ch5_voltage", b"_ch5_voltage", "_ch6_current", b"_ch6_current", "_ch6_voltage", b"_ch6_voltage", "_ch7_current", b"_ch7_current", "_ch7_voltage", b"_ch7_voltage", "_ch8_current", b"_ch8_current", "_ch8_voltage", b"_ch8_voltage", "ch1_current", b"ch1_current", "ch1_voltage", b"ch1_voltage", "ch2_current", b"ch2_current", "ch2_voltage", b"ch2_voltage", "ch3_current", b"ch3_current", "ch3_voltage", b"ch3_voltage", "ch4_current", b"ch4_current", "ch4_voltage", b"ch4_voltage", "ch5_current", b"ch5_current", "ch5_voltage", b"ch5_voltage", "ch6_current", b"ch6_current", "ch6_voltage", b"ch6_voltage", "ch7_current", b"ch7_current", "ch7_voltage", b"ch7_voltage", "ch8_current", b"ch8_current", "ch8_voltage", b"ch8_voltage"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch1_current", b"_ch1_current"]) -> typing.Literal["ch1_current"] | None: ...
|
||||
@typing.overload
|
||||
@@ -649,6 +847,26 @@ class PowerMetrics(google.protobuf.message.Message):
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch3_current", b"_ch3_current"]) -> typing.Literal["ch3_current"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch3_voltage", b"_ch3_voltage"]) -> typing.Literal["ch3_voltage"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch4_current", b"_ch4_current"]) -> typing.Literal["ch4_current"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch4_voltage", b"_ch4_voltage"]) -> typing.Literal["ch4_voltage"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch5_current", b"_ch5_current"]) -> typing.Literal["ch5_current"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch5_voltage", b"_ch5_voltage"]) -> typing.Literal["ch5_voltage"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch6_current", b"_ch6_current"]) -> typing.Literal["ch6_current"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch6_voltage", b"_ch6_voltage"]) -> typing.Literal["ch6_voltage"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch7_current", b"_ch7_current"]) -> typing.Literal["ch7_current"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch7_voltage", b"_ch7_voltage"]) -> typing.Literal["ch7_voltage"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch8_current", b"_ch8_current"]) -> typing.Literal["ch8_current"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_ch8_voltage", b"_ch8_voltage"]) -> typing.Literal["ch8_voltage"] | None: ...
|
||||
|
||||
global___PowerMetrics = PowerMetrics
|
||||
|
||||
@@ -673,57 +891,117 @@ class AirQualityMetrics(google.protobuf.message.Message):
|
||||
PARTICLES_50UM_FIELD_NUMBER: builtins.int
|
||||
PARTICLES_100UM_FIELD_NUMBER: builtins.int
|
||||
CO2_FIELD_NUMBER: builtins.int
|
||||
CO2_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
CO2_HUMIDITY_FIELD_NUMBER: builtins.int
|
||||
FORM_FORMALDEHYDE_FIELD_NUMBER: builtins.int
|
||||
FORM_HUMIDITY_FIELD_NUMBER: builtins.int
|
||||
FORM_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
PM40_STANDARD_FIELD_NUMBER: builtins.int
|
||||
PARTICLES_40UM_FIELD_NUMBER: builtins.int
|
||||
PM_TEMPERATURE_FIELD_NUMBER: builtins.int
|
||||
PM_HUMIDITY_FIELD_NUMBER: builtins.int
|
||||
PM_VOC_IDX_FIELD_NUMBER: builtins.int
|
||||
PM_NOX_IDX_FIELD_NUMBER: builtins.int
|
||||
PARTICLES_TPS_FIELD_NUMBER: builtins.int
|
||||
pm10_standard: builtins.int
|
||||
"""
|
||||
Concentration Units Standard PM1.0
|
||||
Concentration Units Standard PM1.0 in ug/m3
|
||||
"""
|
||||
pm25_standard: builtins.int
|
||||
"""
|
||||
Concentration Units Standard PM2.5
|
||||
Concentration Units Standard PM2.5 in ug/m3
|
||||
"""
|
||||
pm100_standard: builtins.int
|
||||
"""
|
||||
Concentration Units Standard PM10.0
|
||||
Concentration Units Standard PM10.0 in ug/m3
|
||||
"""
|
||||
pm10_environmental: builtins.int
|
||||
"""
|
||||
Concentration Units Environmental PM1.0
|
||||
Concentration Units Environmental PM1.0 in ug/m3
|
||||
"""
|
||||
pm25_environmental: builtins.int
|
||||
"""
|
||||
Concentration Units Environmental PM2.5
|
||||
Concentration Units Environmental PM2.5 in ug/m3
|
||||
"""
|
||||
pm100_environmental: builtins.int
|
||||
"""
|
||||
Concentration Units Environmental PM10.0
|
||||
Concentration Units Environmental PM10.0 in ug/m3
|
||||
"""
|
||||
particles_03um: builtins.int
|
||||
"""
|
||||
0.3um Particle Count
|
||||
0.3um Particle Count in #/0.1l
|
||||
"""
|
||||
particles_05um: builtins.int
|
||||
"""
|
||||
0.5um Particle Count
|
||||
0.5um Particle Count in #/0.1l
|
||||
"""
|
||||
particles_10um: builtins.int
|
||||
"""
|
||||
1.0um Particle Count
|
||||
1.0um Particle Count in #/0.1l
|
||||
"""
|
||||
particles_25um: builtins.int
|
||||
"""
|
||||
2.5um Particle Count
|
||||
2.5um Particle Count in #/0.1l
|
||||
"""
|
||||
particles_50um: builtins.int
|
||||
"""
|
||||
5.0um Particle Count
|
||||
5.0um Particle Count in #/0.1l
|
||||
"""
|
||||
particles_100um: builtins.int
|
||||
"""
|
||||
10.0um Particle Count
|
||||
10.0um Particle Count in #/0.1l
|
||||
"""
|
||||
co2: builtins.int
|
||||
"""
|
||||
10.0um Particle Count
|
||||
CO2 concentration in ppm
|
||||
"""
|
||||
co2_temperature: builtins.float
|
||||
"""
|
||||
CO2 sensor temperature in degC
|
||||
"""
|
||||
co2_humidity: builtins.float
|
||||
"""
|
||||
CO2 sensor relative humidity in %
|
||||
"""
|
||||
form_formaldehyde: builtins.float
|
||||
"""
|
||||
Formaldehyde sensor formaldehyde concentration in ppb
|
||||
"""
|
||||
form_humidity: builtins.float
|
||||
"""
|
||||
Formaldehyde sensor relative humidity in %RH
|
||||
"""
|
||||
form_temperature: builtins.float
|
||||
"""
|
||||
Formaldehyde sensor temperature in degrees Celsius
|
||||
"""
|
||||
pm40_standard: builtins.int
|
||||
"""
|
||||
Concentration Units Standard PM4.0 in ug/m3
|
||||
"""
|
||||
particles_40um: builtins.int
|
||||
"""
|
||||
4.0um Particle Count in #/0.1l
|
||||
"""
|
||||
pm_temperature: builtins.float
|
||||
"""
|
||||
PM Sensor Temperature
|
||||
"""
|
||||
pm_humidity: builtins.float
|
||||
"""
|
||||
PM Sensor humidity
|
||||
"""
|
||||
pm_voc_idx: builtins.float
|
||||
"""
|
||||
PM Sensor VOC Index
|
||||
"""
|
||||
pm_nox_idx: builtins.float
|
||||
"""
|
||||
PM Sensor NOx Index
|
||||
"""
|
||||
particles_tps: builtins.float
|
||||
"""
|
||||
Typical Particle Size in um
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
@@ -741,12 +1019,34 @@ class AirQualityMetrics(google.protobuf.message.Message):
|
||||
particles_50um: builtins.int | None = ...,
|
||||
particles_100um: builtins.int | None = ...,
|
||||
co2: builtins.int | None = ...,
|
||||
co2_temperature: builtins.float | None = ...,
|
||||
co2_humidity: builtins.float | None = ...,
|
||||
form_formaldehyde: builtins.float | None = ...,
|
||||
form_humidity: builtins.float | None = ...,
|
||||
form_temperature: builtins.float | None = ...,
|
||||
pm40_standard: builtins.int | None = ...,
|
||||
particles_40um: builtins.int | None = ...,
|
||||
pm_temperature: builtins.float | None = ...,
|
||||
pm_humidity: builtins.float | None = ...,
|
||||
pm_voc_idx: builtins.float | None = ...,
|
||||
pm_nox_idx: builtins.float | None = ...,
|
||||
particles_tps: builtins.float | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_co2", b"_co2", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_50um", b"_particles_50um", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "co2", b"co2", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_50um", b"particles_50um", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_co2", b"_co2", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_50um", b"_particles_50um", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "co2", b"co2", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_50um", b"particles_50um", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard"]) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_co2", b"_co2"]) -> typing.Literal["co2"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_co2_humidity", b"_co2_humidity"]) -> typing.Literal["co2_humidity"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_co2_temperature", b"_co2_temperature"]) -> typing.Literal["co2_temperature"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_form_formaldehyde", b"_form_formaldehyde"]) -> typing.Literal["form_formaldehyde"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_form_humidity", b"_form_humidity"]) -> typing.Literal["form_humidity"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_form_temperature", b"_form_temperature"]) -> typing.Literal["form_temperature"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_particles_03um", b"_particles_03um"]) -> typing.Literal["particles_03um"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_particles_05um", b"_particles_05um"]) -> typing.Literal["particles_05um"] | None: ...
|
||||
@@ -757,8 +1057,12 @@ class AirQualityMetrics(google.protobuf.message.Message):
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_particles_25um", b"_particles_25um"]) -> typing.Literal["particles_25um"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_particles_40um", b"_particles_40um"]) -> typing.Literal["particles_40um"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_particles_50um", b"_particles_50um"]) -> typing.Literal["particles_50um"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_particles_tps", b"_particles_tps"]) -> typing.Literal["particles_tps"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm100_environmental", b"_pm100_environmental"]) -> typing.Literal["pm100_environmental"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm100_standard", b"_pm100_standard"]) -> typing.Literal["pm100_standard"] | None: ...
|
||||
@@ -770,6 +1074,16 @@ class AirQualityMetrics(google.protobuf.message.Message):
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm25_environmental", b"_pm25_environmental"]) -> typing.Literal["pm25_environmental"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm25_standard", b"_pm25_standard"]) -> typing.Literal["pm25_standard"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm40_standard", b"_pm40_standard"]) -> typing.Literal["pm40_standard"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm_humidity", b"_pm_humidity"]) -> typing.Literal["pm_humidity"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm_nox_idx", b"_pm_nox_idx"]) -> typing.Literal["pm_nox_idx"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm_temperature", b"_pm_temperature"]) -> typing.Literal["pm_temperature"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_pm_voc_idx", b"_pm_voc_idx"]) -> typing.Literal["pm_voc_idx"] | None: ...
|
||||
|
||||
global___AirQualityMetrics = AirQualityMetrics
|
||||
|
||||
@@ -792,6 +1106,10 @@ class LocalStats(google.protobuf.message.Message):
|
||||
NUM_RX_DUPE_FIELD_NUMBER: builtins.int
|
||||
NUM_TX_RELAY_FIELD_NUMBER: builtins.int
|
||||
NUM_TX_RELAY_CANCELED_FIELD_NUMBER: builtins.int
|
||||
HEAP_TOTAL_BYTES_FIELD_NUMBER: builtins.int
|
||||
HEAP_FREE_BYTES_FIELD_NUMBER: builtins.int
|
||||
NUM_TX_DROPPED_FIELD_NUMBER: builtins.int
|
||||
NOISE_FLOOR_FIELD_NUMBER: builtins.int
|
||||
uptime_seconds: builtins.int
|
||||
"""
|
||||
How long the device has been running since the last reboot (in seconds)
|
||||
@@ -838,6 +1156,22 @@ class LocalStats(google.protobuf.message.Message):
|
||||
Number of times we canceled a packet to be relayed, because someone else did it before us.
|
||||
This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you.
|
||||
"""
|
||||
heap_total_bytes: builtins.int
|
||||
"""
|
||||
Number of bytes used in the heap
|
||||
"""
|
||||
heap_free_bytes: builtins.int
|
||||
"""
|
||||
Number of bytes free in the heap
|
||||
"""
|
||||
num_tx_dropped: builtins.int
|
||||
"""
|
||||
Number of packets that were dropped because the transmit queue was full.
|
||||
"""
|
||||
noise_floor: builtins.int
|
||||
"""
|
||||
Noise floor value measured in dBm
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -852,11 +1186,73 @@ class LocalStats(google.protobuf.message.Message):
|
||||
num_rx_dupe: builtins.int = ...,
|
||||
num_tx_relay: builtins.int = ...,
|
||||
num_tx_relay_canceled: builtins.int = ...,
|
||||
heap_total_bytes: builtins.int = ...,
|
||||
heap_free_bytes: builtins.int = ...,
|
||||
num_tx_dropped: builtins.int = ...,
|
||||
noise_floor: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["air_util_tx", b"air_util_tx", "channel_utilization", b"channel_utilization", "num_online_nodes", b"num_online_nodes", "num_packets_rx", b"num_packets_rx", "num_packets_rx_bad", b"num_packets_rx_bad", "num_packets_tx", b"num_packets_tx", "num_rx_dupe", b"num_rx_dupe", "num_total_nodes", b"num_total_nodes", "num_tx_relay", b"num_tx_relay", "num_tx_relay_canceled", b"num_tx_relay_canceled", "uptime_seconds", b"uptime_seconds"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["air_util_tx", b"air_util_tx", "channel_utilization", b"channel_utilization", "heap_free_bytes", b"heap_free_bytes", "heap_total_bytes", b"heap_total_bytes", "noise_floor", b"noise_floor", "num_online_nodes", b"num_online_nodes", "num_packets_rx", b"num_packets_rx", "num_packets_rx_bad", b"num_packets_rx_bad", "num_packets_tx", b"num_packets_tx", "num_rx_dupe", b"num_rx_dupe", "num_total_nodes", b"num_total_nodes", "num_tx_dropped", b"num_tx_dropped", "num_tx_relay", b"num_tx_relay", "num_tx_relay_canceled", b"num_tx_relay_canceled", "uptime_seconds", b"uptime_seconds"]) -> None: ...
|
||||
|
||||
global___LocalStats = LocalStats
|
||||
|
||||
@typing.final
|
||||
class TrafficManagementStats(google.protobuf.message.Message):
|
||||
"""
|
||||
Traffic management statistics for mesh network optimization
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
PACKETS_INSPECTED_FIELD_NUMBER: builtins.int
|
||||
POSITION_DEDUP_DROPS_FIELD_NUMBER: builtins.int
|
||||
NODEINFO_CACHE_HITS_FIELD_NUMBER: builtins.int
|
||||
RATE_LIMIT_DROPS_FIELD_NUMBER: builtins.int
|
||||
UNKNOWN_PACKET_DROPS_FIELD_NUMBER: builtins.int
|
||||
HOP_EXHAUSTED_PACKETS_FIELD_NUMBER: builtins.int
|
||||
ROUTER_HOPS_PRESERVED_FIELD_NUMBER: builtins.int
|
||||
packets_inspected: builtins.int
|
||||
"""
|
||||
Total number of packets inspected by traffic management
|
||||
"""
|
||||
position_dedup_drops: builtins.int
|
||||
"""
|
||||
Number of position packets dropped due to deduplication
|
||||
"""
|
||||
nodeinfo_cache_hits: builtins.int
|
||||
"""
|
||||
Number of NodeInfo requests answered from cache
|
||||
"""
|
||||
rate_limit_drops: builtins.int
|
||||
"""
|
||||
Number of packets dropped due to rate limiting
|
||||
"""
|
||||
unknown_packet_drops: builtins.int
|
||||
"""
|
||||
Number of unknown/undecryptable packets dropped
|
||||
"""
|
||||
hop_exhausted_packets: builtins.int
|
||||
"""
|
||||
Number of packets with hop_limit exhausted for local-only broadcast
|
||||
"""
|
||||
router_hops_preserved: builtins.int
|
||||
"""
|
||||
Number of times router hop preservation was applied
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
packets_inspected: builtins.int = ...,
|
||||
position_dedup_drops: builtins.int = ...,
|
||||
nodeinfo_cache_hits: builtins.int = ...,
|
||||
rate_limit_drops: builtins.int = ...,
|
||||
unknown_packet_drops: builtins.int = ...,
|
||||
hop_exhausted_packets: builtins.int = ...,
|
||||
router_hops_preserved: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["hop_exhausted_packets", b"hop_exhausted_packets", "nodeinfo_cache_hits", b"nodeinfo_cache_hits", "packets_inspected", b"packets_inspected", "position_dedup_drops", b"position_dedup_drops", "rate_limit_drops", b"rate_limit_drops", "router_hops_preserved", b"router_hops_preserved", "unknown_packet_drops", b"unknown_packet_drops"]) -> None: ...
|
||||
|
||||
global___TrafficManagementStats = TrafficManagementStats
|
||||
|
||||
@typing.final
|
||||
class HealthMetrics(google.protobuf.message.Message):
|
||||
"""
|
||||
@@ -898,6 +1294,84 @@ class HealthMetrics(google.protobuf.message.Message):
|
||||
|
||||
global___HealthMetrics = HealthMetrics
|
||||
|
||||
@typing.final
|
||||
class HostMetrics(google.protobuf.message.Message):
|
||||
"""
|
||||
Linux host metrics
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
UPTIME_SECONDS_FIELD_NUMBER: builtins.int
|
||||
FREEMEM_BYTES_FIELD_NUMBER: builtins.int
|
||||
DISKFREE1_BYTES_FIELD_NUMBER: builtins.int
|
||||
DISKFREE2_BYTES_FIELD_NUMBER: builtins.int
|
||||
DISKFREE3_BYTES_FIELD_NUMBER: builtins.int
|
||||
LOAD1_FIELD_NUMBER: builtins.int
|
||||
LOAD5_FIELD_NUMBER: builtins.int
|
||||
LOAD15_FIELD_NUMBER: builtins.int
|
||||
USER_STRING_FIELD_NUMBER: builtins.int
|
||||
uptime_seconds: builtins.int
|
||||
"""
|
||||
Host system uptime
|
||||
"""
|
||||
freemem_bytes: builtins.int
|
||||
"""
|
||||
Host system free memory
|
||||
"""
|
||||
diskfree1_bytes: builtins.int
|
||||
"""
|
||||
Host system disk space free for /
|
||||
"""
|
||||
diskfree2_bytes: builtins.int
|
||||
"""
|
||||
Secondary system disk space free
|
||||
"""
|
||||
diskfree3_bytes: builtins.int
|
||||
"""
|
||||
Tertiary disk space free
|
||||
"""
|
||||
load1: builtins.int
|
||||
"""
|
||||
Host system one minute load in 1/100ths
|
||||
"""
|
||||
load5: builtins.int
|
||||
"""
|
||||
Host system five minute load in 1/100ths
|
||||
"""
|
||||
load15: builtins.int
|
||||
"""
|
||||
Host system fifteen minute load in 1/100ths
|
||||
"""
|
||||
user_string: builtins.str
|
||||
"""
|
||||
Optional User-provided string for arbitrary host system information
|
||||
that doesn't make sense as a dedicated entry.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
uptime_seconds: builtins.int = ...,
|
||||
freemem_bytes: builtins.int = ...,
|
||||
diskfree1_bytes: builtins.int = ...,
|
||||
diskfree2_bytes: builtins.int | None = ...,
|
||||
diskfree3_bytes: builtins.int | None = ...,
|
||||
load1: builtins.int = ...,
|
||||
load5: builtins.int = ...,
|
||||
load15: builtins.int = ...,
|
||||
user_string: builtins.str | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_diskfree2_bytes", b"_diskfree2_bytes", "_diskfree3_bytes", b"_diskfree3_bytes", "_user_string", b"_user_string", "diskfree2_bytes", b"diskfree2_bytes", "diskfree3_bytes", b"diskfree3_bytes", "user_string", b"user_string"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_diskfree2_bytes", b"_diskfree2_bytes", "_diskfree3_bytes", b"_diskfree3_bytes", "_user_string", b"_user_string", "diskfree1_bytes", b"diskfree1_bytes", "diskfree2_bytes", b"diskfree2_bytes", "diskfree3_bytes", b"diskfree3_bytes", "freemem_bytes", b"freemem_bytes", "load1", b"load1", "load15", b"load15", "load5", b"load5", "uptime_seconds", b"uptime_seconds", "user_string", b"user_string"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_diskfree2_bytes", b"_diskfree2_bytes"]) -> typing.Literal["diskfree2_bytes"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_diskfree3_bytes", b"_diskfree3_bytes"]) -> typing.Literal["diskfree3_bytes"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_user_string", b"_user_string"]) -> typing.Literal["user_string"] | None: ...
|
||||
|
||||
global___HostMetrics = HostMetrics
|
||||
|
||||
@typing.final
|
||||
class Telemetry(google.protobuf.message.Message):
|
||||
"""
|
||||
@@ -913,6 +1387,8 @@ class Telemetry(google.protobuf.message.Message):
|
||||
POWER_METRICS_FIELD_NUMBER: builtins.int
|
||||
LOCAL_STATS_FIELD_NUMBER: builtins.int
|
||||
HEALTH_METRICS_FIELD_NUMBER: builtins.int
|
||||
HOST_METRICS_FIELD_NUMBER: builtins.int
|
||||
TRAFFIC_MANAGEMENT_STATS_FIELD_NUMBER: builtins.int
|
||||
time: builtins.int
|
||||
"""
|
||||
Seconds since 1970 - or 0 for unknown/unset
|
||||
@@ -953,6 +1429,18 @@ class Telemetry(google.protobuf.message.Message):
|
||||
Health telemetry metrics
|
||||
"""
|
||||
|
||||
@property
|
||||
def host_metrics(self) -> global___HostMetrics:
|
||||
"""
|
||||
Linux host metrics
|
||||
"""
|
||||
|
||||
@property
|
||||
def traffic_management_stats(self) -> global___TrafficManagementStats:
|
||||
"""
|
||||
Traffic management statistics
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -963,10 +1451,12 @@ class Telemetry(google.protobuf.message.Message):
|
||||
power_metrics: global___PowerMetrics | None = ...,
|
||||
local_stats: global___LocalStats | None = ...,
|
||||
health_metrics: global___HealthMetrics | None = ...,
|
||||
host_metrics: global___HostMetrics | None = ...,
|
||||
traffic_management_stats: global___TrafficManagementStats | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "variant", b"variant"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "time", b"time", "variant", b"variant"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["variant", b"variant"]) -> typing.Literal["device_metrics", "environment_metrics", "air_quality_metrics", "power_metrics", "local_stats", "health_metrics"] | None: ...
|
||||
def HasField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "host_metrics", b"host_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "traffic_management_stats", b"traffic_management_stats", "variant", b"variant"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["air_quality_metrics", b"air_quality_metrics", "device_metrics", b"device_metrics", "environment_metrics", b"environment_metrics", "health_metrics", b"health_metrics", "host_metrics", b"host_metrics", "local_stats", b"local_stats", "power_metrics", b"power_metrics", "time", b"time", "traffic_management_stats", b"traffic_management_stats", "variant", b"variant"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["variant", b"variant"]) -> typing.Literal["device_metrics", "environment_metrics", "air_quality_metrics", "power_metrics", "local_stats", "health_metrics", "host_metrics", "traffic_management_stats"] | None: ...
|
||||
|
||||
global___Telemetry = Telemetry
|
||||
|
||||
@@ -997,3 +1487,62 @@ class Nau7802Config(google.protobuf.message.Message):
|
||||
def ClearField(self, field_name: typing.Literal["calibrationFactor", b"calibrationFactor", "zeroOffset", b"zeroOffset"]) -> None: ...
|
||||
|
||||
global___Nau7802Config = Nau7802Config
|
||||
|
||||
@typing.final
|
||||
class SEN5XState(google.protobuf.message.Message):
|
||||
"""
|
||||
SEN5X State, for saving to flash
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
LAST_CLEANING_TIME_FIELD_NUMBER: builtins.int
|
||||
LAST_CLEANING_VALID_FIELD_NUMBER: builtins.int
|
||||
ONE_SHOT_MODE_FIELD_NUMBER: builtins.int
|
||||
VOC_STATE_TIME_FIELD_NUMBER: builtins.int
|
||||
VOC_STATE_VALID_FIELD_NUMBER: builtins.int
|
||||
VOC_STATE_ARRAY_FIELD_NUMBER: builtins.int
|
||||
last_cleaning_time: builtins.int
|
||||
"""
|
||||
Last cleaning time for SEN5X
|
||||
"""
|
||||
last_cleaning_valid: builtins.bool
|
||||
"""
|
||||
Last cleaning time for SEN5X - valid flag
|
||||
"""
|
||||
one_shot_mode: builtins.bool
|
||||
"""
|
||||
Config flag for one-shot mode (see admin.proto)
|
||||
"""
|
||||
voc_state_time: builtins.int
|
||||
"""
|
||||
Last VOC state time for SEN55
|
||||
"""
|
||||
voc_state_valid: builtins.bool
|
||||
"""
|
||||
Last VOC state validity flag for SEN55
|
||||
"""
|
||||
voc_state_array: builtins.int
|
||||
"""
|
||||
VOC state array (8x uint8t) for SEN55
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
last_cleaning_time: builtins.int = ...,
|
||||
last_cleaning_valid: builtins.bool = ...,
|
||||
one_shot_mode: builtins.bool = ...,
|
||||
voc_state_time: builtins.int | None = ...,
|
||||
voc_state_valid: builtins.bool | None = ...,
|
||||
voc_state_array: builtins.int | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["_voc_state_array", b"_voc_state_array", "_voc_state_time", b"_voc_state_time", "_voc_state_valid", b"_voc_state_valid", "voc_state_array", b"voc_state_array", "voc_state_time", b"voc_state_time", "voc_state_valid", b"voc_state_valid"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["_voc_state_array", b"_voc_state_array", "_voc_state_time", b"_voc_state_time", "_voc_state_valid", b"_voc_state_valid", "last_cleaning_time", b"last_cleaning_time", "last_cleaning_valid", b"last_cleaning_valid", "one_shot_mode", b"one_shot_mode", "voc_state_array", b"voc_state_array", "voc_state_time", b"voc_state_time", "voc_state_valid", b"voc_state_valid"]) -> None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_voc_state_array", b"_voc_state_array"]) -> typing.Literal["voc_state_array"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_voc_state_time", b"_voc_state_time"]) -> typing.Literal["voc_state_time"] | None: ...
|
||||
@typing.overload
|
||||
def WhichOneof(self, oneof_group: typing.Literal["_voc_state_valid", b"_voc_state_valid"]) -> typing.Literal["voc_state_valid"] | None: ...
|
||||
|
||||
global___SEN5XState = SEN5XState
|
||||
|
||||
19
meshtastic/protobuf/xmodem_pb2.py
generated
19
meshtastic/protobuf/xmodem_pb2.py
generated
@@ -11,18 +11,25 @@ from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n meshtastic/protobuf/xmodem.proto\x12\x13meshtastic.protobuf\"\xbf\x01\n\x06XModem\x12\x34\n\x07\x63ontrol\x18\x01 \x01(\x0e\x32#.meshtastic.protobuf.XModem.Control\x12\x0b\n\x03seq\x18\x02 \x01(\r\x12\r\n\x05\x63rc16\x18\x03 \x01(\r\x12\x0e\n\x06\x62uffer\x18\x04 \x01(\x0c\"S\n\x07\x43ontrol\x12\x07\n\x03NUL\x10\x00\x12\x07\n\x03SOH\x10\x01\x12\x07\n\x03STX\x10\x02\x12\x07\n\x03\x45OT\x10\x04\x12\x07\n\x03\x41\x43K\x10\x06\x12\x07\n\x03NAK\x10\x15\x12\x07\n\x03\x43\x41N\x10\x18\x12\t\n\x05\x43TRLZ\x10\x1a\x42\x61\n\x13\x63om.geeksville.meshB\x0cXmodemProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n meshtastic/protobuf/xmodem.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xd5\x01\n\x06XModem\x12\x34\n\x07\x63ontrol\x18\x01 \x01(\x0e\x32#.meshtastic.protobuf.XModem.Control\x12\x12\n\x03seq\x18\x02 \x01(\rB\x05\x92?\x02\x38\x10\x12\x14\n\x05\x63rc16\x18\x03 \x01(\rB\x05\x92?\x02\x38\x10\x12\x16\n\x06\x62uffer\x18\x04 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x01\"S\n\x07\x43ontrol\x12\x07\n\x03NUL\x10\x00\x12\x07\n\x03SOH\x10\x01\x12\x07\n\x03STX\x10\x02\x12\x07\n\x03\x45OT\x10\x04\x12\x07\n\x03\x41\x43K\x10\x06\x12\x07\n\x03NAK\x10\x15\x12\x07\n\x03\x43\x41N\x10\x18\x12\t\n\x05\x43TRLZ\x10\x1a\x42\x62\n\x14org.meshtastic.protoB\x0cXmodemProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.xmodem_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\014XmodemProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_XMODEM']._serialized_start=58
|
||||
_globals['_XMODEM']._serialized_end=249
|
||||
_globals['_XMODEM_CONTROL']._serialized_start=166
|
||||
_globals['_XMODEM_CONTROL']._serialized_end=249
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\014XmodemProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_XMODEM.fields_by_name['seq']._options = None
|
||||
_XMODEM.fields_by_name['seq']._serialized_options = b'\222?\0028\020'
|
||||
_XMODEM.fields_by_name['crc16']._options = None
|
||||
_XMODEM.fields_by_name['crc16']._serialized_options = b'\222?\0028\020'
|
||||
_XMODEM.fields_by_name['buffer']._options = None
|
||||
_XMODEM.fields_by_name['buffer']._serialized_options = b'\222?\003\010\200\001'
|
||||
_globals['_XMODEM']._serialized_start=92
|
||||
_globals['_XMODEM']._serialized_end=305
|
||||
_globals['_XMODEM_CONTROL']._serialized_start=222
|
||||
_globals['_XMODEM_CONTROL']._serialized_end=305
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
@@ -7,10 +7,11 @@ from pubsub import pub # type: ignore[import-untyped]
|
||||
from meshtastic.protobuf import portnums_pb2, remote_hardware_pb2
|
||||
from meshtastic.util import our_exit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def onGPIOreceive(packet, interface) -> None:
|
||||
"""Callback for received GPIO responses"""
|
||||
logging.debug(f"packet:{packet} interface:{interface}")
|
||||
logger.debug(f"packet:{packet} interface:{interface}")
|
||||
gpioValue = 0
|
||||
hw = packet["decoded"]["remotehw"]
|
||||
if "gpioValue" in hw:
|
||||
@@ -76,7 +77,7 @@ class RemoteHardwareClient:
|
||||
Write the specified vals bits to the device GPIOs. Only bits in mask that
|
||||
are 1 will be changed
|
||||
"""
|
||||
logging.debug(f"writeGPIOs nodeid:{nodeid} mask:{mask} vals:{vals}")
|
||||
logger.debug(f"writeGPIOs nodeid:{nodeid} mask:{mask} vals:{vals}")
|
||||
r = remote_hardware_pb2.HardwareMessage()
|
||||
r.type = remote_hardware_pb2.HardwareMessage.Type.WRITE_GPIOS
|
||||
r.gpio_mask = mask
|
||||
@@ -85,7 +86,7 @@ class RemoteHardwareClient:
|
||||
|
||||
def readGPIOs(self, nodeid, mask, onResponse=None):
|
||||
"""Read the specified bits from GPIO inputs on the device"""
|
||||
logging.debug(f"readGPIOs nodeid:{nodeid} mask:{mask}")
|
||||
logger.debug(f"readGPIOs nodeid:{nodeid} mask:{mask}")
|
||||
r = remote_hardware_pb2.HardwareMessage()
|
||||
r.type = remote_hardware_pb2.HardwareMessage.Type.READ_GPIOS
|
||||
r.gpio_mask = mask
|
||||
@@ -93,7 +94,7 @@ class RemoteHardwareClient:
|
||||
|
||||
def watchGPIOs(self, nodeid, mask):
|
||||
"""Watch the specified bits from GPIO inputs on the device for changes"""
|
||||
logging.debug(f"watchGPIOs nodeid:{nodeid} mask:{mask}")
|
||||
logger.debug(f"watchGPIOs nodeid:{nodeid} mask:{mask}")
|
||||
r = remote_hardware_pb2.HardwareMessage()
|
||||
r.type = remote_hardware_pb2.HardwareMessage.Type.WATCH_GPIOS
|
||||
r.gpio_mask = mask
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"""
|
||||
# pylint: disable=R0917
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
from io import TextIOWrapper
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -12,28 +13,33 @@ import serial # type: ignore[import-untyped]
|
||||
import meshtastic.util
|
||||
from meshtastic.stream_interface import StreamInterface
|
||||
|
||||
if platform.system() != "Windows":
|
||||
import termios
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SerialInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a serial link"""
|
||||
|
||||
def __init__(self, devPath: Optional[str]=None, debugOut=None, noProto: bool=False, connectNow: bool=True, noNodes: bool=False) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
devPath: Optional[str] = None,
|
||||
debugOut=None,
|
||||
noProto: bool = False,
|
||||
connectNow: bool = True,
|
||||
noNodes: bool = False,
|
||||
timeout: int = 300
|
||||
) -> None:
|
||||
"""Constructor, opens a connection to a specified serial port, or if unspecified try to
|
||||
find one Meshtastic device by probing
|
||||
|
||||
Keyword Arguments:
|
||||
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})
|
||||
timeout -- How long to wait for replies (default: 300 seconds)
|
||||
"""
|
||||
self.noProto = noProto
|
||||
|
||||
self.devPath: Optional[str] = devPath
|
||||
|
||||
if self.devPath is None:
|
||||
ports: List[str] = meshtastic.util.findPorts(True)
|
||||
logging.debug(f"ports:{ports}")
|
||||
logger.debug(f"ports:{ports}")
|
||||
if len(ports) == 0:
|
||||
print("No Serial Meshtastic device detected, attempting TCP connection on localhost.")
|
||||
return
|
||||
@@ -44,27 +50,51 @@ class SerialInterface(StreamInterface):
|
||||
else:
|
||||
self.devPath = ports[0]
|
||||
|
||||
logging.debug(f"Connecting to {self.devPath}")
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes, timeout=timeout
|
||||
)
|
||||
|
||||
# first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR
|
||||
# see https://github.com/pyserial/pyserial/issues/124
|
||||
if platform.system() != "Windows":
|
||||
with open(self.devPath, encoding="utf8") as f:
|
||||
attrs = termios.tcgetattr(f)
|
||||
attrs[2] = attrs[2] & ~termios.HUPCL
|
||||
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
|
||||
f.close()
|
||||
def connect(self) -> None:
|
||||
logger.debug(f"Connecting to {self.devPath}")
|
||||
dev_path = self.devPath
|
||||
if dev_path is None:
|
||||
raise RuntimeError("Serial device path is not set")
|
||||
|
||||
if sys.platform != "win32":
|
||||
with open(dev_path, encoding="utf8") as f:
|
||||
self._set_hupcl_with_termios(f)
|
||||
time.sleep(0.1)
|
||||
|
||||
self.stream = serial.Serial(
|
||||
self.devPath, 115200, exclusive=True, timeout=0.5, write_timeout=0
|
||||
dev_path, 115200, exclusive=True, timeout=0.5, write_timeout=0
|
||||
)
|
||||
self.stream.flush() # type: ignore[attr-defined]
|
||||
time.sleep(0.1)
|
||||
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes
|
||||
)
|
||||
super().connect()
|
||||
|
||||
def _set_hupcl_with_termios(self, f: TextIOWrapper):
|
||||
"""first we need to set the HUPCL so the device will not reboot based on RTS and/or DTR
|
||||
see https://github.com/pyserial/pyserial/issues/124
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
return
|
||||
|
||||
import termios # pylint: disable=C0415,E0401
|
||||
attrs = termios.tcgetattr(f)
|
||||
attrs[2] = attrs[2] & ~termios.HUPCL
|
||||
termios.tcsetattr(f, termios.TCSAFLUSH, attrs)
|
||||
|
||||
def __repr__(self):
|
||||
rep = f"SerialInterface(devPath={self.devPath!r}"
|
||||
if hasattr(self, 'debugOut') and self.debugOut is not None:
|
||||
rep += f", debugOut={self.debugOut!r}"
|
||||
if self.noProto:
|
||||
rep += ", noProto=True"
|
||||
if hasattr(self, 'noNodes') and self.noNodes:
|
||||
rep += ", noNodes=True"
|
||||
rep += ")"
|
||||
return rep
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close a connection to the device"""
|
||||
@@ -73,5 +103,5 @@ class SerialInterface(StreamInterface):
|
||||
time.sleep(0.1)
|
||||
self.stream.flush()
|
||||
time.sleep(0.1)
|
||||
logging.debug("Closing Serial stream")
|
||||
logger.debug("Closing Serial stream")
|
||||
StreamInterface.close(self)
|
||||
|
||||
@@ -10,6 +10,7 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
import parse # type: ignore[import-untyped]
|
||||
@@ -22,6 +23,7 @@ from meshtastic.powermon import PowerMeter
|
||||
|
||||
from .arrow import FeatherWriter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def root_dir() -> str:
|
||||
"""Return the root directory for slog files."""
|
||||
@@ -29,9 +31,9 @@ def root_dir() -> str:
|
||||
app_name = "meshtastic"
|
||||
app_author = "meshtastic"
|
||||
app_dir = platformdirs.user_data_dir(app_name, app_author)
|
||||
dir_name = f"{app_dir}/slogs"
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
return dir_name
|
||||
dir_name = Path(app_dir, "slogs")
|
||||
dir_name.mkdir(exist_ok=True, parents=True)
|
||||
return str(dir_name)
|
||||
|
||||
|
||||
@dataclass(init=False)
|
||||
@@ -144,7 +146,7 @@ class StructuredLogger:
|
||||
self.power_logger = power_logger
|
||||
|
||||
# Setup the arrow writer (and its schema)
|
||||
self.writer = FeatherWriter(f"{dir_path}/slog")
|
||||
self.writer = FeatherWriter(os.path.join(dir_path, "slog"))
|
||||
all_fields = reduce(
|
||||
(lambda x, y: x + y), map(lambda x: x.fields, log_defs.values())
|
||||
)
|
||||
@@ -164,7 +166,7 @@ class StructuredLogger:
|
||||
self.raw_file: Optional[
|
||||
io.TextIOWrapper
|
||||
] = open( # pylint: disable=consider-using-with
|
||||
f"{dir_path}/raw.txt", "w", encoding="utf8"
|
||||
os.path.join(dir_path, "raw.txt"), "w", encoding="utf8"
|
||||
)
|
||||
|
||||
# We need a closure here because the subscription API is very strict about exact arg matching
|
||||
@@ -197,7 +199,7 @@ class StructuredLogger:
|
||||
if m:
|
||||
src = m.group(1)
|
||||
args = m.group(2)
|
||||
logging.debug(f"SLog {src}, args: {args}")
|
||||
logger.debug(f"SLog {src}, args: {args}")
|
||||
|
||||
d = log_defs.get(src)
|
||||
if d:
|
||||
@@ -219,9 +221,9 @@ class StructuredLogger:
|
||||
# If the last field is an empty string, remove it
|
||||
del di[last_field[0]]
|
||||
else:
|
||||
logging.warning(f"Failed to parse slog {line} with {d.format}")
|
||||
logger.warning(f"Failed to parse slog {line} with {d.format}")
|
||||
else:
|
||||
logging.warning(f"Unknown Structured Log: {line}")
|
||||
logger.warning(f"Unknown Structured Log: {line}")
|
||||
|
||||
# Store our structured log record
|
||||
if di or self.include_raw:
|
||||
@@ -256,23 +258,28 @@ class LogSet:
|
||||
|
||||
if not dir_name:
|
||||
app_dir = root_dir()
|
||||
dir_name = f"{app_dir}/{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
app_time_dir = Path(app_dir, datetime.now().strftime('%Y%m%d-%H%M%S'))
|
||||
app_time_dir.mkdir(exist_ok=True)
|
||||
dir_name = str(app_time_dir)
|
||||
|
||||
# Also make a 'latest' directory that always points to the most recent logs
|
||||
latest_dir = Path(app_dir, "latest")
|
||||
latest_dir.unlink(missing_ok=True)
|
||||
|
||||
# symlink might fail on some platforms, if it does fail silently
|
||||
if os.path.exists(f"{app_dir}/latest"):
|
||||
os.unlink(f"{app_dir}/latest")
|
||||
os.symlink(dir_name, f"{app_dir}/latest", target_is_directory=True)
|
||||
try:
|
||||
latest_dir.symlink_to(dir_name, target_is_directory=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.dir_name = dir_name
|
||||
|
||||
logging.info(f"Writing slogs to {dir_name}")
|
||||
logger.info(f"Writing slogs to {dir_name}")
|
||||
|
||||
self.power_logger: Optional[PowerLogger] = (
|
||||
None
|
||||
if not power_meter
|
||||
else PowerLogger(power_meter, f"{self.dir_name}/power")
|
||||
else PowerLogger(power_meter, os.path.join(self.dir_name, "power"))
|
||||
)
|
||||
|
||||
self.slog_logger: Optional[StructuredLogger] = StructuredLogger(
|
||||
@@ -286,7 +293,7 @@ class LogSet:
|
||||
"""Close the log set."""
|
||||
|
||||
if self.slog_logger:
|
||||
logging.info(f"Closing slogs in {self.dir_name}")
|
||||
logger.info(f"Closing slogs in {self.dir_name}")
|
||||
atexit.unregister(
|
||||
self.atexit_handler
|
||||
) # docs say it will silently ignore if not found
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Stream Interface base class
|
||||
"""
|
||||
import contextlib
|
||||
import io
|
||||
import logging
|
||||
import threading
|
||||
@@ -17,28 +18,36 @@ START1 = 0x94
|
||||
START2 = 0xC3
|
||||
HEADER_LEN = 4
|
||||
MAX_TO_FROM_RADIO_SIZE = 512
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StreamInterface(MeshInterface):
|
||||
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
|
||||
|
||||
def __init__(self, debugOut: Optional[io.TextIOWrapper]=None, noProto: bool=False, connectNow: bool=True, noNodes: bool=False) -> None:
|
||||
def __init__( # pylint: disable=R0917
|
||||
self,
|
||||
debugOut: Optional[io.TextIOWrapper] = None,
|
||||
noProto: bool = False,
|
||||
connectNow: bool = True,
|
||||
noNodes: bool = False,
|
||||
timeout: int = 300
|
||||
) -> None:
|
||||
"""Constructor, opens a connection to self.stream
|
||||
|
||||
Keyword Arguments:
|
||||
debugOut {stream} -- If a stream is provided, any debug serial output from the
|
||||
device will be emitted to that stream. (default: {None})
|
||||
timeout -- How long to wait for replies (default: 300 seconds)
|
||||
|
||||
Raises:
|
||||
Exception: [description]
|
||||
Exception: [description]
|
||||
RuntimeError: Raised if StreamInterface is instantiated when noProto is false.
|
||||
"""
|
||||
|
||||
if not hasattr(self, "stream") and not noProto:
|
||||
raise Exception( # pylint: disable=W0719
|
||||
if not noProto and type(self) == StreamInterface: # pylint: disable=C0123
|
||||
raise RuntimeError(
|
||||
"StreamInterface is now abstract (to update existing code create SerialInterface instead)"
|
||||
)
|
||||
self.stream: Optional[serial.Serial] # only serial uses this, TCPInterface overrides the relevant methods instead
|
||||
self.stream: Optional[serial.Serial] = None # only serial uses this, TCPInterface overrides the relevant methods instead
|
||||
self._rxBuf = bytes() # empty
|
||||
self._wantExit = False
|
||||
|
||||
@@ -48,13 +57,21 @@ class StreamInterface(MeshInterface):
|
||||
# FIXME, figure out why daemon=True causes reader thread to exit too early
|
||||
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True, name="stream reader")
|
||||
|
||||
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes)
|
||||
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes, timeout=timeout)
|
||||
|
||||
# Start the reader thread after superclass constructor completes init
|
||||
if connectNow:
|
||||
self.connect()
|
||||
if not noProto:
|
||||
self.waitForConfig()
|
||||
try:
|
||||
self.connect()
|
||||
if not noProto:
|
||||
self.waitForConfig()
|
||||
except Exception:
|
||||
# If the handshake raises, the caller never receives a reference
|
||||
# to this instance and cannot call close() themselves. Clean up
|
||||
# the reader thread + stream here so retries don't leak.
|
||||
with contextlib.suppress(Exception):
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Connect to our radio
|
||||
@@ -82,7 +99,7 @@ class StreamInterface(MeshInterface):
|
||||
"""We override the superclass implementation to close our port"""
|
||||
MeshInterface._disconnected(self)
|
||||
|
||||
logging.debug("Closing our port")
|
||||
logger.debug("Closing our port")
|
||||
# pylint: disable=E0203
|
||||
if not self.stream is None:
|
||||
# pylint: disable=E0203
|
||||
@@ -111,23 +128,32 @@ class StreamInterface(MeshInterface):
|
||||
|
||||
def _sendToRadioImpl(self, toRadio) -> None:
|
||||
"""Send a ToRadio protobuf to the device"""
|
||||
logging.debug(f"Sending: {stripnl(toRadio)}")
|
||||
logger.debug(f"Sending: {stripnl(toRadio)}")
|
||||
b: bytes = toRadio.SerializeToString()
|
||||
bufLen: int = len(b)
|
||||
# We convert into a string, because the TCP code doesn't work with byte arrays
|
||||
header: bytes = bytes([START1, START2, (bufLen >> 8) & 0xFF, bufLen & 0xFF])
|
||||
logging.debug(f"sending header:{header!r} b:{b!r}")
|
||||
logger.debug(f"sending header:{header!r} b:{b!r}")
|
||||
self._writeBytes(header + b)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing stream")
|
||||
logger.debug("Closing stream")
|
||||
MeshInterface.close(self)
|
||||
# pyserial cancel_read doesn't seem to work, therefore we ask the
|
||||
# reader thread to close things for us
|
||||
self._wantExit = True
|
||||
if self._rxThread != threading.current_thread():
|
||||
self._rxThread.join() # wait for it to exit
|
||||
try:
|
||||
self._rxThread.join() # wait for it to exit
|
||||
except RuntimeError:
|
||||
# Thread was never started — happens when close() is invoked
|
||||
# from a failed __init__ before connect() could spawn it.
|
||||
# In this case there is no reader thread to close the stream.
|
||||
if self.stream is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
self.stream.close()
|
||||
self.stream = None
|
||||
|
||||
def _handleLogByte(self, b):
|
||||
"""Handle a byte that is part of a log message from the device."""
|
||||
@@ -148,18 +174,18 @@ class StreamInterface(MeshInterface):
|
||||
|
||||
def __reader(self) -> None:
|
||||
"""The reader thread that reads bytes from our stream"""
|
||||
logging.debug("in __reader()")
|
||||
logger.debug("in __reader()")
|
||||
empty = bytes()
|
||||
|
||||
try:
|
||||
while not self._wantExit:
|
||||
# logging.debug("reading character")
|
||||
# logger.debug("reading character")
|
||||
b: Optional[bytes] = self._readBytes(1)
|
||||
# logging.debug("In reader loop")
|
||||
# logging.debug(f"read returned {b}")
|
||||
# logger.debug("In reader loop")
|
||||
# logger.debug(f"read returned {b}")
|
||||
if b is not None and len(cast(bytes, b)) > 0:
|
||||
c: int = b[0]
|
||||
# logging.debug(f'c:{c}')
|
||||
# logger.debug(f'c:{c}')
|
||||
ptr: int = len(self._rxBuf)
|
||||
|
||||
# Assume we want to append this byte, fixme use bytearray instead
|
||||
@@ -176,7 +202,7 @@ class StreamInterface(MeshInterface):
|
||||
if c != START2:
|
||||
self._rxBuf = empty # failed to find start2
|
||||
elif ptr >= HEADER_LEN - 1: # we've at least got a header
|
||||
# logging.debug('at least we received a header')
|
||||
# logger.debug('at least we received a header')
|
||||
# big endian length follows header
|
||||
packetlen = (self._rxBuf[2] << 8) + self._rxBuf[3]
|
||||
|
||||
@@ -192,32 +218,32 @@ class StreamInterface(MeshInterface):
|
||||
try:
|
||||
self._handleFromRadio(self._rxBuf[HEADER_LEN:])
|
||||
except Exception as ex:
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"Error while handling message from radio {ex}"
|
||||
)
|
||||
traceback.print_exc()
|
||||
self._rxBuf = empty
|
||||
else:
|
||||
# logging.debug(f"timeout")
|
||||
# logger.debug(f"timeout")
|
||||
pass
|
||||
except serial.SerialException as ex:
|
||||
if (
|
||||
not self._wantExit
|
||||
): # We might intentionally get an exception during shutdown
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f"Meshtastic serial port disconnected, disconnecting... {ex}"
|
||||
)
|
||||
except OSError as ex:
|
||||
if (
|
||||
not self._wantExit
|
||||
): # We might intentionally get an exception during shutdown
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"Unexpected OSError, terminating meshtastic reader... {ex}"
|
||||
)
|
||||
except Exception as ex:
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"Unexpected exception, terminating meshtastic reader... {ex}"
|
||||
)
|
||||
finally:
|
||||
logging.debug("reader is exiting")
|
||||
logger.debug("reader is exiting")
|
||||
self._disconnected()
|
||||
|
||||
@@ -207,6 +207,30 @@ nano_g1 = SupportedDevice(
|
||||
usb_product_id_in_hex="55d4",
|
||||
)
|
||||
|
||||
seeed_xiao_s3 = SupportedDevice(
|
||||
name = "Seeed Xiao ESP32-S3",
|
||||
version = "",
|
||||
for_firmware="seeed-xiao-esp32s3",
|
||||
baseport_on_linux="ttyACM",
|
||||
baseport_on_mac="cu.usbmodem",
|
||||
usb_vendor_id_in_hex="2886",
|
||||
usb_product_id_in_hex="0059",
|
||||
)
|
||||
|
||||
tdeck = SupportedDevice(
|
||||
name="T-Deck",
|
||||
version="",
|
||||
for_firmware="t-deck", # Confirmed firmware identifier
|
||||
device_class="esp32",
|
||||
baseport_on_linux="ttyACM",
|
||||
baseport_on_mac="cu.usbmodem",
|
||||
baseport_on_windows="COM",
|
||||
usb_vendor_id_in_hex="303a", # Espressif Systems (VERIFIED)
|
||||
usb_product_id_in_hex="1001", # VERIFIED from actual device
|
||||
)
|
||||
|
||||
|
||||
|
||||
supported_devices = [
|
||||
tbeam_v0_7,
|
||||
tbeam_v1_1,
|
||||
@@ -226,4 +250,6 @@ supported_devices = [
|
||||
rak4631_19003,
|
||||
rak11200,
|
||||
nano_g1,
|
||||
seeed_xiao_s3,
|
||||
tdeck, # T-Deck support added
|
||||
]
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from meshtastic.stream_interface import StreamInterface
|
||||
|
||||
DEFAULT_TCP_PORT = 4403
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TCPInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a TCP link"""
|
||||
@@ -18,30 +21,46 @@ class TCPInterface(StreamInterface):
|
||||
self,
|
||||
hostname: str,
|
||||
debugOut=None,
|
||||
noProto: bool=False,
|
||||
connectNow: bool=True,
|
||||
portNumber: int=DEFAULT_TCP_PORT,
|
||||
noNodes:bool=False,
|
||||
noProto: bool = False,
|
||||
connectNow: bool = True,
|
||||
portNumber: int = DEFAULT_TCP_PORT,
|
||||
noNodes: bool = False,
|
||||
timeout: int = 300,
|
||||
):
|
||||
"""Constructor, opens a connection to a specified IP address/hostname
|
||||
|
||||
Keyword Arguments:
|
||||
hostname {string} -- Hostname/IP address of the device to connect to
|
||||
timeout -- How long to wait for replies (default: 300 seconds)
|
||||
"""
|
||||
|
||||
self.stream = None
|
||||
|
||||
self.hostname: str = hostname
|
||||
self.portNumber: int = portNumber
|
||||
|
||||
self.socket: Optional[socket.socket] = None
|
||||
self.reconnectLock = threading.Lock()
|
||||
|
||||
if connectNow:
|
||||
self.myConnect()
|
||||
else:
|
||||
self.socket = None
|
||||
super().__init__(
|
||||
debugOut=debugOut,
|
||||
noProto=noProto,
|
||||
connectNow=connectNow,
|
||||
noNodes=noNodes,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
super().__init__(debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes)
|
||||
def __repr__(self):
|
||||
rep = f"TCPInterface({self.hostname!r}"
|
||||
if self.debugOut is not None:
|
||||
rep += f", debugOut={self.debugOut!r}"
|
||||
if self.noProto:
|
||||
rep += ", noProto=True"
|
||||
if self.socket is None:
|
||||
rep += ", connectNow=False"
|
||||
if self.portNumber != DEFAULT_TCP_PORT:
|
||||
rep += f", portNumber={self.portNumber!r}"
|
||||
if self.noNodes:
|
||||
rep += ", noNodes=True"
|
||||
rep += ")"
|
||||
return rep
|
||||
|
||||
def _socket_shutdown(self) -> None:
|
||||
"""Shutdown the socket.
|
||||
@@ -50,30 +69,44 @@ class TCPInterface(StreamInterface):
|
||||
if self.socket is not None:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Connect the interface"""
|
||||
self.myConnect()
|
||||
super().connect()
|
||||
|
||||
def myConnect(self) -> None:
|
||||
"""Connect to socket"""
|
||||
logging.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe]
|
||||
"""Connect to socket (without attempting to start the interface's receive thread)"""
|
||||
logger.debug(f"Connecting to {self.hostname}") # type: ignore[str-bytes-safe]
|
||||
server_address = (self.hostname, self.portNumber)
|
||||
self.socket = socket.create_connection(server_address)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing TCP stream")
|
||||
super().close()
|
||||
"""Close a connection to the device."""
|
||||
logger.debug("Closing TCP stream")
|
||||
# Sometimes the socket read might be blocked in the reader thread.
|
||||
# Therefore we force the shutdown by closing the socket here
|
||||
# Therefore force a shutdown first to unblock reader thread reads.
|
||||
self._wantExit = True
|
||||
if self.socket is not None:
|
||||
with contextlib.suppress(Exception): # Ignore errors in shutdown, because we might have a race with the server
|
||||
with contextlib.suppress(
|
||||
Exception
|
||||
): # Ignore errors in shutdown, because we might have a race with the server
|
||||
self._socket_shutdown()
|
||||
self.socket.close()
|
||||
with contextlib.suppress(Exception):
|
||||
self.socket.close()
|
||||
|
||||
self.socket = None
|
||||
super().close()
|
||||
|
||||
def _writeBytes(self, b: bytes) -> None:
|
||||
"""Write an array of bytes to our stream and flush"""
|
||||
"""Write an array of bytes to our stream"""
|
||||
if self.socket is not None:
|
||||
self.socket.send(b)
|
||||
try:
|
||||
self.socket.sendall(b)
|
||||
except OSError as e:
|
||||
logger.error(f"Socket send error, reconnecting: {e}")
|
||||
if not self._wantExit:
|
||||
self._reconnect()
|
||||
raise
|
||||
|
||||
def _readBytes(self, length) -> Optional[bytes]:
|
||||
"""Read an array of bytes from our stream"""
|
||||
@@ -81,19 +114,36 @@ class TCPInterface(StreamInterface):
|
||||
data = self.socket.recv(length)
|
||||
# empty byte indicates a disconnected socket,
|
||||
# we need to handle it to avoid an infinite loop reading from null socket
|
||||
if data == b'':
|
||||
logging.debug("dead socket, re-connecting")
|
||||
# cleanup and reconnect socket without breaking reader thread
|
||||
with contextlib.suppress(Exception):
|
||||
self._socket_shutdown()
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
time.sleep(1)
|
||||
self.myConnect()
|
||||
self._startConfig()
|
||||
return None
|
||||
if data == b"":
|
||||
logger.debug("Closed socket, re-connecting")
|
||||
if not self._wantExit:
|
||||
self._reconnect()
|
||||
return data
|
||||
|
||||
# no socket, break reader thread
|
||||
self._wantExit = True
|
||||
return None
|
||||
|
||||
def _reconnect(self) -> None:
|
||||
"""Reconnect to the socket"""
|
||||
# Save the socket reference before attempting to acquire the lock.
|
||||
sock = self.socket
|
||||
start_config = False
|
||||
with self.reconnectLock:
|
||||
if self._wantExit:
|
||||
return
|
||||
# Don't reconnect: someone else already did it.
|
||||
if sock is not self.socket:
|
||||
return
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self._socket_shutdown()
|
||||
if self.socket is not None:
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
time.sleep(1)
|
||||
self.myConnect()
|
||||
start_config = True
|
||||
|
||||
if start_config and not self._wantExit and self.socket is not None:
|
||||
self._startConfig()
|
||||
|
||||
@@ -29,6 +29,7 @@ testNumber: int = 0
|
||||
|
||||
sendingInterface = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def onReceive(packet, interface) -> None:
|
||||
"""Callback invoked when a packet arrives"""
|
||||
@@ -79,7 +80,7 @@ def testSend(
|
||||
else:
|
||||
toNode = toInterface.myInfo.my_node_num
|
||||
|
||||
logging.debug(f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}")
|
||||
logger.debug(f"Sending test wantAck={wantAck} packet from {fromNode} to {toNode}")
|
||||
# pylint: disable=W0603
|
||||
global sendingInterface
|
||||
sendingInterface = fromInterface
|
||||
@@ -98,7 +99,7 @@ def testSend(
|
||||
|
||||
def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool:
|
||||
"""Run the tests."""
|
||||
logging.info(f"Running {numTests} tests with wantAck={wantAck}")
|
||||
logger.info(f"Running {numTests} tests with wantAck={wantAck}")
|
||||
numFail: int = 0
|
||||
numSuccess: int = 0
|
||||
for _ in range(numTests):
|
||||
@@ -112,26 +113,26 @@ def runTests(numTests: int=50, wantAck: bool=False, maxFailures: int=0) -> bool:
|
||||
)
|
||||
if not success:
|
||||
numFail = numFail + 1
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"Test {testNumber} failed, expected packet not received ({numFail} failures so far)"
|
||||
)
|
||||
else:
|
||||
numSuccess = numSuccess + 1
|
||||
logging.info(
|
||||
logger.info(
|
||||
f"Test {testNumber} succeeded {numSuccess} successes {numFail} failures so far"
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if numFail > maxFailures:
|
||||
logging.error("Too many failures! Test failed!")
|
||||
logger.error("Too many failures! Test failed!")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def testThread(numTests=50) -> bool:
|
||||
"""Test thread"""
|
||||
logging.info("Found devices, starting tests...")
|
||||
logger.info("Found devices, starting tests...")
|
||||
result: bool = runTests(numTests, wantAck=True)
|
||||
if result:
|
||||
# Run another test
|
||||
@@ -148,7 +149,7 @@ def onConnection(topic=pub.AUTO_TOPIC) -> None:
|
||||
def openDebugLog(portName) -> io.TextIOWrapper:
|
||||
"""Open the debug log file"""
|
||||
debugname = "log" + portName.replace("/", "_")
|
||||
logging.info(f"Writing serial debugging to {debugname}")
|
||||
logger.info(f"Writing serial debugging to {debugname}")
|
||||
return open(debugname, "w+", buffering=1, encoding="utf8")
|
||||
|
||||
|
||||
@@ -177,7 +178,7 @@ def testAll(numTests: int=5) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
logging.info("Ports opened, starting test")
|
||||
logger.info("Ports opened, starting test")
|
||||
result: bool = testThread(numTests)
|
||||
|
||||
for i in interfaces:
|
||||
@@ -196,14 +197,14 @@ def testSimulator() -> None:
|
||||
python3 -c 'from meshtastic.test import testSimulator; testSimulator()'
|
||||
"""
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.info("Connecting to simulator on localhost!")
|
||||
logger.info("Connecting to simulator on localhost!")
|
||||
try:
|
||||
iface: meshtastic.tcp_interface.TCPInterface = TCPInterface("localhost")
|
||||
iface.showInfo()
|
||||
iface.localNode.showInfo()
|
||||
iface.localNode.exitSimulator()
|
||||
iface.close()
|
||||
logging.info("Integration test successful!")
|
||||
logger.info("Integration test successful!")
|
||||
except:
|
||||
print("Error while testing simulator:", sys.exc_info()[0])
|
||||
traceback.print_exc()
|
||||
|
||||
69
meshtastic/tests/test_ble_interface.py
Normal file
69
meshtastic/tests/test_ble_interface.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Meshtastic unit tests for ble_interface.py"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from bleak.exc import BleakError
|
||||
|
||||
from ..ble_interface import BLEInterface
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ble_error_default_kind_unknown():
|
||||
"""BLEError defaults to UNKNOWN kind."""
|
||||
error = BLEInterface.BLEError("test")
|
||||
assert error.kind == BLEInterface.BLEError.UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ble_find_device_not_found_sets_kind():
|
||||
"""find_device emits DEVICE_NOT_FOUND for no scan results."""
|
||||
iface = object.__new__(BLEInterface)
|
||||
with patch("meshtastic.ble_interface.BLEInterface.scan", return_value=[]):
|
||||
with pytest.raises(BLEInterface.BLEError) as excinfo:
|
||||
iface.find_device("missing")
|
||||
assert excinfo.value.kind == BLEInterface.BLEError.DEVICE_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ble_find_device_multiple_sets_kind():
|
||||
"""find_device emits MULTIPLE_DEVICES for ambiguous matches."""
|
||||
iface = object.__new__(BLEInterface)
|
||||
first = MagicMock()
|
||||
first.name = "dup"
|
||||
first.address = "AA:AA:AA:AA:AA:01"
|
||||
second = MagicMock()
|
||||
second.name = "dup"
|
||||
second.address = "AA:AA:AA:AA:AA:02"
|
||||
with patch(
|
||||
"meshtastic.ble_interface.BLEInterface.scan", return_value=[first, second]
|
||||
):
|
||||
with pytest.raises(BLEInterface.BLEError) as excinfo:
|
||||
iface.find_device("dup")
|
||||
assert excinfo.value.kind == BLEInterface.BLEError.MULTIPLE_DEVICES
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ble_send_to_radio_wraps_write_errors_with_kind():
|
||||
"""_sendToRadioImpl wraps write failures with WRITE_ERROR."""
|
||||
iface = object.__new__(BLEInterface)
|
||||
iface.client = MagicMock()
|
||||
iface.client.write_gatt_char.side_effect = RuntimeError("boom")
|
||||
to_radio = MagicMock()
|
||||
to_radio.SerializeToString.return_value = b"\x01"
|
||||
with pytest.raises(BLEInterface.BLEError) as excinfo:
|
||||
iface._sendToRadioImpl(to_radio)
|
||||
assert excinfo.value.kind == BLEInterface.BLEError.WRITE_ERROR
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ble_receive_wraps_unexpected_bleak_error_with_kind():
|
||||
"""_receiveFromRadioImpl wraps unexpected BleakError with READ_ERROR."""
|
||||
iface = object.__new__(BLEInterface)
|
||||
iface.should_read = True
|
||||
iface._want_receive = True
|
||||
iface.client = MagicMock()
|
||||
iface.client.read_gatt_char.side_effect = BleakError("some other BLE failure")
|
||||
with pytest.raises(BLEInterface.BLEError) as excinfo:
|
||||
iface._receiveFromRadioImpl()
|
||||
assert excinfo.value.kind == BLEInterface.BLEError.READ_ERROR
|
||||
646
meshtastic/tests/test_inject_nanopb_options.py
Normal file
646
meshtastic/tests/test_inject_nanopb_options.py
Normal file
@@ -0,0 +1,646 @@
|
||||
"""Tests for bin/inject_nanopb_options.py — the nanopb options injection script
|
||||
and the generated protobuf descriptors it produces.
|
||||
|
||||
Part 1 (test_parse_*, test_inject_*): unit-tests the script's logic directly,
|
||||
using small synthetic proto snippets.
|
||||
|
||||
Part 2 (test_descriptor_*): smoke-tests the already-generated _pb2.py files to
|
||||
confirm the regen pipeline embedded the expected nanopb options.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from meshtastic.protobuf import (
|
||||
atak_pb2,
|
||||
config_pb2,
|
||||
mesh_pb2,
|
||||
nanopb_pb2,
|
||||
telemetry_pb2,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load bin/inject_nanopb_options.py as a module without adding it to the
|
||||
# package. __main__ guard means no side-effects on import.
|
||||
# ---------------------------------------------------------------------------
|
||||
_SCRIPT_PATH = Path(__file__).parent.parent.parent / "bin" / "inject_nanopb_options.py"
|
||||
|
||||
|
||||
def _load_inject_module():
|
||||
spec = importlib.util.spec_from_file_location("inject_nanopb_options", _SCRIPT_PATH)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
with patch.object(sys, "argv", ["inject_nanopb_options.py"]):
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
_inj = _load_inject_module()
|
||||
|
||||
parse_value = _inj.parse_value
|
||||
parse_options_file = _inj.parse_options_file
|
||||
format_nanopb_opts = _inj.format_nanopb_opts
|
||||
inject_into_proto = _inj.inject_into_proto
|
||||
message_path_matches = _inj.message_path_matches
|
||||
|
||||
# Convenience: the nanopb import path the script uses after the sed fixup
|
||||
NANOPB_IMPORT = 'import "meshtastic/protobuf/nanopb.proto";'
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Part 1 — Script unit tests
|
||||
# ===========================================================================
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_value
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_value_integer():
|
||||
"""parse_value converts a decimal string to int."""
|
||||
assert parse_value("40") == 40
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_value_negative_integer():
|
||||
"""parse_value handles negative integer strings."""
|
||||
assert parse_value("-1") == -1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_value_true():
|
||||
"""parse_value converts 'true' to Python True."""
|
||||
assert parse_value("true") is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_value_false():
|
||||
"""parse_value converts 'false' to Python False."""
|
||||
assert parse_value("false") is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_value_string():
|
||||
"""parse_value returns non-numeric, non-boolean strings as-is."""
|
||||
assert parse_value("IS_8") == "IS_8"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(st.integers())
|
||||
def test_parse_value_any_integer_returns_int(n):
|
||||
"""parse_value always returns int for any decimal integer string."""
|
||||
assert parse_value(str(n)) == n
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(st.text())
|
||||
def test_parse_value_never_crashes(s):
|
||||
"""parse_value never raises on arbitrary input."""
|
||||
result = parse_value(s)
|
||||
assert isinstance(result, (int, bool, str))
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(st.text().filter(lambda s: not s.lstrip("-").isdigit() and s.lower() not in ("true", "false")))
|
||||
def test_parse_value_non_numeric_non_bool_returns_str(s):
|
||||
"""parse_value returns the original string when it is neither an integer nor a boolean."""
|
||||
assert parse_value(s) == s
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_options_file
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _write_options(tmp_path: Path, content: str) -> Path:
|
||||
p = tmp_path / "test.options"
|
||||
p.write_text(textwrap.dedent(content))
|
||||
return p
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_wildcard(tmp_path):
|
||||
"""Wildcard pattern (no dot) lands in the wildcard dict."""
|
||||
f = _write_options(tmp_path, "*macaddr max_size:6 fixed_length:true\n")
|
||||
specific, wildcard = parse_options_file(f)
|
||||
assert "macaddr" in wildcard
|
||||
assert wildcard["macaddr"] == {"max_size": 6, "fixed_length": True}
|
||||
assert specific == {}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_specific(tmp_path):
|
||||
"""Single-dot pattern lands in the specific dict with a 2-tuple key."""
|
||||
f = _write_options(tmp_path, "*User.long_name max_size:40\n")
|
||||
specific, wildcard = parse_options_file(f)
|
||||
assert ("User", "long_name") in specific
|
||||
assert specific[("User", "long_name")] == {"max_size": 40}
|
||||
assert wildcard == {}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_multilevel(tmp_path):
|
||||
"""Three-part pattern (Route.Link.uid) produces a 3-tuple key."""
|
||||
f = _write_options(tmp_path, "*Route.Link.uid max_size:48\n")
|
||||
specific, _ = parse_options_file(f)
|
||||
assert ("Route", "Link", "uid") in specific
|
||||
assert specific[("Route", "Link", "uid")] == {"max_size": 48}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_strips_inline_comments(tmp_path):
|
||||
"""Text after # is ignored."""
|
||||
f = _write_options(tmp_path, "*id max_size:16 # node id strings\n")
|
||||
_, wildcard = parse_options_file(f)
|
||||
assert wildcard["id"] == {"max_size": 16}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_skips_comment_only_lines(tmp_path):
|
||||
"""Lines that are entirely comments produce no entries."""
|
||||
f = _write_options(tmp_path, "# this is a comment\n*id max_size:16\n")
|
||||
_, wildcard = parse_options_file(f)
|
||||
assert list(wildcard.keys()) == ["id"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_skips_blank_lines(tmp_path):
|
||||
"""Blank lines are silently ignored."""
|
||||
f = _write_options(tmp_path, "\n\n*id max_size:16\n\n")
|
||||
_, wildcard = parse_options_file(f)
|
||||
assert "id" in wildcard
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_skips_non_python_options(tmp_path):
|
||||
"""Options not in FIELD_OPTIONS (e.g. anonymous_oneof) are dropped."""
|
||||
f = _write_options(tmp_path, "*MeshPacket.payload_variant anonymous_oneof:true\n")
|
||||
specific, wildcard = parse_options_file(f)
|
||||
# anonymous_oneof is not in FIELD_OPTIONS → no entry should be produced
|
||||
assert specific == {}
|
||||
assert wildcard == {}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_merges_repeated_patterns(tmp_path):
|
||||
"""Two lines for the same pattern are merged."""
|
||||
f = _write_options(
|
||||
tmp_path,
|
||||
"*SecurityConfig.admin_key max_size:32\n"
|
||||
"*SecurityConfig.admin_key max_count:3\n",
|
||||
)
|
||||
specific, _ = parse_options_file(f)
|
||||
assert specific[("SecurityConfig", "admin_key")] == {"max_size": 32, "max_count": 3}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_parse_int_and_bool_values(tmp_path):
|
||||
"""int_size parses as int; fixed_length parses as bool."""
|
||||
f = _write_options(tmp_path, "*Data.payload max_size:233 fixed_length:false\n")
|
||||
specific, _ = parse_options_file(f)
|
||||
opts = specific[("Data", "payload")]
|
||||
assert opts["max_size"] == 233
|
||||
assert opts["fixed_length"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# message_path_matches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_message_path_matches_simple():
|
||||
"""A single-element path matches the current message on the stack."""
|
||||
stack = [("message", "User")]
|
||||
assert message_path_matches(stack, ("User",))
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_message_path_matches_nested():
|
||||
"""Both a 1-element and 2-element path match correctly against a nested stack."""
|
||||
stack = [("message", "Config"), ("message", "DeviceConfig")]
|
||||
assert message_path_matches(stack, ("DeviceConfig",))
|
||||
assert message_path_matches(stack, ("Config", "DeviceConfig"))
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_message_path_matches_with_oneof_in_stack():
|
||||
"""oneof frames in the stack are skipped when looking for messages."""
|
||||
stack = [("message", "MeshPacket"), ("oneof", "payload_variant")]
|
||||
assert message_path_matches(stack, ("MeshPacket",))
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_message_path_no_match():
|
||||
"""A path with the wrong message name does not match."""
|
||||
stack = [("message", "User")]
|
||||
assert not message_path_matches(stack, ("Route",))
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_message_path_multilevel_partial_match():
|
||||
"""A 2-element path must match the last 2 message names on the stack."""
|
||||
stack = [("message", "Route"), ("message", "Link")]
|
||||
assert message_path_matches(stack, ("Route", "Link"))
|
||||
assert not message_path_matches(stack, ("Other", "Link"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# format_nanopb_opts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_max_size():
|
||||
"""max_size is rendered as an integer literal."""
|
||||
assert format_nanopb_opts({"max_size": 40}) == "(nanopb).max_size = 40"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_int_size_as_enum():
|
||||
"""int_size numeric values are rendered as IS_8/IS_16/IS_32/IS_64 enum names."""
|
||||
assert format_nanopb_opts({"int_size": 8}) == "(nanopb).int_size = IS_8"
|
||||
assert format_nanopb_opts({"int_size": 16}) == "(nanopb).int_size = IS_16"
|
||||
assert format_nanopb_opts({"int_size": 32}) == "(nanopb).int_size = IS_32"
|
||||
assert format_nanopb_opts({"int_size": 64}) == "(nanopb).int_size = IS_64"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_bool_true():
|
||||
"""True is rendered as the proto literal 'true'."""
|
||||
assert format_nanopb_opts({"fixed_length": True}) == "(nanopb).fixed_length = true"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_format_bool_false():
|
||||
"""False is rendered as the proto literal 'false'."""
|
||||
assert format_nanopb_opts({"fixed_length": False}) == "(nanopb).fixed_length = false"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# inject_into_proto — helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_NANOPB_IMPORT_PATH = "meshtastic/protobuf/nanopb.proto"
|
||||
|
||||
|
||||
def _inject(proto_src: str, specific=None, wildcard=None) -> str:
|
||||
"""Run inject_into_proto with empty dicts as defaults."""
|
||||
return inject_into_proto(
|
||||
textwrap.dedent(proto_src),
|
||||
specific or {},
|
||||
wildcard or {},
|
||||
_NANOPB_IMPORT_PATH,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# inject_into_proto — option injection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_adds_option_to_plain_field():
|
||||
"""A field with no existing options gets a nanopb annotation."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/channel.proto";
|
||||
message User {
|
||||
string long_name = 1;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, specific={("User", "long_name"): {"max_size": 40}})
|
||||
assert "long_name = 1 [(nanopb).max_size = 40];" in result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_merges_with_existing_options():
|
||||
"""nanopb annotation is appended after existing field options."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/channel.proto";
|
||||
message User {
|
||||
bytes macaddr = 4 [deprecated = true];
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, wildcard={"macaddr": {"max_size": 6}})
|
||||
assert "[deprecated = true, (nanopb).max_size = 6];" in result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_int_size_uses_enum_name():
|
||||
"""int_size values are written as IS_N enum names, not raw integers."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message Foo {
|
||||
uint32 hop_limit = 9;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, specific={("Foo", "hop_limit"): {"int_size": 8}})
|
||||
assert "(nanopb).int_size = IS_8" in result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_wildcard_applied_across_messages():
|
||||
"""A wildcard option hits the matching field in every message."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message A {
|
||||
bytes macaddr = 1;
|
||||
}
|
||||
message B {
|
||||
bytes macaddr = 2;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, wildcard={"macaddr": {"max_size": 6}})
|
||||
assert result.count("(nanopb).max_size = 6") == 2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_specific_not_leaking_to_other_messages():
|
||||
"""A message-specific option does NOT apply to a different message's field."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message User {
|
||||
string id = 1;
|
||||
}
|
||||
message Other {
|
||||
string id = 1;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, specific={("User", "id"): {"max_size": 16}})
|
||||
# Easier: count annotations — should be exactly one
|
||||
assert result.count("(nanopb).max_size = 16") == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_nested_message():
|
||||
"""A 2-part specific key only hits the field in the correct nested message."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message Route {
|
||||
message Link {
|
||||
string uid = 1;
|
||||
}
|
||||
string uid = 2;
|
||||
}
|
||||
"""
|
||||
# Route.Link.uid → key = ('Route', 'Link', 'uid')
|
||||
result = _inject(proto, specific={("Route", "Link", "uid"): {"max_size": 48}})
|
||||
lines = result.splitlines()
|
||||
# Only the uid inside Link should have the annotation
|
||||
assert result.count("(nanopb).max_size = 48") == 1
|
||||
# Confirm it's the inner one (it has 4 spaces more indent than outer uid)
|
||||
annotated = next(l for l in lines if "(nanopb).max_size = 48" in l)
|
||||
plain = next(l for l in lines if "uid = 2" in l)
|
||||
assert annotated.index("uid") > plain.index("uid")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_skips_enum_body_values():
|
||||
"""Enum value lines must not be treated as field declarations."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
message Foo {
|
||||
enum Role {
|
||||
CLIENT = 0;
|
||||
ROUTER = 2;
|
||||
}
|
||||
Role role = 1;
|
||||
}
|
||||
"""
|
||||
# Wildcard for 'role' should only hit the field, not enum values
|
||||
result = _inject(proto, wildcard={"role": {"max_size": 8}})
|
||||
assert result.count("(nanopb)") == 1
|
||||
assert "(nanopb)" not in next(l for l in result.splitlines() if "CLIENT" in l)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_optional_qualifier_preserved():
|
||||
"""The 'optional' qualifier is kept when a field gets an annotation."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message Foo {
|
||||
optional uint32 altitude = 3;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, specific={("Foo", "altitude"): {"int_size": 16}})
|
||||
assert "optional uint32 altitude = 3 [(nanopb).int_size = IS_16];" in result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_repeated_qualifier_preserved():
|
||||
"""The 'repeated' qualifier is kept when a field gets an annotation."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message Foo {
|
||||
repeated int32 snr = 2;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, specific={("Foo", "snr"): {"max_count": 8}})
|
||||
assert "repeated int32 snr = 2 [(nanopb).max_count = 8];" in result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_multiple_options_on_one_field():
|
||||
"""Multiple options from the same pattern are all injected on one field."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message Foo {
|
||||
repeated int32 snr = 1;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, specific={("Foo", "snr"): {"max_count": 8, "int_size": 8}})
|
||||
assert "(nanopb).max_count = 8" in result
|
||||
assert "(nanopb).int_size = IS_8" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# inject_into_proto — import insertion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_adds_nanopb_import_when_absent():
|
||||
"""nanopb.proto import is added when the file has other imports."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message Foo {
|
||||
string name = 1;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, wildcard={"name": {"max_size": 30}})
|
||||
assert NANOPB_IMPORT in result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_no_duplicate_nanopb_import():
|
||||
"""nanopb.proto import is NOT added a second time if already present."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
import "meshtastic/protobuf/nanopb.proto";
|
||||
message Foo {
|
||||
string name = 1;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, wildcard={"name": {"max_size": 30}})
|
||||
assert result.count(NANOPB_IMPORT) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_import_placed_after_existing_imports():
|
||||
"""nanopb import appears after the last existing import, not at the top."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
import "meshtastic/protobuf/mesh.proto";
|
||||
message Foo {
|
||||
string name = 1;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, wildcard={"name": {"max_size": 30}})
|
||||
lines = result.splitlines()
|
||||
mesh_idx = next(i for i, l in enumerate(lines) if "mesh.proto" in l)
|
||||
nanopb_idx = next(i for i, l in enumerate(lines) if "nanopb.proto" in l)
|
||||
assert nanopb_idx == mesh_idx + 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_import_after_syntax_when_no_existing_imports():
|
||||
"""When a proto has no imports, nanopb import goes AFTER the syntax line,
|
||||
not before it (regression test for the last_import_idx == -1 bug)."""
|
||||
proto = """\
|
||||
syntax = "proto3";
|
||||
message XModem {
|
||||
uint32 seq = 2;
|
||||
}
|
||||
"""
|
||||
result = _inject(proto, specific={("XModem", "seq"): {"int_size": 16}})
|
||||
lines = result.splitlines()
|
||||
syntax_idx = next(i for i, l in enumerate(lines) if l.strip().startswith("syntax"))
|
||||
nanopb_idx = next(i for i, l in enumerate(lines) if "nanopb.proto" in l)
|
||||
assert nanopb_idx > syntax_idx, "nanopb import must come after the syntax line"
|
||||
# syntax line must still be first non-blank line
|
||||
first_non_blank = next(l.strip() for l in lines if l.strip())
|
||||
assert first_non_blank.startswith("syntax")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_inject_noop_when_no_options():
|
||||
"""Proto file is returned unchanged when there are no options to inject."""
|
||||
proto = 'syntax = "proto3";\nmessage Foo { string x = 1; }\n'
|
||||
result = _inject(proto)
|
||||
assert result == proto
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Part 2 — Descriptor integration tests
|
||||
# Verify that regen-protobufs.sh produced _pb2.py files with nanopb options
|
||||
# embedded in the serialized descriptors.
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def _field_opts(descriptor, *path):
|
||||
"""Walk a descriptor by field/nested-type path and return its nanopb opts.
|
||||
|
||||
Elements of *path that are message names are looked up in nested_types_by_name;
|
||||
the final element is looked up in fields_by_name.
|
||||
"""
|
||||
desc = descriptor
|
||||
for step in path[:-1]:
|
||||
desc = desc.nested_types_by_name[step]
|
||||
field = desc.fields_by_name[path[-1]]
|
||||
return field.GetOptions().Extensions[nanopb_pb2.nanopb]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_user_long_name():
|
||||
"""User.long_name has max_size = 40 from mesh.options."""
|
||||
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "long_name")
|
||||
assert opts.max_size == 40
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_user_short_name():
|
||||
"""User.short_name has max_size = 5 from mesh.options."""
|
||||
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "short_name")
|
||||
assert opts.max_size == 5
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_wildcard_macaddr():
|
||||
"""Wildcard option from mesh.options applied to User.macaddr."""
|
||||
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["User"], "macaddr")
|
||||
assert opts.max_size == 6
|
||||
assert opts.fixed_length is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_meshpacket_hop_limit():
|
||||
"""MeshPacket.hop_limit has int_size = IS_8 from mesh.options."""
|
||||
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["MeshPacket"], "hop_limit")
|
||||
assert opts.int_size == nanopb_pb2.IS_8
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_routediscovery_snr_towards():
|
||||
"""RouteDiscovery.snr_towards has max_count = 8 and int_size = IS_8 from mesh.options."""
|
||||
opts = _field_opts(
|
||||
mesh_pb2.DESCRIPTOR.message_types_by_name["RouteDiscovery"], "snr_towards"
|
||||
)
|
||||
assert opts.max_count == 8
|
||||
assert opts.int_size == nanopb_pb2.IS_8
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_data_payload():
|
||||
"""Data.payload has max_size = 233 from mesh.options."""
|
||||
opts = _field_opts(mesh_pb2.DESCRIPTOR.message_types_by_name["Data"], "payload")
|
||||
assert opts.max_size == 233
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_nested_deviceconfig_tzdef():
|
||||
"""Config.DeviceConfig.tzdef — option on a field inside a nested message."""
|
||||
config = config_pb2.DESCRIPTOR.message_types_by_name["Config"]
|
||||
opts = _field_opts(config, "DeviceConfig", "tzdef")
|
||||
assert opts.max_size == 65
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_nested_securityconfig_admin_key():
|
||||
"""Config.SecurityConfig.admin_key — two options merged from two .options lines."""
|
||||
config = config_pb2.DESCRIPTOR.message_types_by_name["Config"]
|
||||
opts = _field_opts(config, "SecurityConfig", "admin_key")
|
||||
assert opts.max_size == 32
|
||||
assert opts.max_count == 3
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_multilevel_nested_route_link_uid():
|
||||
"""Route.Link.uid — three-level nested pattern from atak.options."""
|
||||
route = atak_pb2.DESCRIPTOR.message_types_by_name["Route"]
|
||||
opts = _field_opts(route, "Link", "uid")
|
||||
assert opts.max_size == 48
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_descriptor_telemetry_environment_one_wire_temperature():
|
||||
"""EnvironmentMetrics.one_wire_temperature has max_count = 8 from telemetry.options."""
|
||||
env = telemetry_pb2.DESCRIPTOR.message_types_by_name["EnvironmentMetrics"]
|
||||
opts = _field_opts(env, "one_wire_temperature")
|
||||
assert opts.max_count == 8
|
||||
@@ -6,9 +6,12 @@ import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import mock_open, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import meshtastic.__main__ as mt_main
|
||||
|
||||
from meshtastic.__main__ import (
|
||||
export_config,
|
||||
@@ -18,12 +21,14 @@ from meshtastic.__main__ import (
|
||||
onNode,
|
||||
onReceive,
|
||||
tunnelMain,
|
||||
set_missing_flags_false,
|
||||
)
|
||||
from meshtastic import mt_config
|
||||
|
||||
from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611
|
||||
|
||||
# from ..ble_interface import BLEInterface
|
||||
from ..mesh_interface import MeshInterface
|
||||
from ..node import Node
|
||||
|
||||
# from ..radioconfig_pb2 import UserPreferences
|
||||
@@ -34,7 +39,6 @@ from ..tcp_interface import TCPInterface
|
||||
# from ..remote_hardware import onGPIOreceive
|
||||
# from ..config_pb2 import Config
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_init_parser_no_args(capsys):
|
||||
@@ -259,6 +263,113 @@ def test_main_info_with_permission_error(patched_getlogin, capsys, caplog):
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_ble_device_not_found_message(capsys):
|
||||
"""Test BLE device-not-found help text."""
|
||||
sys.argv = ["", "--info", "--ble", "any"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
|
||||
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
|
||||
"missing",
|
||||
mt_main.BLEInterface.BLEError.DEVICE_NOT_FOUND,
|
||||
)
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert excinfo.value.code == 1
|
||||
assert re.search(r"BLE device not found", out, re.MULTILINE)
|
||||
assert re.search(r"--ble-scan", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_ble_multiple_devices_message(capsys):
|
||||
"""Test BLE multiple-devices help text."""
|
||||
sys.argv = ["", "--info", "--ble", "any"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
|
||||
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
|
||||
"multiple",
|
||||
mt_main.BLEInterface.BLEError.MULTIPLE_DEVICES,
|
||||
)
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert excinfo.value.code == 1
|
||||
assert re.search(r"Multiple Meshtastic BLE devices found", out, re.MULTILINE)
|
||||
assert re.search(r"meshtastic --ble <name_or_address>", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_ble_write_error_message(capsys):
|
||||
"""Test BLE write-error help text."""
|
||||
sys.argv = ["", "--info", "--ble", "any"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
|
||||
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
|
||||
"write fail",
|
||||
mt_main.BLEInterface.BLEError.WRITE_ERROR,
|
||||
)
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert excinfo.value.code == 1
|
||||
assert re.search(r"Failed to write to BLE device", out, re.MULTILINE)
|
||||
assert re.search(r"user not in 'bluetooth' group", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_ble_read_error_message(capsys):
|
||||
"""Test BLE read-error help text."""
|
||||
sys.argv = ["", "--info", "--ble", "any"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init:
|
||||
mock_ble_init.side_effect = mt_main.BLEInterface.BLEError(
|
||||
"read fail",
|
||||
mt_main.BLEInterface.BLEError.READ_ERROR,
|
||||
)
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert excinfo.value.code == 1
|
||||
assert re.search(r"Failed to read from BLE device", out, re.MULTILINE)
|
||||
assert re.search(r"Move closer to the device", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_serial_timeout_message(capsys):
|
||||
"""Test serial timeout help text."""
|
||||
sys.argv = ["", "--info"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with patch("meshtastic.serial_interface.SerialInterface") as mock_serial:
|
||||
mock_serial.side_effect = MeshInterface.MeshInterfaceError("Timed out waiting")
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert excinfo.value.code == 1
|
||||
assert re.search(r"Connection timed out", out, re.MULTILINE)
|
||||
assert re.search(r"Device is rebooting", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_info_with_tcp_interface(capsys):
|
||||
@@ -454,6 +565,37 @@ def test_main_set_owner_short_to_bob(capsys):
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_is_unmessageable_to_true(capsys):
|
||||
"""Test --set-is-unmessageable true"""
|
||||
sys.argv = ["", "--set-is-unmessageable", "true"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
assert re.search(r"Setting device owner is_unmessageable to True", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_is_unmessagable_to_true(capsys):
|
||||
"""Test --set-is-unmessagable true"""
|
||||
sys.argv = ["", "--set-is-unmessagable", "true"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
assert re.search(r"Setting device owner is_unmessageable to True", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@@ -494,6 +636,44 @@ def test_main_get_canned_messages(capsys, caplog, iface_with_nodes):
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_ringtone(capsys):
|
||||
"""Test --set-ringtone"""
|
||||
sys.argv = ["", "--set-ringtone", "foo,bar"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
assert re.search(r"Setting ringtone to foo,bar", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_get_ringtone(capsys, caplog, iface_with_nodes):
|
||||
"""Test --get-ringtone"""
|
||||
sys.argv = ["", "--get-ringtone"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
iface = iface_with_nodes
|
||||
iface.devPath = "bar"
|
||||
|
||||
mocked_node = MagicMock(autospec=Node)
|
||||
mocked_node.get_ringtone.return_value = "foo,bar"
|
||||
iface.localNode = mocked_node
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface) as mo:
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
assert re.search(r"ringtone:foo,bar", out, re.MULTILINE)
|
||||
assert err == ""
|
||||
mo.assert_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@@ -688,12 +868,11 @@ def test_main_sendtext_with_invalid_channel_nine(caplog, capsys):
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_get, mock_set, capsys, caplog, iface_with_nodes):
|
||||
def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_hupcl, capsys, caplog, iface_with_nodes):
|
||||
"""Test --sendtext with --dest"""
|
||||
sys.argv = ["", "--sendtext", "hello", "--dest", "foo"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -887,12 +1066,11 @@ def test_main_seturl(capsys):
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
|
||||
def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
|
||||
"""Test --set with valid field"""
|
||||
sys.argv = ["", "--set", "network.wifi_ssid", "foo"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -912,12 +1090,11 @@ def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_get
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
|
||||
def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
|
||||
"""Test --set with valid field"""
|
||||
sys.argv = ["", "--set", "network.wifi_psk", "123456789"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -937,12 +1114,11 @@ def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, m
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
|
||||
def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
|
||||
"""Test --set with an invalid value (psk must be 8 or more characters)"""
|
||||
sys.argv = ["", "--set", "network.wifi_psk", "1234567"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -965,12 +1141,11 @@ def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open,
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
|
||||
def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
|
||||
"""Test --set with valid field"""
|
||||
sys.argv = ["", "--set", "network.wifi_ssid", "foo"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -991,12 +1166,11 @@ def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open,
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
|
||||
def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
|
||||
"""Test --set with invalid field"""
|
||||
sys.argv = ["", "--set", "foo", "foo"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -1017,12 +1191,11 @@ def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, moc
|
||||
# TODO: write some negative --configure tests
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
|
||||
def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
|
||||
"""Test --configure with valid file"""
|
||||
sys.argv = ["", "--configure", "example_config.yaml"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -1050,12 +1223,11 @@ def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mocked_open, mocked_get, mocked_set, capsys):
|
||||
def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
|
||||
"""Test --configure with valid file"""
|
||||
sys.argv = ["", "--configure", "exampleConfig.yaml"]
|
||||
mt_config.args = sys.argv
|
||||
@@ -1660,11 +1832,9 @@ def test_main_onReceive_with_sendtext(caplog, capsys):
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_onReceive_with_text(caplog, capsys):
|
||||
"""Test onReceive with text"""
|
||||
args = MagicMock()
|
||||
args.sendtext.return_value = "foo"
|
||||
mt_config.args = args
|
||||
def test_main_onReceive_with_text_replies_on_target_channel(caplog, capsys):
|
||||
"""Test onReceive replies when channel matches --ch-index (default 0)."""
|
||||
mt_config.args = SimpleNamespace(reply=True, ch_index=0, sendtext=None)
|
||||
|
||||
# Note: 'TEXT_MESSAGE_APP' value is 1
|
||||
# Note: Some of this is faked below.
|
||||
@@ -1690,6 +1860,83 @@ def test_main_onReceive_with_text(caplog, capsys):
|
||||
assert re.search(r"in onReceive", caplog.text, re.MULTILINE)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Sending reply", out, re.MULTILINE)
|
||||
iface.sendText.assert_called_once_with(
|
||||
"got msg 'faked' with rxSnr: 6.0 and hopLimit: 3", channelIndex=0
|
||||
)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_onReceive_with_text_ignores_non_target_channel(caplog, capsys):
|
||||
"""Test onReceive does not reply when packet channel differs from --ch-index."""
|
||||
mt_config.args = SimpleNamespace(reply=True, ch_index=1, sendtext=None)
|
||||
|
||||
packet = {
|
||||
"to": 4294967295,
|
||||
"decoded": {"portnum": 1, "payload": "hello", "text": "faked"},
|
||||
"id": 334776977,
|
||||
"hop_limit": 3,
|
||||
"want_ack": True,
|
||||
"rxSnr": 6.0,
|
||||
"hopLimit": 3,
|
||||
"raw": "faked",
|
||||
"fromId": "!28b5465c",
|
||||
"toId": "^all",
|
||||
"channel": 0,
|
||||
}
|
||||
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
iface.myInfo.my_node_num = 4294967295
|
||||
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
onReceive(packet, iface)
|
||||
assert re.search(r"in onReceive", caplog.text, re.MULTILINE)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(
|
||||
r"Ignored message on channel 0 \(waiting for channel 1\)", out, re.MULTILINE
|
||||
)
|
||||
iface.sendText.assert_not_called()
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_onReceive_with_text_replies_on_explicit_matching_channel(caplog, capsys):
|
||||
"""Test onReceive replies when explicit packet channel matches --ch-index."""
|
||||
mt_config.args = SimpleNamespace(reply=True, ch_index=2, sendtext=None)
|
||||
|
||||
packet = {
|
||||
"to": 4294967295,
|
||||
"decoded": {"portnum": 1, "payload": "hello", "text": "faked"},
|
||||
"id": 334776977,
|
||||
"hop_limit": 3,
|
||||
"want_ack": True,
|
||||
"rxSnr": 6.0,
|
||||
"hopLimit": 3,
|
||||
"raw": "faked",
|
||||
"fromId": "!28b5465c",
|
||||
"toId": "^all",
|
||||
"channel": 2,
|
||||
}
|
||||
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
iface.myInfo.my_node_num = 4294967295
|
||||
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
onReceive(packet, iface)
|
||||
assert re.search(r"in onReceive", caplog.text, re.MULTILINE)
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(
|
||||
r"Received channel 2\. Sending reply: got msg 'faked' with rxSnr: 6.0 and hopLimit: 3",
|
||||
out,
|
||||
re.MULTILINE,
|
||||
)
|
||||
iface.sendText.assert_called_once_with(
|
||||
"got msg 'faked' with rxSnr: 6.0 and hopLimit: 3", channelIndex=2
|
||||
)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@@ -1724,6 +1971,8 @@ def test_main_export_config(capsys):
|
||||
mo.getLongName.return_value = "foo"
|
||||
mo.getShortName.return_value = "oof"
|
||||
mo.localNode.getURL.return_value = "bar"
|
||||
mo.getCannedMessage.return_value = "foo|bar"
|
||||
mo.getRingtone.return_value = "24:d=32,o=5"
|
||||
mo.getMyNodeInfo().get.return_value = {
|
||||
"latitudeI": 1100000000,
|
||||
"longitudeI": 1200000000,
|
||||
@@ -1738,7 +1987,8 @@ position_broadcast_smart: true
|
||||
fixed_position: true
|
||||
position_flags: 35"""
|
||||
export_config(mo)
|
||||
out, err = capsys.readouterr()
|
||||
out = export_config(mo)
|
||||
err = ""
|
||||
|
||||
# ensure we do not output this line
|
||||
assert not re.search(r"Connected to radio", out, re.MULTILINE)
|
||||
@@ -1825,6 +2075,41 @@ position_flags: 35"""
|
||||
# mo.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_set_missing_flags_false():
|
||||
"""Test set_missing_flags_false() function"""
|
||||
config = {
|
||||
"bluetooth": {
|
||||
"enabled": True
|
||||
},
|
||||
"lora": {
|
||||
"txEnabled": True
|
||||
}
|
||||
}
|
||||
|
||||
false_defaults = {
|
||||
("bluetooth", "enabled"),
|
||||
("lora", "sx126xRxBoostedGain"),
|
||||
("lora", "txEnabled"),
|
||||
("lora", "usePreset"),
|
||||
("position", "positionBroadcastSmartEnabled"),
|
||||
("security", "serialEnabled"),
|
||||
("mqtt", "encryptionEnabled"),
|
||||
}
|
||||
|
||||
set_missing_flags_false(config, false_defaults)
|
||||
|
||||
# Preserved
|
||||
assert config["bluetooth"]["enabled"] is True
|
||||
assert config["lora"]["txEnabled"] is True
|
||||
|
||||
# Added
|
||||
assert config["lora"]["usePreset"] is False
|
||||
assert config["lora"]["sx126xRxBoostedGain"] is False
|
||||
assert config["position"]["positionBroadcastSmartEnabled"] is False
|
||||
assert config["security"]["serialEnabled"] is False
|
||||
assert config["mqtt"]["encryptionEnabled"] is False
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_gpio_rd_no_gpio_channel(capsys):
|
||||
@@ -2614,16 +2899,16 @@ def test_tunnel_subnet_arg_with_no_devices(mock_platform_system, caplog, capsys)
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="on windows is no fcntl module")
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("platform.system")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_tunnel_tunnel_arg(
|
||||
mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_platform_system, caplog, iface_with_nodes, capsys
|
||||
mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, mock_platform_system, caplog, iface_with_nodes, capsys
|
||||
):
|
||||
"""Test tunnel with tunnel arg (act like we are on a linux system)"""
|
||||
|
||||
@@ -2713,3 +2998,206 @@ def test_remove_ignored_node():
|
||||
main()
|
||||
|
||||
mocked_node.removeIgnored.assert_called_once_with("!12345678")
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_add_contact_url():
|
||||
"""Test --add-contact with a shareable URL"""
|
||||
url = "https://meshtastic.org/v/#CKqkvZgIElEKCSE4MzBmNTIyYRIQUm9hZHJ1bm5lciBSaWRnZRoEUktTTiIGAAAAAAAAKAk4AkIgRxo_Fw_ergQIhRqBbrHasLYy3gU-Ay8hrhu4OVnIPQc=" # pylint: disable=line-too-long
|
||||
sys.argv = ["", "--add-contact", url]
|
||||
mt_config.args = sys.argv
|
||||
mocked_node = MagicMock(autospec=Node)
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
iface.getNode.return_value = mocked_node
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
|
||||
main()
|
||||
|
||||
mocked_node.addContactURL.assert_called_once_with(url)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_contact_qr():
|
||||
"""Test --contact-qr with a node ID"""
|
||||
sys.argv = ["", "--contact-qr", "!830f522a"]
|
||||
mt_config.args = sys.argv
|
||||
mocked_node = MagicMock(autospec=Node)
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
iface.getNode.return_value = mocked_node
|
||||
mocked_node.iface = iface
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
|
||||
main()
|
||||
|
||||
mocked_node.getContactURL.assert_called_once_with("!830f522a", should_ignore=False, manually_verified=False)
|
||||
mocked_node.getContactURL.reset_mock()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_contact_qr_with_flags():
|
||||
"""Test --contact-qr with --contact-verified and --contact-ignore"""
|
||||
sys.argv = ["", "--contact-qr", "!830f522a", "--contact-verified", "--contact-ignore"]
|
||||
mt_config.args = sys.argv
|
||||
mocked_node = MagicMock(autospec=Node)
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
iface.getNode.return_value = mocked_node
|
||||
mocked_node.iface = iface
|
||||
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
|
||||
main()
|
||||
|
||||
mocked_node.getContactURL.assert_called_once_with("!830f522a", should_ignore=True, manually_verified=True)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_owner_whitespace_only(capsys):
|
||||
"""Test --set-owner with whitespace-only name"""
|
||||
sys.argv = ["", "--set-owner", " "]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_owner_empty_string(capsys):
|
||||
"""Test --set-owner with empty string"""
|
||||
sys.argv = ["", "--set-owner", ""]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_owner_short_whitespace_only(capsys):
|
||||
"""Test --set-owner-short with whitespace-only name"""
|
||||
sys.argv = ["", "--set-owner-short", " "]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_owner_short_empty_string(capsys):
|
||||
"""Test --set-owner-short with empty string"""
|
||||
sys.argv = ["", "--set-owner-short", ""]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_ham_whitespace_only(capsys):
|
||||
"""Test --set-ham with whitespace-only name"""
|
||||
sys.argv = ["", "--set-ham", " "]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_set_ham_empty_string(capsys):
|
||||
"""Test --set-ham with empty string"""
|
||||
sys.argv = ["", "--set-ham", ""]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
main()
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Ham radio callsign cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
# OTA-related tests
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_main_ota_update_file_not_found(capsys):
|
||||
"""Test --ota-update with non-existent file"""
|
||||
sys.argv = [
|
||||
"",
|
||||
"--ota-update",
|
||||
"/nonexistent/firmware.bin",
|
||||
"--host",
|
||||
"192.168.1.100",
|
||||
]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
main()
|
||||
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@patch("meshtastic.ota.ESP32WiFiOTA")
|
||||
@patch("meshtastic.__main__.meshtastic.util.our_exit")
|
||||
def test_main_ota_update_retries(mock_our_exit, mock_ota_class, capsys):
|
||||
"""Test --ota-update retries on failure"""
|
||||
# Create a temporary firmware file
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"fake firmware data")
|
||||
firmware_file = f.name
|
||||
|
||||
try:
|
||||
sys.argv = ["", "--ota-update", firmware_file, "--host", "192.168.1.100"]
|
||||
mt_config.args = sys.argv
|
||||
|
||||
# Mock the OTA class to fail all 5 retries
|
||||
mock_ota = MagicMock()
|
||||
mock_ota_class.return_value = mock_ota
|
||||
mock_ota.hash_bytes.return_value = b"\x00" * 32
|
||||
mock_ota.hash_hex.return_value = "a" * 64
|
||||
mock_ota.update.side_effect = Exception("Connection failed")
|
||||
|
||||
# Mock isinstance to return True
|
||||
with patch("meshtastic.__main__.isinstance", return_value=True):
|
||||
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
|
||||
mock_iface = MagicMock()
|
||||
mock_iface.hostname = "192.168.1.100"
|
||||
mock_iface.localNode = MagicMock(autospec=Node)
|
||||
mock_tcp.return_value = mock_iface
|
||||
|
||||
with patch("time.sleep"):
|
||||
main()
|
||||
|
||||
# Should have exhausted all retries and called our_exit
|
||||
# Note: our_exit might be called twice - once for TCP check, once for failure
|
||||
assert mock_our_exit.call_count >= 1
|
||||
# Check the last call was for OTA failure
|
||||
last_call_args = mock_our_exit.call_args[0][0]
|
||||
assert "OTA update failed" in last_call_args
|
||||
|
||||
finally:
|
||||
os.unlink(firmware_file)
|
||||
|
||||
@@ -525,6 +525,28 @@ def test_getMyNodeInfo():
|
||||
myinfo = iface.getMyNodeInfo()
|
||||
assert myinfo == anode
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getCannedMessage():
|
||||
"""Test MeshInterface.getCannedMessage()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
node = MagicMock()
|
||||
node.get_canned_message.return_value = "Hi|Bye|Yes"
|
||||
iface.localNode = node
|
||||
result = iface.getCannedMessage()
|
||||
assert result == "Hi|Bye|Yes"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_getRingtone():
|
||||
"""Test MeshInterface.getRingtone()"""
|
||||
iface = MeshInterface(noProto=True)
|
||||
node = MagicMock()
|
||||
node.get_ringtone.return_value = "foo,bar"
|
||||
iface.localNode = node
|
||||
result = iface.getRingtone()
|
||||
assert result == "foo,bar"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
@@ -543,7 +565,6 @@ def test_generatePacketId(capsys):
|
||||
assert err == ""
|
||||
assert pytest_wrapped_e.type == MeshInterface.MeshInterfaceError
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_fixupPosition_empty_pos():
|
||||
@@ -651,15 +672,21 @@ def test_getOrCreateByNum(iface_with_nodes):
|
||||
@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)
|
||||
try:
|
||||
with MeshInterface(noProto=True):
|
||||
raise ValueError("Something went wrong")
|
||||
except:
|
||||
assert re.search(
|
||||
r"An exception of type <class \'ValueError\'> with value Something went wrong has occurred",
|
||||
caplog.text,
|
||||
re.MULTILINE,
|
||||
)
|
||||
assert re.search(
|
||||
r"Traceback:\n.*in test_exit_with_exception\n {4}raise ValueError\(\"Something went wrong\"\)",
|
||||
caplog.text,
|
||||
re.MULTILINE
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -730,3 +757,43 @@ def test_timeago_fuzz(seconds):
|
||||
"""Fuzz _timeago to ensure it works with any integer"""
|
||||
val = _timeago(seconds)
|
||||
assert re.match(r"(now|\d+ (secs?|mins?|hours?|days?|months?|years?))", val)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_onResponseTraceRoute_routing_error(capsys):
|
||||
"""Test that onResponseTraceRoute handles ROUTING_APP error packets correctly."""
|
||||
iface = MeshInterface(noProto=True)
|
||||
|
||||
packet = {
|
||||
"decoded": {
|
||||
"portnum": "ROUTING_APP",
|
||||
"routing": {"errorReason": "MAX_RETRANSMIT"},
|
||||
}
|
||||
}
|
||||
|
||||
iface.onResponseTraceRoute(packet)
|
||||
|
||||
assert iface._acknowledgment.receivedTraceRoute is True
|
||||
out, _ = capsys.readouterr()
|
||||
assert "Traceroute failed: MAX_RETRANSMIT" in out
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_onResponseTraceRoute_routing_none(capsys):
|
||||
"""Test that onResponseTraceRoute does not print error for ROUTING_APP with NONE errorReason."""
|
||||
iface = MeshInterface(noProto=True)
|
||||
|
||||
packet = {
|
||||
"decoded": {
|
||||
"portnum": "ROUTING_APP",
|
||||
"routing": {"errorReason": "NONE"},
|
||||
}
|
||||
}
|
||||
|
||||
iface.onResponseTraceRoute(packet)
|
||||
|
||||
assert iface._acknowledgment.receivedTraceRoute is True
|
||||
out, _ = capsys.readouterr()
|
||||
assert "Traceroute failed" not in out
|
||||
|
||||
22
meshtastic/tests/test_mesh_interface_traffic_management.py
Normal file
22
meshtastic/tests/test_mesh_interface_traffic_management.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Meshtastic unit tests for traffic management handling in mesh_interface.py."""
|
||||
|
||||
import pytest
|
||||
|
||||
from ..mesh_interface import MeshInterface
|
||||
from ..protobuf import mesh_pb2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_handleFromRadio_with_traffic_management_module_config():
|
||||
"""Test _handleFromRadio with moduleConfig.traffic_management."""
|
||||
iface = MeshInterface(noProto=True)
|
||||
from_radio = mesh_pb2.FromRadio()
|
||||
from_radio.moduleConfig.traffic_management.enabled = True
|
||||
from_radio.moduleConfig.traffic_management.rate_limit_enabled = True
|
||||
|
||||
iface._handleFromRadio(from_radio.SerializeToString())
|
||||
|
||||
assert iface.localNode.moduleConfig.traffic_management.enabled is True
|
||||
assert iface.localNode.moduleConfig.traffic_management.rate_limit_enabled is True
|
||||
iface.close()
|
||||
@@ -1,16 +1,20 @@
|
||||
"""Meshtastic unit tests for node.py"""
|
||||
# pylint: disable=C0302
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from ..protobuf import admin_pb2, localonly_pb2, config_pb2
|
||||
from ..protobuf import admin_pb2, localonly_pb2, config_pb2, mesh_pb2, nanopb_pb2
|
||||
from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611
|
||||
from ..node import Node
|
||||
from ..serial_interface import SerialInterface
|
||||
from ..mesh_interface import MeshInterface
|
||||
from ..util import to_node_num
|
||||
|
||||
# from ..config_pb2 import Config
|
||||
# from ..cannedmessages_pb2 import (CannedMessagePluginMessagePart1, CannedMessagePluginMessagePart2,
|
||||
@@ -18,6 +22,11 @@ from ..mesh_interface import MeshInterface
|
||||
# CannedMessagePluginMessagePart5)
|
||||
# from ..util import Timeout
|
||||
|
||||
# Extract nanopb max_size constraints from the User protobuf descriptor
|
||||
_USER_NANOPB = {
|
||||
field.name: field.GetOptions().Extensions[nanopb_pb2.nanopb]
|
||||
for field in mesh_pb2.User.DESCRIPTOR.fields
|
||||
}
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_node(capsys):
|
||||
@@ -261,6 +270,36 @@ def test_shutdown(caplog):
|
||||
assert re.search(r"Telling node to shutdown", caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_factoryReset_config_uses_int_field():
|
||||
"""Test factoryReset(config) sets int32 protobuf field with an int value."""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 1234567890, noProto=True)
|
||||
|
||||
amesg = admin_pb2.AdminMessage()
|
||||
with patch("meshtastic.node.admin_pb2.AdminMessage", return_value=amesg):
|
||||
with patch.object(anode, "_sendAdmin") as mock_send_admin:
|
||||
anode.factoryReset(full=False)
|
||||
|
||||
assert amesg.factory_reset_config == 1
|
||||
mock_send_admin.assert_called_once_with(amesg, onResponse=anode.onAckNak)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_factoryReset_full_sets_device_field():
|
||||
"""Test factoryReset(full=True) sets the full-device reset protobuf field."""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 1234567890, noProto=True)
|
||||
|
||||
amesg = admin_pb2.AdminMessage()
|
||||
with patch("meshtastic.node.admin_pb2.AdminMessage", return_value=amesg):
|
||||
with patch.object(anode, "_sendAdmin") as mock_send_admin:
|
||||
anode.factoryReset(full=True)
|
||||
|
||||
assert amesg.factory_reset_device == 1
|
||||
mock_send_admin.assert_called_once_with(amesg, onResponse=anode.onAckNak)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setURL_empty_url(capsys):
|
||||
"""Test reboot"""
|
||||
@@ -308,6 +347,248 @@ def test_setURL_valid_URL_but_no_settings(capsys):
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("node_id,node_data,should_ignore,manually_verified", [
|
||||
pytest.param(
|
||||
"!830f522a",
|
||||
{
|
||||
"num": 2198819370,
|
||||
"user": {
|
||||
"id": "!830f522a",
|
||||
"longName": "Roadrunner Ridge",
|
||||
"shortName": "RKSN",
|
||||
"macaddr": "AAAAAAAAAAA=",
|
||||
"hwModel": "RAK4631",
|
||||
"role": "ROUTER",
|
||||
"publicKey": "Rx8XD96uBAiFGoFusdqwti3eBT4DLyGuG7g5Wcg9Bw==",
|
||||
"isLicensed": True,
|
||||
"isUnmessagable": False,
|
||||
},
|
||||
},
|
||||
True,
|
||||
True,
|
||||
id="all_fields_all_flags",
|
||||
),
|
||||
pytest.param(
|
||||
"!12345678",
|
||||
{
|
||||
"num": 305419896,
|
||||
"user": {
|
||||
"id": "!12345678",
|
||||
"longName": "Test Node",
|
||||
"shortName": "TN",
|
||||
"macaddr": "QkVTVEVWRVI=",
|
||||
"hwModel": "TBEAM",
|
||||
},
|
||||
},
|
||||
False,
|
||||
False,
|
||||
id="minimal_fields_no_flags",
|
||||
),
|
||||
pytest.param(
|
||||
305419896,
|
||||
{
|
||||
"num": 305419896,
|
||||
"user": {
|
||||
"id": "!12345678",
|
||||
"longName": "Another Node",
|
||||
"shortName": "AN",
|
||||
"macaddr": "QkVTVEVWRVI=",
|
||||
"hwModel": "HELTEC_V3",
|
||||
"role": "CLIENT",
|
||||
"publicKey": "AAAAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
"isLicensed": False,
|
||||
},
|
||||
},
|
||||
True,
|
||||
False,
|
||||
id="int_node_id_should_ignore_only",
|
||||
),
|
||||
pytest.param(
|
||||
"!deadbeef",
|
||||
{
|
||||
"num": 3735928559,
|
||||
"user": {
|
||||
"id": "!deadbeef",
|
||||
"longName": "Minimal Contact",
|
||||
"shortName": "MC",
|
||||
"macaddr": "BQYHCAkKCw==",
|
||||
"hwModel": "UNSET",
|
||||
"role": "CLIENT_MUTE",
|
||||
},
|
||||
},
|
||||
False,
|
||||
True,
|
||||
id="unset_hw_model_verified_only",
|
||||
),
|
||||
pytest.param(
|
||||
"!1a2b3c4d",
|
||||
{
|
||||
"num": 439041101,
|
||||
"user": {
|
||||
"id": "!1a2b3c4d",
|
||||
"longName": "Licensed Node",
|
||||
"shortName": "LN",
|
||||
"macaddr": "DA0ODxAREg==",
|
||||
"hwModel": "NANO_G1",
|
||||
"isLicensed": True,
|
||||
"isUnmessagable": True,
|
||||
},
|
||||
},
|
||||
False,
|
||||
False,
|
||||
id="licensed_unmessagable_no_flags",
|
||||
),
|
||||
])
|
||||
def test_contact_url_roundtrip(node_id, node_data, should_ignore, manually_verified):
|
||||
"""Verify that contact URL generation via getContactURL() and parsing via addContactURL() is fully reversible"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
node_num = to_node_num(node_id)
|
||||
iface.nodesByNum = {node_num: node_data}
|
||||
iface.localNode = None
|
||||
|
||||
anode = Node(iface, node_num, noProto=True)
|
||||
|
||||
sent_admin = []
|
||||
def capture_send(p, *_args, **_kwargs):
|
||||
sent_admin.append(p)
|
||||
|
||||
with patch.object(anode, "_sendAdmin", side_effect=capture_send):
|
||||
url = anode.getContactURL(node_id, should_ignore=should_ignore, manually_verified=manually_verified)
|
||||
assert url.startswith("https://meshtastic.org/v/#")
|
||||
|
||||
anode.addContactURL(url)
|
||||
|
||||
assert len(sent_admin) == 1
|
||||
contact = sent_admin[0].add_contact
|
||||
u = node_data["user"]
|
||||
|
||||
assert contact.node_num == node_num
|
||||
assert contact.user.id == u["id"]
|
||||
assert contact.user.long_name == u["longName"]
|
||||
assert contact.user.short_name == u["shortName"]
|
||||
assert contact.user.macaddr == base64.b64decode(u["macaddr"])
|
||||
|
||||
if u.get("hwModel") and u["hwModel"] != "UNSET":
|
||||
assert contact.user.hw_model == mesh_pb2.HardwareModel.Value(u["hwModel"])
|
||||
if u.get("role"):
|
||||
assert contact.user.role == config_pb2.Config.DeviceConfig.Role.Value(u["role"])
|
||||
if u.get("publicKey"):
|
||||
assert contact.user.public_key == base64.b64decode(u["publicKey"])
|
||||
if u.get("isLicensed"):
|
||||
assert contact.user.is_licensed is True
|
||||
if u.get("isUnmessagable") is not None:
|
||||
assert contact.user.is_unmessagable == u["isUnmessagable"]
|
||||
|
||||
assert contact.should_ignore == should_ignore
|
||||
assert contact.manually_verified == manually_verified
|
||||
|
||||
|
||||
@st.composite
|
||||
def contact_url_roundtrip_params(draw):
|
||||
"""Hypothesis strategy: generate a full node config and roundtrip flags"""
|
||||
should_ignore = draw(st.booleans())
|
||||
manually_verified = draw(st.booleans())
|
||||
|
||||
node_num = draw(st.integers(min_value=6, max_value=2**32 - 2))
|
||||
node_id = f"!{node_num:08x}"
|
||||
|
||||
hw_model = draw(st.sampled_from(list(mesh_pb2.HardwareModel.keys())))
|
||||
role = draw(st.one_of(
|
||||
st.none(),
|
||||
st.sampled_from(list(config_pb2.Config.DeviceConfig.Role.keys())),
|
||||
))
|
||||
|
||||
long_name = draw(st.text(
|
||||
min_size=1, max_size=_USER_NANOPB['long_name'].max_size
|
||||
))
|
||||
short_name = draw(st.text(
|
||||
min_size=1, max_size=_USER_NANOPB['short_name'].max_size
|
||||
))
|
||||
|
||||
macaddr_bytes = draw(st.binary(
|
||||
min_size=_USER_NANOPB['macaddr'].max_size,
|
||||
max_size=_USER_NANOPB['macaddr'].max_size,
|
||||
))
|
||||
macaddr_b64 = base64.b64encode(macaddr_bytes).decode("ascii")
|
||||
|
||||
has_public_key = draw(st.booleans())
|
||||
public_key_b64 = None
|
||||
if has_public_key:
|
||||
pk_bytes = draw(st.binary(
|
||||
min_size=_USER_NANOPB['public_key'].max_size,
|
||||
max_size=_USER_NANOPB['public_key'].max_size,
|
||||
))
|
||||
public_key_b64 = base64.b64encode(pk_bytes).decode("ascii")
|
||||
|
||||
is_licensed = draw(st.booleans())
|
||||
is_unmessagable = draw(st.booleans())
|
||||
|
||||
node_data = {
|
||||
"num": node_num,
|
||||
"user": {
|
||||
"id": node_id,
|
||||
"longName": long_name,
|
||||
"shortName": short_name,
|
||||
"macaddr": macaddr_b64,
|
||||
"hwModel": hw_model,
|
||||
"isLicensed": is_licensed,
|
||||
"isUnmessagable": is_unmessagable,
|
||||
},
|
||||
}
|
||||
if role is not None:
|
||||
node_data["user"]["role"] = role
|
||||
if public_key_b64 is not None:
|
||||
node_data["user"]["publicKey"] = public_key_b64
|
||||
|
||||
return node_num, node_data, should_ignore, manually_verified
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(contact_url_roundtrip_params())
|
||||
def test_contact_url_roundtrip_hypothesis(params):
|
||||
"""Property: roundtrip preserves data across random field configurations"""
|
||||
node_num, node_data, should_ignore, manually_verified = params
|
||||
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
iface.nodesByNum = {node_num: node_data}
|
||||
iface.localNode = None
|
||||
|
||||
anode = Node(iface, node_num, noProto=True)
|
||||
|
||||
sent_admin = []
|
||||
def capture_send(p, *_args, **_kwargs):
|
||||
sent_admin.append(p)
|
||||
|
||||
with patch.object(anode, "_sendAdmin", side_effect=capture_send):
|
||||
url = anode.getContactURL(
|
||||
node_num,
|
||||
should_ignore=should_ignore,
|
||||
manually_verified=manually_verified,
|
||||
)
|
||||
anode.addContactURL(url)
|
||||
|
||||
assert len(sent_admin) == 1
|
||||
contact = sent_admin[0].add_contact
|
||||
u = node_data["user"]
|
||||
|
||||
assert contact.node_num == node_num
|
||||
assert contact.user.id == u["id"]
|
||||
assert contact.user.long_name == u["longName"]
|
||||
assert contact.user.short_name == u["shortName"]
|
||||
assert contact.user.macaddr == base64.b64decode(u["macaddr"])
|
||||
assert contact.user.hw_model == mesh_pb2.HardwareModel.Value(u["hwModel"])
|
||||
|
||||
if "role" in u:
|
||||
assert contact.user.role == config_pb2.Config.DeviceConfig.Role.Value(u["role"])
|
||||
if "publicKey" in u:
|
||||
assert contact.user.public_key == base64.b64decode(u["publicKey"])
|
||||
assert contact.user.is_licensed == u["isLicensed"]
|
||||
assert contact.user.is_unmessagable == u["isUnmessagable"]
|
||||
assert contact.should_ignore == should_ignore
|
||||
assert contact.manually_verified == manually_verified
|
||||
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unit
|
||||
# def test_showChannels(capsys):
|
||||
@@ -794,6 +1075,30 @@ def test_writeConfig_with_no_radioConfig(capsys):
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_writeConfig_traffic_management():
|
||||
"""Test writeConfig with traffic_management module config."""
|
||||
iface = MagicMock(autospec=SerialInterface)
|
||||
anode = Node(iface, 123, noProto=True)
|
||||
anode.moduleConfig.traffic_management.enabled = True
|
||||
anode.moduleConfig.traffic_management.rate_limit_enabled = True
|
||||
|
||||
sent_admin = []
|
||||
|
||||
def capture_send(p, *args, **kwargs): # pylint: disable=W0613
|
||||
sent_admin.append(p)
|
||||
|
||||
with patch.object(anode, "_sendAdmin", side_effect=capture_send):
|
||||
anode.writeConfig("traffic_management")
|
||||
|
||||
assert len(sent_admin) == 1
|
||||
assert sent_admin[0].HasField("set_module_config")
|
||||
assert sent_admin[0].set_module_config.HasField("traffic_management")
|
||||
assert sent_admin[0].set_module_config.traffic_management.enabled is True
|
||||
assert sent_admin[0].set_module_config.traffic_management.rate_limit_enabled is True
|
||||
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unit
|
||||
# def test_writeConfig(caplog):
|
||||
@@ -1254,8 +1559,7 @@ def test_requestChannels_non_localNode_starting_index(caplog):
|
||||
# },
|
||||
# 'id': 1692918436,
|
||||
# 'hopLimit': 3,
|
||||
# 'priority':
|
||||
# 'RELIABLE',
|
||||
# 'priority': 'RELIABLE',
|
||||
# 'raw': 'fake',
|
||||
# 'fromId': '!9388f81c',
|
||||
# 'toId': '!9388f81c'
|
||||
@@ -1480,6 +1784,112 @@ def test_remove_ignored(ignored):
|
||||
iface.sendData.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setOwner_whitespace_only_long_name(capsys):
|
||||
"""Test setOwner with whitespace-only long name"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 123, noProto=True)
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
anode.setOwner(long_name=" ")
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setOwner_empty_long_name(capsys):
|
||||
"""Test setOwner with empty long name"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 123, noProto=True)
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
anode.setOwner(long_name="")
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Long Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setOwner_whitespace_only_short_name(capsys):
|
||||
"""Test setOwner with whitespace-only short name"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 123, noProto=True)
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
anode.setOwner(short_name=" ")
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setOwner_empty_short_name(capsys):
|
||||
"""Test setOwner with empty short name"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 123, noProto=True)
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
anode.setOwner(short_name="")
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "ERROR: Short Name cannot be empty or contain only whitespace characters" in out
|
||||
assert excinfo.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_setOwner_valid_names(caplog):
|
||||
"""Test setOwner with valid names"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 123, noProto=True)
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
anode.setOwner(long_name="ValidName", short_name="VN")
|
||||
|
||||
# Should not raise any exceptions
|
||||
# Note: When noProto=True, _sendAdmin is not called as the method returns early
|
||||
assert re.search(r'p.set_owner.long_name:ValidName:', caplog.text, re.MULTILINE)
|
||||
assert re.search(r'p.set_owner.short_name:VN:', caplog.text, re.MULTILINE)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_start_ota_local_node():
|
||||
"""Test startOTA on local node"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
anode = Node(iface, 1234567890, noProto=True)
|
||||
# Set up as local node
|
||||
iface.localNode = anode
|
||||
|
||||
amesg = admin_pb2.AdminMessage()
|
||||
with patch("meshtastic.admin_pb2.AdminMessage", return_value=amesg):
|
||||
with patch.object(anode, "_sendAdmin") as mock_send_admin:
|
||||
test_hash = b"\x01\x02\x03" * 8 # 24 bytes hash
|
||||
anode.startOTA(ota_mode=admin_pb2.OTAMode.OTA_WIFI, ota_file_hash=test_hash)
|
||||
|
||||
# Verify the OTA request was set correctly
|
||||
assert amesg.ota_request.reboot_ota_mode == admin_pb2.OTAMode.OTA_WIFI
|
||||
assert amesg.ota_request.ota_hash == test_hash
|
||||
mock_send_admin.assert_called_once_with(amesg)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_start_ota_remote_node_raises_error():
|
||||
"""Test startOTA on remote node raises ValueError"""
|
||||
iface = MagicMock(autospec=MeshInterface)
|
||||
local_node = Node(iface, 1234567890, noProto=True)
|
||||
remote_node = Node(iface, 9876543210, noProto=True)
|
||||
iface.localNode = local_node
|
||||
|
||||
test_hash = b"\x01\x02\x03" * 8
|
||||
with pytest.raises(ValueError, match="startOTA only possible in local node"):
|
||||
remote_node.startOTA(
|
||||
ota_mode=admin_pb2.OTAMode.OTA_WIFI, ota_file_hash=test_hash
|
||||
)
|
||||
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unitslow
|
||||
# def test_waitForConfig():
|
||||
|
||||
455
meshtastic/tests/test_ota.py
Normal file
455
meshtastic/tests/test_ota.py
Normal file
@@ -0,0 +1,455 @@
|
||||
"""Meshtastic unit tests for ota.py"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from meshtastic.ota import (
|
||||
_file_sha256,
|
||||
ESP32WiFiOTA,
|
||||
OTAError,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_file_sha256():
|
||||
"""Test _file_sha256 calculates correct hash"""
|
||||
# Create a temporary file with known content
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
test_data = b"Hello, World!"
|
||||
f.write(test_data)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
result = _file_sha256(temp_file)
|
||||
expected_hash = hashlib.sha256(test_data).hexdigest()
|
||||
assert result.hexdigest() == expected_hash
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_file_sha256_large_file():
|
||||
"""Test _file_sha256 handles files larger than chunk size"""
|
||||
# Create a temporary file with more than 4096 bytes
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
test_data = b"A" * 8192 # More than 4096 bytes
|
||||
f.write(test_data)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
result = _file_sha256(temp_file)
|
||||
expected_hash = hashlib.sha256(test_data).hexdigest()
|
||||
assert result.hexdigest() == expected_hash
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_init_file_not_found():
|
||||
"""Test ESP32WiFiOTA raises FileNotFoundError for non-existent file"""
|
||||
with pytest.raises(FileNotFoundError, match="does not exist"):
|
||||
ESP32WiFiOTA("/nonexistent/firmware.bin", "192.168.1.1")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_init_success():
|
||||
"""Test ESP32WiFiOTA initializes correctly with valid file"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"fake firmware data")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1", 3232)
|
||||
assert ota._filename == temp_file
|
||||
assert ota._hostname == "192.168.1.1"
|
||||
assert ota._port == 3232
|
||||
assert ota._socket is None
|
||||
# Verify hash is calculated
|
||||
assert ota._file_hash is not None
|
||||
assert len(ota.hash_hex()) == 64 # SHA256 hex is 64 chars
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_init_default_port():
|
||||
"""Test ESP32WiFiOTA uses default port 3232"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"fake firmware data")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
assert ota._port == 3232
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_hash_bytes():
|
||||
"""Test hash_bytes returns correct bytes"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
test_data = b"firmware data"
|
||||
f.write(test_data)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
hash_bytes = ota.hash_bytes()
|
||||
expected_bytes = hashlib.sha256(test_data).digest()
|
||||
assert hash_bytes == expected_bytes
|
||||
assert len(hash_bytes) == 32 # SHA256 is 32 bytes
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_hash_hex():
|
||||
"""Test hash_hex returns correct hex string"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
test_data = b"firmware data"
|
||||
f.write(test_data)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
hash_hex = ota.hash_hex()
|
||||
expected_hex = hashlib.sha256(test_data).hexdigest()
|
||||
assert hash_hex == expected_hex
|
||||
assert len(hash_hex) == 64 # SHA256 hex is 64 chars
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_read_line_not_connected():
|
||||
"""Test _read_line raises ConnectionError when not connected"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
with pytest.raises(ConnectionError, match="Socket not connected"):
|
||||
ota._read_line()
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_read_line_connection_closed():
|
||||
"""Test _read_line raises ConnectionError when connection closed"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
mock_socket = MagicMock()
|
||||
# Simulate connection closed
|
||||
mock_socket.recv.return_value = b""
|
||||
ota._socket = mock_socket
|
||||
|
||||
with pytest.raises(ConnectionError, match="Connection closed"):
|
||||
ota._read_line()
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_esp32_wifi_ota_read_line_success():
|
||||
"""Test _read_line successfully reads a line"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
mock_socket = MagicMock()
|
||||
# Simulate receiving "OK\n"
|
||||
mock_socket.recv.side_effect = [b"O", b"K", b"\n"]
|
||||
ota._socket = mock_socket
|
||||
|
||||
result = ota._read_line()
|
||||
assert result == "OK"
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_success(mock_socket_class):
|
||||
"""Test update() with successful OTA"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
test_data = b"A" * 1024 # 1KB of data
|
||||
f.write(test_data)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Setup mock socket
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
# Mock _read_line to return appropriate responses
|
||||
# First call: ERASING, Second call: OK (ready), Third call: OK (complete)
|
||||
with patch.object(ota, "_read_line") as mock_read_line:
|
||||
mock_read_line.side_effect = [
|
||||
"ERASING", # Device is erasing flash
|
||||
"OK", # Device ready for firmware
|
||||
"OK", # Device finished successfully
|
||||
]
|
||||
|
||||
ota.update()
|
||||
|
||||
# Verify socket was created and connected
|
||||
mock_socket_class.assert_called_once_with(
|
||||
socket.AF_INET, socket.SOCK_STREAM
|
||||
)
|
||||
mock_socket.settimeout.assert_called_once_with(15)
|
||||
mock_socket.connect.assert_called_once_with(("192.168.1.1", 3232))
|
||||
|
||||
# Verify start command was sent
|
||||
start_cmd = f"OTA {len(test_data)} {ota.hash_hex()}\n".encode("utf-8")
|
||||
mock_socket.sendall.assert_any_call(start_cmd)
|
||||
|
||||
# Verify firmware was sent (at least one chunk)
|
||||
assert mock_socket.sendall.call_count >= 2
|
||||
|
||||
# Verify socket was closed
|
||||
mock_socket.close.assert_called_once()
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_with_progress_callback(mock_socket_class):
|
||||
"""Test update() with progress callback"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
test_data = b"A" * 1024 # 1KB of data
|
||||
f.write(test_data)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
# Setup mock socket
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
# Track progress callback calls
|
||||
progress_calls = []
|
||||
|
||||
def progress_callback(sent, total):
|
||||
progress_calls.append((sent, total))
|
||||
|
||||
# Mock _read_line
|
||||
with patch.object(ota, "_read_line") as mock_read_line:
|
||||
mock_read_line.side_effect = [
|
||||
"OK", # Device ready
|
||||
"OK", # Device finished
|
||||
]
|
||||
|
||||
ota.update(progress_callback=progress_callback)
|
||||
|
||||
# Verify progress callback was called
|
||||
assert len(progress_calls) > 0
|
||||
# First call should show some progress
|
||||
assert progress_calls[0][0] > 0
|
||||
# Total should be the firmware size
|
||||
assert progress_calls[0][1] == len(test_data)
|
||||
# Last call should show all bytes sent
|
||||
assert progress_calls[-1][0] == len(test_data)
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_device_error_on_start(mock_socket_class):
|
||||
"""Test update() raises OTAError when device reports error during start"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
with patch.object(ota, "_read_line") as mock_read_line:
|
||||
mock_read_line.return_value = "ERR BAD_HASH"
|
||||
|
||||
with pytest.raises(OTAError, match="Device reported error"):
|
||||
ota.update()
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_device_error_on_finish(mock_socket_class):
|
||||
"""Test update() raises OTAError when device reports error after firmware sent"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
with patch.object(ota, "_read_line") as mock_read_line:
|
||||
mock_read_line.side_effect = [
|
||||
"OK", # Device ready
|
||||
"ERR FLASH_ERR", # Error after firmware sent
|
||||
]
|
||||
|
||||
with pytest.raises(OTAError, match="OTA update failed"):
|
||||
ota.update()
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_socket_cleanup_on_error(mock_socket_class):
|
||||
"""Test that socket is properly cleaned up on error"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
# Simulate connection error
|
||||
mock_socket.connect.side_effect = ConnectionRefusedError("Connection refused")
|
||||
|
||||
with pytest.raises(ConnectionRefusedError):
|
||||
ota.update()
|
||||
|
||||
# Verify socket was closed even on error
|
||||
mock_socket.close.assert_called_once()
|
||||
assert ota._socket is None
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_large_firmware(mock_socket_class):
|
||||
"""Test update() correctly chunks large firmware files"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
# Create a file larger than chunk_size (1024)
|
||||
test_data = b"B" * 3000
|
||||
f.write(test_data)
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
with patch.object(ota, "_read_line") as mock_read_line:
|
||||
mock_read_line.side_effect = [
|
||||
"OK", # Device ready
|
||||
"OK", # Device finished
|
||||
]
|
||||
|
||||
ota.update()
|
||||
|
||||
# Verify that all data was sent in chunks
|
||||
# 3000 bytes should be sent in ~3 chunks of 1024 bytes
|
||||
sendall_calls = [
|
||||
call
|
||||
for call in mock_socket.sendall.call_args_list
|
||||
if call[0][0]
|
||||
!= f"OTA {len(test_data)} {ota.hash_hex()}\n".encode("utf-8")
|
||||
]
|
||||
# Calculate total data sent (excluding the start command)
|
||||
total_sent = sum(len(call[0][0]) for call in sendall_calls)
|
||||
assert total_sent == len(test_data)
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_unexpected_response_warning(mock_socket_class, caplog):
|
||||
"""Test update() logs warning on unexpected response during startup"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
with patch.object(ota, "_read_line") as mock_read_line:
|
||||
mock_read_line.side_effect = [
|
||||
"UNKNOWN", # Unexpected response
|
||||
"OK", # Then proceed
|
||||
"OK", # Device finished
|
||||
]
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota.update()
|
||||
|
||||
# Check that warning was logged for unexpected response
|
||||
assert "Unexpected response" in caplog.text
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.ota.socket.socket")
|
||||
def test_esp32_wifi_ota_update_unexpected_final_response(mock_socket_class, caplog):
|
||||
"""Test update() logs warning on unexpected final response after firmware upload"""
|
||||
with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f:
|
||||
f.write(b"firmware")
|
||||
temp_file = f.name
|
||||
|
||||
try:
|
||||
mock_socket = MagicMock()
|
||||
mock_socket_class.return_value = mock_socket
|
||||
|
||||
ota = ESP32WiFiOTA(temp_file, "192.168.1.1")
|
||||
|
||||
with patch.object(ota, "_read_line") as mock_read_line:
|
||||
mock_read_line.side_effect = [
|
||||
"OK", # Device ready for firmware
|
||||
"UNKNOWN", # Unexpected final response (not OK, not ERR, not ACK)
|
||||
"OK", # Then succeed
|
||||
]
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
ota.update()
|
||||
|
||||
# Check that warning was logged for unexpected final response
|
||||
assert "Unexpected final response" in caplog.text
|
||||
|
||||
finally:
|
||||
os.unlink(temp_file)
|
||||
@@ -2,6 +2,7 @@
|
||||
# pylint: disable=R0917
|
||||
|
||||
import re
|
||||
import sys
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,16 +10,14 @@ import pytest
|
||||
from ..serial_interface import SerialInterface
|
||||
from ..protobuf import config_pb2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("time.sleep")
|
||||
@patch("termios.tcsetattr")
|
||||
@patch("termios.tcgetattr")
|
||||
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data="data")
|
||||
@patch("serial.Serial")
|
||||
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
|
||||
def test_SerialInterface_single_port(
|
||||
mocked_findPorts, mocked_serial, mocked_open, mock_get, mock_set, mock_sleep, capsys
|
||||
mocked_findPorts, mocked_serial, mocked_open, mock_hupcl, mock_sleep, capsys
|
||||
):
|
||||
"""Test that we can instantiate a SerialInterface with a single port"""
|
||||
iface = SerialInterface(noProto=True)
|
||||
@@ -28,9 +27,12 @@ def test_SerialInterface_single_port(
|
||||
iface.close()
|
||||
mocked_findPorts.assert_called()
|
||||
mocked_serial.assert_called()
|
||||
mocked_open.assert_called()
|
||||
mock_get.assert_called()
|
||||
mock_set.assert_called()
|
||||
|
||||
# doesn't get called in SerialInterface on windows
|
||||
if sys.platform != "win32":
|
||||
mocked_open.assert_called()
|
||||
mock_hupcl.assert_called()
|
||||
|
||||
mock_sleep.assert_called()
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"Nodes in mesh", out, re.MULTILINE)
|
||||
|
||||
221
meshtastic/tests/test_showNodes_favorite.py
Normal file
221
meshtastic/tests/test_showNodes_favorite.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""Meshtastic unit tests for showNodes favorite column feature"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from ..mesh_interface import MeshInterface
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _iface_with_favorite_nodes():
|
||||
"""Fixture to setup nodes with favorite flags."""
|
||||
nodesById = {
|
||||
"!9388f81c": {
|
||||
"num": 2475227164,
|
||||
"user": {
|
||||
"id": "!9388f81c",
|
||||
"longName": "Favorite Node",
|
||||
"shortName": "FAV1",
|
||||
"macaddr": "RBeTiPgc",
|
||||
"hwModel": "TBEAM",
|
||||
},
|
||||
"position": {},
|
||||
"lastHeard": 1640204888,
|
||||
"isFavorite": True,
|
||||
},
|
||||
"!12345678": {
|
||||
"num": 305419896,
|
||||
"user": {
|
||||
"id": "!12345678",
|
||||
"longName": "Regular Node",
|
||||
"shortName": "REG1",
|
||||
"macaddr": "ABCDEFGH",
|
||||
"hwModel": "TLORA_V2",
|
||||
},
|
||||
"position": {},
|
||||
"lastHeard": 1640204999,
|
||||
"isFavorite": False,
|
||||
},
|
||||
"!abcdef00": {
|
||||
"num": 2882400000,
|
||||
"user": {
|
||||
"id": "!abcdef00",
|
||||
"longName": "Legacy Node",
|
||||
"shortName": "LEG1",
|
||||
"macaddr": "XYZABC00",
|
||||
"hwModel": "HELTEC_V3",
|
||||
},
|
||||
"position": {},
|
||||
"lastHeard": 1640205000,
|
||||
# Note: No isFavorite field - testing backward compatibility
|
||||
},
|
||||
}
|
||||
|
||||
nodesByNum = {
|
||||
2475227164: {
|
||||
"num": 2475227164,
|
||||
"user": {
|
||||
"id": "!9388f81c",
|
||||
"longName": "Favorite Node",
|
||||
"shortName": "FAV1",
|
||||
"macaddr": "RBeTiPgc",
|
||||
"hwModel": "TBEAM",
|
||||
},
|
||||
"position": {"time": 1640206266},
|
||||
"lastHeard": 1640206266,
|
||||
"isFavorite": True,
|
||||
},
|
||||
305419896: {
|
||||
"num": 305419896,
|
||||
"user": {
|
||||
"id": "!12345678",
|
||||
"longName": "Regular Node",
|
||||
"shortName": "REG1",
|
||||
"macaddr": "ABCDEFGH",
|
||||
"hwModel": "TLORA_V2",
|
||||
},
|
||||
"position": {"time": 1640206200},
|
||||
"lastHeard": 1640206200,
|
||||
"isFavorite": False,
|
||||
},
|
||||
2882400000: {
|
||||
"num": 2882400000,
|
||||
"user": {
|
||||
"id": "!abcdef00",
|
||||
"longName": "Legacy Node",
|
||||
"shortName": "LEG1",
|
||||
"macaddr": "XYZABC00",
|
||||
"hwModel": "HELTEC_V3",
|
||||
},
|
||||
"position": {"time": 1640206100},
|
||||
"lastHeard": 1640206100,
|
||||
# Note: No isFavorite field - testing backward compatibility
|
||||
},
|
||||
}
|
||||
|
||||
iface = MeshInterface(noProto=True)
|
||||
iface.nodes = nodesById
|
||||
iface.nodesByNum = nodesByNum
|
||||
myInfo = MagicMock()
|
||||
iface.myInfo = myInfo
|
||||
iface.myInfo.my_node_num = 2475227164
|
||||
return iface
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_showNodes_favorite_column_header(capsys, _iface_with_favorite_nodes):
|
||||
"""Test that 'Fav' column header appears in showNodes output"""
|
||||
iface = _iface_with_favorite_nodes
|
||||
iface.showNodes()
|
||||
out, err = capsys.readouterr()
|
||||
assert "Fav" in out
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_showNodes_favorite_asterisk_display(capsys, _iface_with_favorite_nodes):
|
||||
"""Test that favorite nodes show asterisk and non-favorites show empty"""
|
||||
iface = _iface_with_favorite_nodes
|
||||
iface.showNodes()
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
# Check that the output contains the "Fav" column
|
||||
assert "Fav" in out
|
||||
|
||||
# Find lines containing our nodes
|
||||
lines = out.split('\n')
|
||||
favorite_line = None
|
||||
regular_line = None
|
||||
legacy_line = None
|
||||
for line in lines:
|
||||
if "Favorite Node" in line or "FAV1" in line:
|
||||
favorite_line = line
|
||||
if "Regular Node" in line or "REG1" in line:
|
||||
regular_line = line
|
||||
if "Legacy Node" in line or "LEG1" in line:
|
||||
legacy_line = line
|
||||
|
||||
# Verify all nodes are present in the output
|
||||
assert favorite_line is not None, "Favorite node should be in output"
|
||||
assert regular_line is not None, "Regular node should be in output"
|
||||
assert legacy_line is not None, "Legacy node should be in output"
|
||||
|
||||
# Verify the favorite node has an asterisk in its row
|
||||
assert "*" in favorite_line, "Favorite node should have an asterisk"
|
||||
|
||||
# Verify the regular (non-favorite) node does NOT have an asterisk
|
||||
assert regular_line.count("*") == 0, "Non-favorite node should not have an asterisk"
|
||||
|
||||
# Verify the legacy node (without isFavorite field) does NOT have an asterisk
|
||||
assert legacy_line.count("*") == 0, "Legacy node without isFavorite field should not have an asterisk"
|
||||
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_showNodes_favorite_field_formatting():
|
||||
"""Test the formatting logic for isFavorite field"""
|
||||
# Test favorite node
|
||||
raw_value = True
|
||||
formatted_value = "*" if raw_value else ""
|
||||
assert formatted_value == "*"
|
||||
|
||||
# Test non-favorite node
|
||||
raw_value = False
|
||||
formatted_value = "*" if raw_value else ""
|
||||
assert formatted_value == ""
|
||||
|
||||
# Test None/missing value
|
||||
raw_value = None
|
||||
formatted_value = "*" if raw_value else ""
|
||||
assert formatted_value == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_showNodes_with_custom_fields_including_favorite(capsys, _iface_with_favorite_nodes):
|
||||
"""Test that isFavorite can be specified in custom showFields"""
|
||||
iface = _iface_with_favorite_nodes
|
||||
custom_fields = ["user.longName", "isFavorite"]
|
||||
iface.showNodes(showFields=custom_fields)
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
# Should still show the Fav column when explicitly requested
|
||||
assert "Fav" in out
|
||||
assert err == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_showNodes_default_fields_includes_favorite(_iface_with_favorite_nodes):
|
||||
"""Test that isFavorite is included in default fields"""
|
||||
iface = _iface_with_favorite_nodes
|
||||
|
||||
# Call showNodes which uses default fields
|
||||
result = iface.showNodes()
|
||||
|
||||
# The result should contain the formatted table as a string
|
||||
assert "Fav" in result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_showNodes_backward_compatibility_missing_field(capsys, _iface_with_favorite_nodes):
|
||||
"""Test that nodes without isFavorite field are handled gracefully"""
|
||||
iface = _iface_with_favorite_nodes
|
||||
iface.showNodes()
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
# Find the legacy node line
|
||||
lines = out.split('\n')
|
||||
legacy_line = None
|
||||
for line in lines:
|
||||
if "Legacy Node" in line or "LEG1" in line:
|
||||
legacy_line = line
|
||||
break
|
||||
|
||||
# Verify the legacy node appears in output
|
||||
assert legacy_line is not None, "Legacy node without isFavorite field should appear in output"
|
||||
|
||||
# Verify it doesn't have an asterisk (should be treated as non-favorite)
|
||||
assert legacy_line.count("*") == 0, "Legacy node should not have asterisk (treated as non-favorite)"
|
||||
|
||||
assert err == ""
|
||||
@@ -15,7 +15,115 @@ def test_StreamInterface():
|
||||
"""Test that we cannot instantiate a StreamInterface based on noProto"""
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
StreamInterface()
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
assert pytest_wrapped_e.type == RuntimeError
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_close_safe_when_thread_never_started():
|
||||
"""close() must not raise RuntimeError when called before connect() has started the reader.
|
||||
|
||||
Hits the cleanup path used by __init__ when the handshake raises before the
|
||||
reader thread is started.
|
||||
"""
|
||||
iface = StreamInterface(noProto=True, connectNow=False)
|
||||
iface.stream = MagicMock()
|
||||
# _rxThread was created in __init__ but never .start()'d. close() should
|
||||
# detect that and skip join() instead of raising RuntimeError.
|
||||
iface.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_close_when_thread_never_started_closes_stream():
|
||||
"""If no reader thread was started, close() should still close the stream."""
|
||||
iface = StreamInterface(noProto=True, connectNow=False)
|
||||
stream = MagicMock()
|
||||
iface.stream = stream
|
||||
iface.close()
|
||||
stream.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_init_cleans_up_when_connect_raises():
|
||||
"""If connect() raises during __init__, close() runs and the original exception propagates."""
|
||||
|
||||
cleanup_calls = []
|
||||
|
||||
class FailingConnectStream(StreamInterface):
|
||||
"""Subclass whose connect() raises, to exercise the __init__ cleanup path."""
|
||||
|
||||
def __init__(self):
|
||||
self.stream = MagicMock() # bypass StreamInterface abstract check
|
||||
super().__init__(noProto=False, connectNow=True)
|
||||
|
||||
def connect(self):
|
||||
raise RuntimeError("simulated handshake failure")
|
||||
|
||||
def close(self):
|
||||
cleanup_calls.append("close")
|
||||
super().close()
|
||||
|
||||
with pytest.raises(RuntimeError, match="simulated handshake failure"):
|
||||
FailingConnectStream()
|
||||
assert cleanup_calls == ["close"], "close() should be invoked exactly once on handshake failure"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_init_cleans_up_when_waitForConfig_raises():
|
||||
"""If waitForConfig() raises after a successful connect(), close() runs and exception propagates."""
|
||||
|
||||
cleanup_calls = []
|
||||
|
||||
class FailingWaitStream(StreamInterface):
|
||||
"""Subclass whose waitForConfig() raises, to exercise the second leg of cleanup."""
|
||||
|
||||
def __init__(self):
|
||||
self.stream = MagicMock()
|
||||
super().__init__(noProto=False, connectNow=True)
|
||||
|
||||
def connect(self):
|
||||
# No-op connect — we are simulating handshake-stage failure, not connect-stage.
|
||||
pass
|
||||
|
||||
def waitForConfig(self):
|
||||
raise TimeoutError("simulated config-handshake timeout")
|
||||
|
||||
def close(self):
|
||||
cleanup_calls.append("close")
|
||||
super().close()
|
||||
|
||||
with pytest.raises(TimeoutError, match="simulated config-handshake timeout"):
|
||||
FailingWaitStream()
|
||||
assert cleanup_calls == ["close"], "close() should be invoked exactly once on handshake timeout"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_init_cleanup_does_not_shadow_original_exception():
|
||||
"""If close() itself raises during __init__ cleanup, the original exception still propagates.
|
||||
|
||||
The cleanup uses contextlib.suppress(Exception) so that a secondary failure
|
||||
in close() doesn't replace the real reason for the failed handshake.
|
||||
"""
|
||||
|
||||
class CleanupRaisesStream(StreamInterface):
|
||||
"""Helper stream that raises during close() to verify exception precedence."""
|
||||
|
||||
def __init__(self):
|
||||
self.stream = MagicMock()
|
||||
super().__init__(noProto=False, connectNow=True)
|
||||
|
||||
def connect(self):
|
||||
raise RuntimeError("original handshake failure")
|
||||
|
||||
def close(self):
|
||||
raise RuntimeError("secondary close failure — should be suppressed")
|
||||
|
||||
with pytest.raises(RuntimeError, match="original handshake failure"):
|
||||
CleanupRaisesStream()
|
||||
|
||||
|
||||
# Note: This takes a bit, so moving from unit to slow
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Meshtastic unit tests for tcp_interface.py"""
|
||||
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -54,3 +54,66 @@ def test_TCPInterface_without_connecting():
|
||||
with patch("socket.socket"):
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
assert iface.socket is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_close_shutdowns_socket_before_super_close():
|
||||
"""Close should unblock socket reads before waiting on StreamInterface.close()."""
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
sock = MagicMock()
|
||||
iface.socket = sock
|
||||
call_order = []
|
||||
|
||||
with patch.object(TCPInterface, "_socket_shutdown", autospec=True) as mock_shutdown:
|
||||
with patch(
|
||||
"meshtastic.stream_interface.StreamInterface.close", autospec=True
|
||||
) as mock_super_close:
|
||||
mock_shutdown.side_effect = lambda _self: call_order.append("shutdown")
|
||||
mock_super_close.side_effect = lambda _self: call_order.append("super_close")
|
||||
|
||||
iface.close()
|
||||
|
||||
assert call_order == ["shutdown", "super_close"]
|
||||
sock.close.assert_called_once()
|
||||
assert iface.socket is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_reconnect():
|
||||
"""Test that _reconnect correctly reconnects"""
|
||||
with patch("socket.socket") as mock_socket:
|
||||
with patch("time.sleep"):
|
||||
iface = TCPInterface(hostname="localhost", noProto=True)
|
||||
old_socket = iface.socket
|
||||
assert old_socket is not None
|
||||
|
||||
iface._reconnect()
|
||||
|
||||
assert old_socket.close.called
|
||||
# We expect socket class to be instantiated at least twice (init + reconnect)
|
||||
assert mock_socket.call_count >= 2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_writeBytes_reconnects():
|
||||
"""Test that _writeBytes reconnects and re-raises on OSError."""
|
||||
with patch("socket.socket"):
|
||||
iface = TCPInterface(hostname="localhost", noProto=True)
|
||||
iface.socket.sendall.side_effect = OSError("Broken pipe")
|
||||
|
||||
with patch.object(iface, "_reconnect") as mock_reconnect:
|
||||
with pytest.raises(OSError, match="Broken pipe"):
|
||||
iface._writeBytes(b"some data")
|
||||
mock_reconnect.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_readBytes_reconnects():
|
||||
"""Test that _readBytes calls _reconnect on empty bytes"""
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
iface.socket = MagicMock()
|
||||
iface.socket.recv.return_value = b""
|
||||
|
||||
with patch.object(iface, "_reconnect") as mock_reconnect:
|
||||
iface._readBytes(10)
|
||||
mock_reconnect.assert_called_once()
|
||||
|
||||
@@ -11,16 +11,19 @@ from hypothesis import given, strategies as st
|
||||
from meshtastic.supported_device import SupportedDevice
|
||||
from meshtastic.protobuf import mesh_pb2
|
||||
from meshtastic.util import (
|
||||
DEFAULT_KEY,
|
||||
Timeout,
|
||||
active_ports_on_supported_devices,
|
||||
camel_to_snake,
|
||||
catchAndIgnore,
|
||||
channel_hash,
|
||||
convert_mac_addr,
|
||||
eliminate_duplicate_port,
|
||||
findPorts,
|
||||
fixme,
|
||||
fromPSK,
|
||||
fromStr,
|
||||
generate_channel_hash,
|
||||
genPSK256,
|
||||
hexstr,
|
||||
ipstr,
|
||||
@@ -34,6 +37,7 @@ from meshtastic.util import (
|
||||
stripnl,
|
||||
support_info,
|
||||
message_to_json,
|
||||
to_node_num,
|
||||
Acknowledgment
|
||||
)
|
||||
|
||||
@@ -563,7 +567,7 @@ def test_active_ports_on_supported_devices_mac_duplicates_check(mock_platform, m
|
||||
def test_message_to_json_shows_all():
|
||||
"""Test that message_to_json prints fields that aren't included in data passed in"""
|
||||
actual = json.loads(message_to_json(mesh_pb2.MyNodeInfo()))
|
||||
expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0, "deviceId": "", "pioEnv": "" }
|
||||
expected = { "myNodeNum": 0, "rebootCount": 0, "minAppVersion": 0, "deviceId": "", "pioEnv": "", 'firmwareEdition': 'VANILLA', 'nodedbCount': 0 }
|
||||
assert actual == expected
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -670,3 +674,111 @@ def test_shorthex():
|
||||
assert result == b'\x05'
|
||||
result = fromStr('0xffff')
|
||||
assert result == b'\xff\xff'
|
||||
|
||||
def test_channel_hash_basics():
|
||||
"Test the default key and LongFast with channel_hash"
|
||||
assert channel_hash(DEFAULT_KEY) == 2
|
||||
assert channel_hash("LongFast".encode("utf-8")) == 10
|
||||
|
||||
@given(st.text(min_size=1, max_size=12))
|
||||
def test_channel_hash_fuzz(channel_name):
|
||||
"Test channel_hash with fuzzed channel names, ensuring it produces single-byte values"
|
||||
hashed = channel_hash(channel_name.encode("utf-8"))
|
||||
assert 0 <= hashed <= 0xFF
|
||||
|
||||
def test_generate_channel_hash_basics():
|
||||
"Test the default key and LongFast/MediumFast with generate_channel_hash"
|
||||
assert generate_channel_hash("LongFast", "AQ==") == 8
|
||||
assert generate_channel_hash("LongFast", bytes([1])) == 8
|
||||
assert generate_channel_hash("LongFast", DEFAULT_KEY) == 8
|
||||
assert generate_channel_hash("MediumFast", DEFAULT_KEY) == 31
|
||||
|
||||
@given(st.text(min_size=1, max_size=12))
|
||||
def test_generate_channel_hash_fuzz_default_key(channel_name):
|
||||
"Test generate_channel_hash with fuzzed channel names and the default key, ensuring it produces single-byte values"
|
||||
hashed = generate_channel_hash(channel_name, DEFAULT_KEY)
|
||||
assert 0 <= hashed <= 0xFF
|
||||
|
||||
@given(st.text(min_size=1, max_size=12), st.binary(min_size=1, max_size=1))
|
||||
def test_generate_channel_hash_fuzz_simple(channel_name, key_bytes):
|
||||
"Test generate_channel_hash with fuzzed channel names and one-byte keys, ensuring it produces single-byte values"
|
||||
hashed = generate_channel_hash(channel_name, key_bytes)
|
||||
assert 0 <= hashed <= 0xFF
|
||||
|
||||
@given(st.text(min_size=1, max_size=12), st.binary(min_size=16, max_size=16))
|
||||
def test_generate_channel_hash_fuzz_aes128(channel_name, key_bytes):
|
||||
"Test generate_channel_hash with fuzzed channel names and 128-bit keys, ensuring it produces single-byte values"
|
||||
hashed = generate_channel_hash(channel_name, key_bytes)
|
||||
assert 0 <= hashed <= 0xFF
|
||||
|
||||
@given(st.text(min_size=1, max_size=12), st.binary(min_size=32, max_size=32))
|
||||
def test_generate_channel_hash_fuzz_aes256(channel_name, key_bytes):
|
||||
"Test generate_channel_hash with fuzzed channel names and 256-bit keys, ensuring it produces single-byte values"
|
||||
hashed = generate_channel_hash(channel_name, key_bytes)
|
||||
assert 0 <= hashed <= 0xFF
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("input_val,expected", [
|
||||
# int passthrough
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(6, 6),
|
||||
(502009325, 502009325),
|
||||
(2198819370, 2198819370),
|
||||
(0xFFFFFFFF, 0xFFFFFFFF),
|
||||
# !hex format (always treated as hex)
|
||||
("!00000000", 0x00000000),
|
||||
("!00000001", 0x00000001),
|
||||
("!00000010", 0x00000010),
|
||||
("!000000ff", 0x000000FF),
|
||||
("!830f522a", 0x830F522A),
|
||||
("!1dec0ded", 0x1DEC0DED),
|
||||
("!ffffffff", 0xFFFFFFFF),
|
||||
("!FFFFFFFF", 0xFFFFFFFF),
|
||||
# 0xhex format
|
||||
("0x00000000", 0x00000000),
|
||||
("0x00000010", 0x00000010),
|
||||
("0x830f522a", 0x830F522A),
|
||||
("0x1dec0ded", 0x1DEC0DED),
|
||||
("0xFFFFFFFF", 0xFFFFFFFF),
|
||||
# Unprefixed hex string (falls back to hex when decimal fails)
|
||||
("830f522a", 0x830F522A),
|
||||
("1dec0ded", 0x1DEC0DED),
|
||||
# Decimal string
|
||||
("42", 42),
|
||||
("12345678", 12345678),
|
||||
("0", 0),
|
||||
("1", 1),
|
||||
# With whitespace
|
||||
(" !830f522a ", 2198819370),
|
||||
(" !00000010 ", 16),
|
||||
(" 0x830f522a ", 2198819370),
|
||||
])
|
||||
def test_to_node_num(input_val, expected):
|
||||
"""Test to_node_num with various valid inputs"""
|
||||
assert to_node_num(input_val) == expected
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("input_val", [
|
||||
"",
|
||||
"!",
|
||||
"!!",
|
||||
"!0x10",
|
||||
"!xyz",
|
||||
])
|
||||
def test_to_node_num_invalid(input_val):
|
||||
"""Test to_node_num raises ValueError for invalid inputs"""
|
||||
with pytest.raises(ValueError):
|
||||
to_node_num(input_val)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(st.integers(min_value=0, max_value=2**32 - 1))
|
||||
def test_to_node_num_hypothesis_roundtrip(n):
|
||||
"""Property: all supported input formats roundtrip for any valid node number"""
|
||||
assert to_node_num(n) == n
|
||||
assert to_node_num(f"!{n:08x}") == n
|
||||
assert to_node_num(f"0x{n:x}") == n
|
||||
assert to_node_num(str(n)) == n
|
||||
|
||||
@@ -26,10 +26,11 @@ from meshtastic.protobuf import portnums_pb2
|
||||
from meshtastic import mt_config
|
||||
from meshtastic.util import ipstr, readnet_u16
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def onTunnelReceive(packet, interface): # pylint: disable=W0613
|
||||
"""Callback for received tunneled messages from mesh."""
|
||||
logging.debug(f"in onTunnelReceive()")
|
||||
logger.debug(f"in onTunnelReceive()")
|
||||
tunnelInstance = mt_config.tunnelInstance
|
||||
tunnelInstance.onReceive(packet)
|
||||
|
||||
@@ -92,7 +93,7 @@ class Tunnel:
|
||||
self.LOG_TRACE = 5
|
||||
|
||||
# TODO: check if root?
|
||||
logging.info(
|
||||
logger.info(
|
||||
"Starting IP to mesh tunnel (you must be root for this *pre-alpha* "
|
||||
"feature to work). Mesh members:"
|
||||
)
|
||||
@@ -104,13 +105,13 @@ class Tunnel:
|
||||
for node in self.iface.nodes.values():
|
||||
nodeId = node["user"]["id"]
|
||||
ip = self._nodeNumToIp(node["num"])
|
||||
logging.info(f"Node { nodeId } has IP address { ip }")
|
||||
logger.info(f"Node { nodeId } has IP address { ip }")
|
||||
|
||||
logging.debug("creating TUN device with MTU=200")
|
||||
logger.debug("creating TUN device with MTU=200")
|
||||
# FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data
|
||||
self.tun = None
|
||||
if self.iface.noProto:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f"Not creating a TapDevice() because it is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
@@ -120,11 +121,11 @@ class Tunnel:
|
||||
|
||||
self._rxThread = None
|
||||
if self.iface.noProto:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f"Not starting TUN reader because it is disabled by noProto"
|
||||
)
|
||||
else:
|
||||
logging.debug(f"starting TUN reader, our IP address is {myAddr}")
|
||||
logger.debug(f"starting TUN reader, our IP address is {myAddr}")
|
||||
self._rxThread = threading.Thread(
|
||||
target=self.__tunReader, args=(), daemon=True
|
||||
)
|
||||
@@ -134,9 +135,9 @@ class Tunnel:
|
||||
"""onReceive"""
|
||||
p = packet["decoded"]["payload"]
|
||||
if packet["from"] == self.iface.myInfo.my_node_num:
|
||||
logging.debug("Ignoring message we sent")
|
||||
logger.debug("Ignoring message we sent")
|
||||
else:
|
||||
logging.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
|
||||
logger.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
|
||||
# we don't really need to check for filtering here (sender should have checked),
|
||||
# but this provides useful debug printing on types of packets received
|
||||
if not self.iface.noProto:
|
||||
@@ -152,7 +153,7 @@ class Tunnel:
|
||||
ignore = False # Assume we will be forwarding the packet
|
||||
if protocol in self.protocolBlacklist:
|
||||
ignore = True
|
||||
logging.log(
|
||||
logger.log(
|
||||
self.LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}"
|
||||
)
|
||||
elif protocol == 0x01: # ICMP
|
||||
@@ -160,7 +161,7 @@ class Tunnel:
|
||||
icmpCode = p[21]
|
||||
checksum = p[22:24]
|
||||
# pylint: disable=line-too-long
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}"
|
||||
)
|
||||
# reply to pings (swap src and dest but keep rest of packet unchanged)
|
||||
@@ -171,19 +172,19 @@ class Tunnel:
|
||||
destport = readnet_u16(p, subheader + 2)
|
||||
if destport in self.udpBlacklist:
|
||||
ignore = True
|
||||
logging.log(self.LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
|
||||
logger.log(self.LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
|
||||
else:
|
||||
logging.debug(f"forwarding udp srcport={srcport}, destport={destport}")
|
||||
logger.debug(f"forwarding udp srcport={srcport}, destport={destport}")
|
||||
elif protocol == 0x06: # TCP
|
||||
srcport = readnet_u16(p, subheader)
|
||||
destport = readnet_u16(p, subheader + 2)
|
||||
if destport in self.tcpBlacklist:
|
||||
ignore = True
|
||||
logging.log(self.LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
|
||||
logger.log(self.LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
|
||||
else:
|
||||
logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
|
||||
logger.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
|
||||
else:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f"forwarding unexpected protocol 0x{protocol:02x}, "
|
||||
"src={ipstr(srcaddr)}, dest={ipstr(destAddr)}"
|
||||
)
|
||||
@@ -192,10 +193,10 @@ class Tunnel:
|
||||
|
||||
def __tunReader(self):
|
||||
tap = self.tun
|
||||
logging.debug("TUN reader running")
|
||||
logger.debug("TUN reader running")
|
||||
while True:
|
||||
p = tap.read()
|
||||
# logging.debug(f"IP packet received on TUN interface, type={type(p)}")
|
||||
# logger.debug(f"IP packet received on TUN interface, type={type(p)}")
|
||||
destAddr = p[16:20]
|
||||
|
||||
if not self._shouldFilterPacket(p):
|
||||
@@ -210,7 +211,7 @@ class Tunnel:
|
||||
|
||||
for node in self.iface.nodes.values():
|
||||
nodeNum = node["num"] & 0xFFFF
|
||||
# logging.debug(f"Considering nodenum 0x{nodeNum:x} for ipBits 0x{ipBits:x}")
|
||||
# logger.debug(f"Considering nodenum 0x{nodeNum:x} for ipBits 0x{ipBits:x}")
|
||||
if (nodeNum) == ipBits:
|
||||
return node["user"]["id"]
|
||||
return None
|
||||
@@ -222,12 +223,12 @@ class Tunnel:
|
||||
"""Forward the provided IP packet into the mesh"""
|
||||
nodeId = self._ipToNodeId(destAddr)
|
||||
if nodeId is not None:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}"
|
||||
)
|
||||
self.iface.sendData(p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck=False)
|
||||
else:
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
f"Dropping packet because no node found for destIP={ipstr(destAddr)}"
|
||||
)
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ blacklistVids: Dict = dict.fromkeys([0x1366, 0x0483, 0x1915, 0x0925, 0x04b4])
|
||||
0x303a Heltec tracker"""
|
||||
whitelistVids = dict.fromkeys([0x239a, 0x303a])
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_KEY = base64.b64decode("1PG7OiApB1nwvP+rz05pAQ==".encode("utf-8"))
|
||||
|
||||
def quoteBooleans(a_string: str) -> str:
|
||||
"""Quote booleans
|
||||
@@ -141,7 +144,7 @@ def catchAndIgnore(reason: str, closure) -> None:
|
||||
try:
|
||||
closure()
|
||||
except BaseException as ex:
|
||||
logging.error(f"Exception thrown in {reason}: {ex}")
|
||||
logger.error(f"Exception thrown in {reason}: {ex}")
|
||||
|
||||
|
||||
def findPorts(eliminate_duplicates: bool=False) -> List[str]:
|
||||
@@ -198,9 +201,9 @@ class Timeout:
|
||||
self.sleepInterval: float = 0.1
|
||||
self.expireTimeout: int = maxSecs
|
||||
|
||||
def reset(self) -> None:
|
||||
def reset(self, expireTimeout=None):
|
||||
"""Restart the waitForSet timer"""
|
||||
self.expireTime = time.time() + self.expireTimeout
|
||||
self.expireTime = time.time() + (self.expireTimeout if expireTimeout is None else expireTimeout)
|
||||
|
||||
def waitForSet(self, target, attrs=()) -> bool:
|
||||
"""Block until the specified attributes are set. Returns True if config has been received."""
|
||||
@@ -225,8 +228,7 @@ class Timeout:
|
||||
|
||||
def waitForTraceRoute(self, waitFactor, acknowledgment, attr="receivedTraceRoute") -> bool:
|
||||
"""Block until traceroute response is received. Returns True if traceroute response has been received."""
|
||||
self.expireTimeout *= waitFactor
|
||||
self.reset()
|
||||
self.reset(self.expireTimeout * waitFactor)
|
||||
while time.time() < self.expireTime:
|
||||
if getattr(acknowledgment, attr, None):
|
||||
acknowledgment.reset()
|
||||
@@ -308,7 +310,7 @@ class DeferredExecution:
|
||||
o = self.queue.get()
|
||||
o()
|
||||
except:
|
||||
logging.error(
|
||||
logger.error(
|
||||
f"Unexpected error in deferred execution {sys.exc_info()[0]}"
|
||||
)
|
||||
print(traceback.format_exc())
|
||||
@@ -365,6 +367,30 @@ def remove_keys_from_dict(keys: Union[Tuple, List, Set], adict: Dict) -> Dict:
|
||||
remove_keys_from_dict(keys, val)
|
||||
return adict
|
||||
|
||||
def channel_hash(data: bytes) -> int:
|
||||
"""Compute an XOR hash from bytes for channel evaluation."""
|
||||
result = 0
|
||||
for char in data:
|
||||
result ^= char
|
||||
return result
|
||||
|
||||
def generate_channel_hash(name: Union[str, bytes], key: Union[str, bytes]) -> int:
|
||||
"""generate the channel number by hashing the channel name and psk (accepts str or bytes for both)"""
|
||||
# Handle key as str or bytes
|
||||
if isinstance(key, str):
|
||||
key = base64.b64decode(key.replace("-", "+").replace("_", "/").encode("utf-8"))
|
||||
|
||||
if len(key) == 1:
|
||||
key = DEFAULT_KEY[:-1] + key
|
||||
|
||||
# Handle name as str or bytes
|
||||
if isinstance(name, str):
|
||||
name = name.encode("utf-8")
|
||||
|
||||
h_name = channel_hash(name)
|
||||
h_key = channel_hash(key)
|
||||
result: int = h_name ^ h_key
|
||||
return result
|
||||
|
||||
def hexstr(barray: bytes) -> str:
|
||||
"""Print a string of hex digits"""
|
||||
@@ -692,3 +718,33 @@ def message_to_json(message: Message, multiline: bool=False) -> str:
|
||||
except TypeError:
|
||||
json = MessageToJson(message, including_default_value_fields=True) # type: ignore[call-arg] # pylint: disable=E1123
|
||||
return stripnl(json) if not multiline else json
|
||||
|
||||
|
||||
def to_node_num(node_id: Union[int, str]) -> int:
|
||||
"""
|
||||
Normalize a node id from int | '!hex' | '0xhex' | 'decimal' to int.
|
||||
"""
|
||||
if isinstance(node_id, int):
|
||||
return node_id
|
||||
s = str(node_id).strip()
|
||||
if s.startswith("!"):
|
||||
s = "0x" + s[1:]
|
||||
if s.lower().startswith("0x"):
|
||||
return int(s, 16)
|
||||
try:
|
||||
return int(s, 10)
|
||||
except ValueError:
|
||||
return int(s, 16)
|
||||
|
||||
def flags_to_list(flag_type, flags: int) -> List[str]:
|
||||
"""Given a flag_type that's a protobuf EnumTypeWrapper, and a flag int, give a list of flags enabled."""
|
||||
ret = []
|
||||
for key in flag_type.keys():
|
||||
if key == "EXCLUDED_NONE":
|
||||
continue
|
||||
if flags & flag_type.Value(key):
|
||||
ret.append(key)
|
||||
flags = flags - flag_type.Value(key)
|
||||
if flags > 0:
|
||||
ret.append(f"UNKNOWN_ADDITIONAL_FLAGS({flags})")
|
||||
return ret
|
||||
|
||||
5127
poetry.lock
generated
5127
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
Submodule protobufs updated: f00e96f12d...dd6c3f850a
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user