mirror of
https://github.com/meshtastic/python.git
synced 2026-07-31 17:16:45 -04:00
Compare commits
298 Commits
revert-753
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d94ae4067a | ||
|
|
ae710478fb | ||
|
|
3b084d7df2 | ||
|
|
3f62c099ae | ||
|
|
a509bca86d | ||
|
|
dca0b1fced | ||
|
|
11b71e9587 | ||
|
|
19d066970e | ||
|
|
485d860911 | ||
|
|
e6f07123f6 | ||
|
|
f623732694 | ||
|
|
82220ba49b | ||
|
|
4339cda6bd | ||
|
|
5760fb3ad9 | ||
|
|
2281ea5b64 | ||
|
|
7950798378 | ||
|
|
bffaff1c71 | ||
|
|
ceca7b3b10 | ||
|
|
15438c8a6e | ||
|
|
2f9b285dd5 | ||
|
|
9b892711df | ||
|
|
1cd1aed33c | ||
|
|
ab14fb2a86 | ||
|
|
7a7353ca15 | ||
|
|
e2844cb8d6 | ||
|
|
441c6e80a1 | ||
|
|
b05e545396 | ||
|
|
b6a13d58fe | ||
|
|
8c5efb926c | ||
|
|
6d76edf8a7 | ||
|
|
74cce6bab9 | ||
|
|
1309f4f344 | ||
|
|
fa01cb7a5d | ||
|
|
dcdbda0524 | ||
|
|
9479fb2a77 | ||
|
|
990ecc2350 | ||
|
|
7fc69e957c | ||
|
|
f0de977be5 | ||
|
|
c118334933 | ||
|
|
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 |
16
.coderabbit.yaml
Normal file
16
.coderabbit.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
language: en-US
|
||||
reviews:
|
||||
profile: chill
|
||||
high_level_summary: true
|
||||
poem: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
ignore_usernames:
|
||||
- renovate
|
||||
- renovate[bot]
|
||||
abort_on_close: true
|
||||
|
||||
path_filters:
|
||||
- "!/protobufs/**"
|
||||
- "!/meshtastic/protobuf/**"
|
||||
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
|
||||
71
.github/workflows/ci.yml
vendored
71
.github/workflows/ci.yml
vendored
@@ -1,4 +1,7 @@
|
||||
name: CI
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -8,7 +11,29 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.12"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install meshtastic from local
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install poetry
|
||||
poetry install --all-extras --with dev,powermon
|
||||
poetry run meshtastic --version
|
||||
|
||||
build:
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -17,10 +42,14 @@ jobs:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python 3
|
||||
- name: Install Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Uninstall meshtastic
|
||||
run: |
|
||||
pip3 uninstall -y meshtastic
|
||||
@@ -32,15 +61,8 @@ jobs:
|
||||
run: |
|
||||
poetry install --all-extras --with dev,powermon
|
||||
poetry run meshtastic --version
|
||||
- name: Run pylint
|
||||
run: poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"
|
||||
- name: Check types with mypy
|
||||
run: poetry run mypy meshtastic/
|
||||
- name: Run tests with pytest
|
||||
run: poetry run pytest --cov=meshtastic
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
poetry run pytest --cov=meshtastic --cov-report=xml
|
||||
- name: Run lint, type check, and tests in parallel
|
||||
run: make -j3 --output-sync=target ci
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
@@ -51,22 +73,37 @@ jobs:
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
validate:
|
||||
|
||||
simradio_testing:
|
||||
needs: [validate, build]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
channel:
|
||||
- beta
|
||||
- alpha
|
||||
- daily
|
||||
continue-on-error: ${{ matrix.channel == 'daily' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Python 3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install meshtastic from local
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install poetry
|
||||
poetry install
|
||||
poetry install --all-extras --with dev
|
||||
poetry run meshtastic --version
|
||||
- name: Install meshtasticd (${{ matrix.channel }}) from PPA
|
||||
run: |
|
||||
sudo add-apt-repository -y ppa:meshtastic/${{ matrix.channel }}
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y meshtasticd
|
||||
- name: Run firmware smoke tests
|
||||
run: poetry run pytest -m "smokevirt or smokemesh" -v
|
||||
timeout-minutes: 15
|
||||
|
||||
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
|
||||
36
.github/workflows/daily_simradio.yml
vendored
Normal file
36
.github/workflows/daily_simradio.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Daily SimRadio Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 6 * * *
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
simradio_testing:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'meshtastic/python'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Python 3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install meshtastic from local
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install poetry
|
||||
poetry install --all-extras --with dev
|
||||
poetry run meshtastic --version
|
||||
- name: Install meshtasticd (daily) from PPA
|
||||
run: |
|
||||
sudo add-apt-repository -y ppa:meshtastic/daily
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y meshtasticd
|
||||
- name: Run firmware smoke tests
|
||||
run: poetry run pytest -m "smokevirt or smokemesh" -v
|
||||
timeout-minutes: 15
|
||||
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
|
||||
14
Makefile
14
Makefile
@@ -42,5 +42,19 @@ cov:
|
||||
examples: FORCE
|
||||
pytest -mexamples
|
||||
|
||||
# CI targets (run via poetry, executed in parallel by the CI workflow)
|
||||
.PHONY: ci-pylint ci-mypy ci-test ci
|
||||
|
||||
ci-pylint:
|
||||
poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$$"
|
||||
|
||||
ci-mypy:
|
||||
poetry run mypy meshtastic/
|
||||
|
||||
ci-test:
|
||||
poetry run pytest --cov=meshtastic --cov-report=xml
|
||||
|
||||
ci: ci-pylint ci-mypy ci-test
|
||||
|
||||
# Makefile hack to get the examples to always run
|
||||
FORCE: ;
|
||||
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -253,12 +287,22 @@ class Node:
|
||||
# for sending admin channels will also change
|
||||
adminIndex = self.iface.localNode._getAdminChannelIndex()
|
||||
|
||||
# Snapshot serialized channel payloads from channelIndex onward so we
|
||||
# can avoid writing slots whose protobuf content did not change after
|
||||
# the shift. Use bytes (not message objects), because _fixupChannels()
|
||||
# mutates message fields in-place.
|
||||
old_channels = [
|
||||
self.channels[i].SerializeToString()
|
||||
for i in range(channelIndex, len(self.channels))
|
||||
]
|
||||
|
||||
self.channels.pop(channelIndex)
|
||||
self._fixupChannels() # expand back to 8 channels
|
||||
|
||||
index = channelIndex
|
||||
while index < 8:
|
||||
self.writeChannel(index, adminIndex=adminIndex)
|
||||
for old_ch in old_channels:
|
||||
if self.channels[index].SerializeToString() != old_ch:
|
||||
self.writeChannel(index, adminIndex=adminIndex)
|
||||
index += 1
|
||||
|
||||
# if we are updating the local node, we might end up
|
||||
@@ -289,28 +333,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 +390,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 +486,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 +495,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 +536,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 +556,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 +589,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 +599,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 +612,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 +635,7 @@ class Node:
|
||||
while self.gotResponse is False:
|
||||
time.sleep(0.1)
|
||||
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"self.cannedPluginMessageMessages:{self.cannedPluginMessageMessages}"
|
||||
)
|
||||
|
||||
@@ -515,12 +643,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 +670,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 +684,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 +693,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 +707,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 +721,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 +731,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 +744,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 +779,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 +792,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 +804,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 +820,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 +834,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 +848,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 +862,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 +876,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 +892,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 +933,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 +948,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 +980,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 +992,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 +1035,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 +1081,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 +1102,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 +1110,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 +1128,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
1190
meshtastic/protobuf/mesh_pb2.pyi
generated
1190
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,6 +4,7 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
@@ -11,6 +12,13 @@ from meshtastic.stream_interface import StreamInterface
|
||||
|
||||
DEFAULT_TCP_PORT = 4403
|
||||
|
||||
# How long close() gives the device to consume what we last wrote before forcing
|
||||
# the connection down. See close() for why this exists.
|
||||
GRACEFUL_CLOSE_TIMEOUT = 0.25
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TCPInterface(StreamInterface):
|
||||
"""Interface class for meshtastic devices over a TCP link"""
|
||||
|
||||
@@ -18,30 +26,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 +74,70 @@ 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 _wait_for_reader_exit(self, timeout: float) -> None:
|
||||
"""Wait briefly for the reader thread to drain and exit after a half-close.
|
||||
|
||||
Returns as soon as it exits, or after timeout: the device is not obliged
|
||||
to close just because we did.
|
||||
"""
|
||||
rx = getattr(self, "_rxThread", None)
|
||||
if rx is None or rx is threading.current_thread():
|
||||
return
|
||||
with contextlib.suppress(Exception):
|
||||
rx.join(timeout)
|
||||
|
||||
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
|
||||
# Half-close first. shutdown(SHUT_WR) sends FIN, which tells the
|
||||
# device we are done writing and lets it consume what we last wrote
|
||||
# before the connection goes away -- typically the admin message a
|
||||
# one-shot command such as `--set` just sent.
|
||||
#
|
||||
# Going straight to shutdown(SHUT_RDWR) + close() while either side
|
||||
# still has unread data makes the stack send RST instead. Winsock
|
||||
# then discards data the peer had already received but not yet read,
|
||||
# so the write is lost; Linux delivers it before reporting
|
||||
# ECONNRESET, which is why this only bites on Windows.
|
||||
with contextlib.suppress(Exception):
|
||||
self.socket.shutdown(socket.SHUT_WR)
|
||||
self._wait_for_reader_exit(GRACEFUL_CLOSE_TIMEOUT)
|
||||
|
||||
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 +145,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()
|
||||
|
||||
@@ -8,6 +8,87 @@ import pytest
|
||||
from meshtastic import mt_config
|
||||
|
||||
from ..mesh_interface import MeshInterface
|
||||
from .firmware_harness import (
|
||||
CHAIN_TOPOLOGY,
|
||||
DEFAULT_BASE_PORT,
|
||||
SimMesh,
|
||||
find_meshtasticd,
|
||||
is_compatible_host,
|
||||
)
|
||||
from .fw_helpers import set_region
|
||||
|
||||
# Use a different base port for the single-node fixture so it doesn't
|
||||
# conflict with the multi-node mesh fixture.
|
||||
SINGLE_NODE_BASE_PORT = DEFAULT_BASE_PORT + 100
|
||||
|
||||
|
||||
def _skip_firmware_if_unavailable() -> None:
|
||||
"""Skip the test when meshtasticd can't run on this host."""
|
||||
if not is_compatible_host():
|
||||
pytest.skip("meshtasticd firmware tests require Linux")
|
||||
if find_meshtasticd() is None:
|
||||
pytest.skip(
|
||||
"meshtasticd not found — set MESHTASTICD_BIN or install it on PATH"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def firmware_node():
|
||||
"""A single meshtasticd sim node for smokevirt tests.
|
||||
|
||||
Function-scoped so every test gets a freshly-erased node with no
|
||||
state leaking from previous tests. This makes destructive commands
|
||||
(``--reboot``, ``--set factory_reset true``) safe to run and lets
|
||||
tests be order-independent.
|
||||
|
||||
Yields the SimNode instance. The node is booted with a fresh erased
|
||||
config and listens on localhost at its TCP port. Region is set to US
|
||||
so modem-preset tests work against firmware >= 2.8, which clamps
|
||||
presets to the legal set for the current region.
|
||||
"""
|
||||
_skip_firmware_if_unavailable()
|
||||
mesh = SimMesh(n_nodes=1, base_port=SINGLE_NODE_BASE_PORT)
|
||||
mesh.start()
|
||||
node = mesh.get_node(0)
|
||||
set_region(node.port, "US")
|
||||
# The region commit restarts the TCP listener, so reconnect the harness
|
||||
# interface in case a test wants to use it directly.
|
||||
if node.iface is not None:
|
||||
try:
|
||||
node.iface.close()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
node.connect()
|
||||
yield node
|
||||
mesh.stop()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def firmware_mesh():
|
||||
"""A 3-node chain (A-B-C) meshtasticd sim mesh for smokemesh tests.
|
||||
|
||||
Yields the SimMesh instance. Nodes are connected and the SIMULATOR_APP
|
||||
packet bridge is running. Region is set to US for firmware >= 2.8
|
||||
compatibility, interfaces are reconnected after the region change, and
|
||||
node DB convergence is awaited.
|
||||
"""
|
||||
_skip_firmware_if_unavailable()
|
||||
mesh = SimMesh(n_nodes=3, topology=CHAIN_TOPOLOGY)
|
||||
mesh.start()
|
||||
for node in mesh.nodes:
|
||||
set_region(node.port, "US")
|
||||
# The region commit restarts each node's TCP listener, so reconnect the
|
||||
# harness interfaces before waiting for convergence.
|
||||
for node in mesh.nodes:
|
||||
if node.iface is not None:
|
||||
try:
|
||||
node.iface.close()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
node.connect()
|
||||
mesh.wait_for_convergence(timeout=30)
|
||||
yield mesh
|
||||
mesh.stop()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
357
meshtastic/tests/firmware_harness.py
Normal file
357
meshtastic/tests/firmware_harness.py
Normal file
@@ -0,0 +1,357 @@
|
||||
"""Test harness for running real meshtasticd firmware instances.
|
||||
|
||||
Launches one or more meshtasticd processes in simulator mode (-s) and bridges
|
||||
their "over-the-air" packets via the SIMULATOR_APP protocol so that multiple
|
||||
instances can communicate as if via LoRa, with a configurable topology.
|
||||
|
||||
The harness expects meshtasticd to be available on PATH or via the
|
||||
MESHTASTICD_BIN environment variable. It does not download or build the
|
||||
binary itself.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
from pubsub import pub # type: ignore[import-untyped]
|
||||
|
||||
from meshtastic import BROADCAST_NUM, mesh_pb2, portnums_pb2
|
||||
from meshtastic.tcp_interface import TCPInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HW_ID_OFFSET = 16
|
||||
DEFAULT_BASE_PORT = 4404
|
||||
DEFAULT_RSSI = -50
|
||||
DEFAULT_SNR = 10.0
|
||||
BOOT_TIMEOUT = 30
|
||||
CONNECT_TIMEOUT = 30
|
||||
|
||||
CHAIN_TOPOLOGY: Dict[int, Set[int]] = {
|
||||
0: {1},
|
||||
1: {0, 2},
|
||||
2: {1},
|
||||
}
|
||||
|
||||
|
||||
def find_meshtasticd() -> Optional[str]:
|
||||
"""Return the path to the meshtasticd binary, or None if not found."""
|
||||
env_path = os.environ.get("MESHTASTICD_BIN")
|
||||
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
|
||||
return env_path
|
||||
return shutil.which("meshtasticd")
|
||||
|
||||
|
||||
def is_compatible_host() -> bool:
|
||||
"""True when the host can run meshtasticd natively (Linux only)."""
|
||||
return platform.system() == "Linux"
|
||||
|
||||
|
||||
def _wait_for_port(port: int, timeout: int = BOOT_TIMEOUT) -> bool:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
s = socket.create_connection(("localhost", port), timeout=0.5)
|
||||
s.close()
|
||||
return True
|
||||
except OSError:
|
||||
time.sleep(0.5)
|
||||
return False
|
||||
|
||||
|
||||
class SimNode:
|
||||
"""A single meshtasticd simulator instance."""
|
||||
|
||||
def __init__(self, node_id: int, base_port: int = DEFAULT_BASE_PORT):
|
||||
self.node_id = node_id
|
||||
self.hw_id = node_id + HW_ID_OFFSET
|
||||
self.port = base_port + node_id
|
||||
self.process: Optional[subprocess.Popen] = None
|
||||
self.workdir: Optional[str] = None
|
||||
self.iface: Optional[TCPInterface] = None
|
||||
self._log_files: list = []
|
||||
|
||||
@property
|
||||
def node_num(self) -> int:
|
||||
"""The firmware-assigned node number (== hw_id)."""
|
||||
if self.iface and self.iface.myInfo:
|
||||
return self.iface.myInfo.my_node_num
|
||||
return self.hw_id
|
||||
|
||||
def start(self, binary: str) -> None:
|
||||
"""Launch the meshtasticd process in simulator mode."""
|
||||
self.workdir = tempfile.mkdtemp(prefix=f"mtd_node{self.node_id}_")
|
||||
vfs_dir = os.path.join(self.workdir, "vfs")
|
||||
os.mkdir(vfs_dir)
|
||||
# Files are closed in _kill(); keep them open for the process lifetime.
|
||||
log_stdout = open( # pylint: disable=consider-using-with
|
||||
os.path.join(self.workdir, "meshtasticd.log"), "wb", buffering=0
|
||||
)
|
||||
log_stderr = open( # pylint: disable=consider-using-with
|
||||
os.path.join(self.workdir, "meshtasticd.err"), "wb", buffering=0
|
||||
)
|
||||
self._log_files = [log_stdout, log_stderr]
|
||||
self.process = subprocess.Popen( # pylint: disable=consider-using-with
|
||||
[
|
||||
binary,
|
||||
"-s",
|
||||
"-h", str(self.hw_id),
|
||||
"-p", str(self.port),
|
||||
"-d", vfs_dir,
|
||||
"-e",
|
||||
],
|
||||
stdout=log_stdout,
|
||||
stderr=log_stderr,
|
||||
start_new_session=True,
|
||||
)
|
||||
if not _wait_for_port(self.port):
|
||||
self._kill()
|
||||
raise RuntimeError(
|
||||
f"meshtasticd node {self.node_id} did not start listening on port {self.port}"
|
||||
)
|
||||
def connect(self) -> None:
|
||||
"""Open a TCPInterface connection to this node."""
|
||||
self.iface = TCPInterface(
|
||||
hostname="localhost",
|
||||
portNumber=self.port,
|
||||
connectNow=False,
|
||||
)
|
||||
self.iface.myConnect()
|
||||
self.iface.connect()
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the interface and kill the process."""
|
||||
if self.iface is not None:
|
||||
try:
|
||||
self.iface.localNode.exitSimulator()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.iface.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.iface = None
|
||||
self._kill()
|
||||
if self.workdir:
|
||||
shutil.rmtree(self.workdir, ignore_errors=True)
|
||||
self.workdir = None
|
||||
|
||||
def _kill(self) -> None:
|
||||
for f in self._log_files:
|
||||
try:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._log_files = []
|
||||
if self.process is not None:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||
except (ProcessLookupError, OSError):
|
||||
pass
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except Exception:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
||||
except Exception:
|
||||
pass
|
||||
# Give OS time to release TCP port (avoid TIME_WAIT preventing
|
||||
# next instance from binding the same port)
|
||||
time.sleep(1.0)
|
||||
self.process = None
|
||||
|
||||
|
||||
class SimMesh:
|
||||
"""Manages N meshtasticd sim instances and bridges their SIMULATOR_APP packets.
|
||||
|
||||
When *topology* is None every node hears every other node (full mesh).
|
||||
Otherwise *topology* maps a transmitter's node index to the set of receiver
|
||||
node indices that can hear it.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
n_nodes: int = 1,
|
||||
topology: Optional[Dict[int, Set[int]]] = None,
|
||||
base_port: int = DEFAULT_BASE_PORT,
|
||||
):
|
||||
self.n_nodes = n_nodes
|
||||
self.topology = topology
|
||||
self.base_port = base_port
|
||||
self.nodes: List[SimNode] = [
|
||||
SimNode(i, base_port) for i in range(n_nodes)
|
||||
]
|
||||
self._port_to_idx: Dict[int, int] = {}
|
||||
self._started = False
|
||||
|
||||
def start(self) -> None:
|
||||
"""Launch all nodes, connect, and start the packet bridge."""
|
||||
binary = find_meshtasticd()
|
||||
if binary is None:
|
||||
raise RuntimeError(
|
||||
"meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH."
|
||||
)
|
||||
|
||||
for node in self.nodes:
|
||||
node.start(binary)
|
||||
|
||||
for node in self.nodes:
|
||||
node.connect()
|
||||
self._port_to_idx[node.port] = node.node_id
|
||||
|
||||
pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
|
||||
self._started = True
|
||||
|
||||
if self.n_nodes > 1:
|
||||
self._trigger_convergence()
|
||||
|
||||
def _trigger_convergence(self) -> None:
|
||||
"""Actively trigger NodeInfo exchange instead of waiting passively.
|
||||
|
||||
Sends a NODEINFO_APP packet with wantResponse from each node so the
|
||||
firmware's NodeInfoModule responds with its own user info, populating
|
||||
all node DBs deterministically.
|
||||
"""
|
||||
for node in self.nodes:
|
||||
iface = node.iface
|
||||
if iface is None:
|
||||
continue
|
||||
user = mesh_pb2.User()
|
||||
user.id = f"!{node.node_num:08x}"
|
||||
user.long_name = f"Node {node.node_id}"
|
||||
user.short_name = f"{node.node_id:04d}"[:4]
|
||||
user.hw_model = mesh_pb2.HardwareModel.PORTDUINO
|
||||
try:
|
||||
iface.sendData(
|
||||
user,
|
||||
destinationId=BROADCAST_NUM,
|
||||
portNum=portnums_pb2.PortNum.NODEINFO_APP,
|
||||
wantAck=False,
|
||||
wantResponse=True,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.debug("NodeInfo trigger for node %d failed: %s", node.node_id, ex)
|
||||
time.sleep(5)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Shut down all nodes and clean up."""
|
||||
if not self._started:
|
||||
return
|
||||
try:
|
||||
pub.unsubscribe(self._on_sim_packet, "meshtastic.receive.simulator")
|
||||
except Exception:
|
||||
pass
|
||||
for node in self.nodes:
|
||||
node.close()
|
||||
self._started = False
|
||||
|
||||
def get_node(self, idx: int) -> SimNode:
|
||||
"""Return the SimNode at the given index."""
|
||||
return self.nodes[idx]
|
||||
|
||||
def get_iface(self, idx: int) -> TCPInterface:
|
||||
"""Return the TCPInterface for the node at the given index."""
|
||||
iface = self.nodes[idx].iface
|
||||
assert iface is not None, f"node {idx} has no interface"
|
||||
return iface
|
||||
|
||||
def wait_for_convergence(self, timeout: int = 30) -> bool:
|
||||
"""Wait until every node sees all others in its node DB.
|
||||
|
||||
Returns True if converged, False if timed out (non-fatal — the packet
|
||||
bridge forwards regardless of node DB state).
|
||||
"""
|
||||
if self.n_nodes <= 1:
|
||||
return True
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if all(
|
||||
node.iface is not None
|
||||
and node.iface.nodes is not None
|
||||
and len(node.iface.nodes) >= self.n_nodes
|
||||
for node in self.nodes
|
||||
):
|
||||
return True
|
||||
time.sleep(2)
|
||||
logger.warning("Mesh did not fully converge within %ds", timeout)
|
||||
return False
|
||||
|
||||
def _get_receivers(self, tx_idx: int) -> List[int]:
|
||||
"""Return node indices that can hear a transmission from *tx_idx*."""
|
||||
if self.topology is not None:
|
||||
return sorted(self.topology.get(tx_idx, set()))
|
||||
return [i for i in range(self.n_nodes) if i != tx_idx]
|
||||
|
||||
def _on_sim_packet(self, interface, packet) -> None:
|
||||
"""Bridge callback: forward a SIMULATOR_APP packet to receiving nodes."""
|
||||
tx_port = getattr(interface, "portNumber", None)
|
||||
tx_idx = self._port_to_idx.get(tx_port) if tx_port else None
|
||||
if tx_idx is None:
|
||||
return
|
||||
|
||||
rx_indices = self._get_receivers(tx_idx)
|
||||
if not rx_indices:
|
||||
return
|
||||
|
||||
data = packet["decoded"]["payload"]
|
||||
if hasattr(data, "SerializeToString"):
|
||||
data = data.SerializeToString()
|
||||
|
||||
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
|
||||
logger.warning("Simulator payload too big (%d bytes), dropping", len(data))
|
||||
return
|
||||
|
||||
mesh_packet = _build_mesh_packet(packet, data)
|
||||
|
||||
for rx_idx in rx_indices:
|
||||
rx_iface = self.nodes[rx_idx].iface
|
||||
if rx_iface is None:
|
||||
continue
|
||||
mesh_packet.rx_rssi = DEFAULT_RSSI
|
||||
mesh_packet.rx_snr = DEFAULT_SNR
|
||||
to_radio = mesh_pb2.ToRadio()
|
||||
to_radio.packet.CopyFrom(mesh_packet)
|
||||
try:
|
||||
rx_iface._sendToRadio(to_radio)
|
||||
except Exception as ex:
|
||||
logger.error("Error forwarding packet to node %d: %s", rx_idx, ex)
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.stop()
|
||||
|
||||
|
||||
def _build_mesh_packet(packet: dict, data: bytes) -> mesh_pb2.MeshPacket:
|
||||
"""Reconstruct a MeshPacket for SIMULATOR_APP injection."""
|
||||
mp = mesh_pb2.MeshPacket()
|
||||
mp.decoded.payload = data
|
||||
mp.decoded.portnum = portnums_pb2.PortNum.SIMULATOR_APP
|
||||
mp.to = packet.get("to", BROADCAST_NUM)
|
||||
setattr(mp, "from", packet.get("from", 0))
|
||||
mp.id = packet.get("id", 0)
|
||||
mp.want_ack = packet.get("wantAck", False)
|
||||
mp.hop_limit = packet.get("hopLimit", 0)
|
||||
mp.hop_start = packet.get("hopStart", 0)
|
||||
mp.via_mqtt = packet.get("viaMQTT", False)
|
||||
mp.relay_node = packet.get("relayNode", 0)
|
||||
mp.next_hop = packet.get("nextHop", 0)
|
||||
mp.channel = int(packet.get("channel", 0))
|
||||
|
||||
decoded = packet.get("decoded", {})
|
||||
if "requestId" in decoded:
|
||||
mp.decoded.request_id = decoded["requestId"]
|
||||
if "wantResponse" in decoded:
|
||||
mp.decoded.want_response = decoded["wantResponse"]
|
||||
if "bitfield" in decoded:
|
||||
mp.decoded.bitfield = decoded["bitfield"]
|
||||
|
||||
return mp
|
||||
374
meshtastic/tests/fw_helpers.py
Normal file
374
meshtastic/tests/fw_helpers.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""Shared helpers for meshtasticd-backed smoke tests.
|
||||
|
||||
Both ``test_smokevirt`` (single node) and ``test_smokemesh`` (multi-node
|
||||
chain) use these helpers to drive the ``meshtastic`` CLI against real
|
||||
``meshtasticd`` simulator instances and then verify the resulting firmware
|
||||
state through the Python library's ``TCPInterface``.
|
||||
|
||||
Verifying through the library (rather than regex on CLI stdout) is the
|
||||
core design choice: it makes tests robust against CLI wording changes
|
||||
while still exercising both the CLI argparse path and the firmware I/O
|
||||
path of every feature.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
|
||||
from pubsub import pub # type: ignore[import-untyped]
|
||||
|
||||
from meshtastic.tcp_interface import TCPInterface
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pause between a CLI command finishing and a verification interface
|
||||
# opening, to let the firmware flush its TCP bookkeeping. Keeps the
|
||||
# simulator happy when many short-lived connections are happening.
|
||||
PAUSE_AFTER_CLI = 0.2
|
||||
|
||||
# Pause after changing the LoRa region. The firmware may restart the TCP
|
||||
# listener while committing the new radio config, so give it time to settle
|
||||
# before the next CLI call or harness reconnect.
|
||||
PAUSE_AFTER_REGION_CHANGE = 2.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI invocation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def resolve_cli() -> str:
|
||||
"""Return a shell-invokable ``meshtastic`` command.
|
||||
|
||||
Prefers the in-tree module run through the current interpreter so
|
||||
tests exercise the source we are editing, regardless of any
|
||||
separately-installed ``meshtastic`` entry point on PATH.
|
||||
"""
|
||||
# The PATH binary may live outside the nono sandbox's allowed paths;
|
||||
# ``python -m meshtastic`` is more portable and always available.
|
||||
return f"{sys.executable} -m meshtastic"
|
||||
|
||||
|
||||
def run_cli(
|
||||
port: int,
|
||||
*args: str,
|
||||
timeout: int = 60,
|
||||
retries: int = 2,
|
||||
retry_delay: float = 1.0,
|
||||
) -> Tuple[int, str]:
|
||||
"""Run the ``meshtastic`` CLI against the sim node on *port*.
|
||||
|
||||
Returns ``(return_code, merged_stdout_stderr)``. ``--host
|
||||
localhost:PORT`` is prefixed automatically. stderr is merged into
|
||||
stdout so callers can match warning text such as "Warning: Need to
|
||||
specify ..." regardless of which stream it lands on.
|
||||
|
||||
If the CLI fails to connect on the first attempt (which happens
|
||||
transiently when a freshly-booted sim node needs a moment to settle
|
||||
after the harness interface connects), retry up to *retries*
|
||||
additional times after *retry_delay* seconds.
|
||||
"""
|
||||
cli = resolve_cli()
|
||||
argv = [*_shlex_split(cli)]
|
||||
argv.extend(["--host", f"localhost:{port}"])
|
||||
argv.extend(args)
|
||||
logger.debug("run_cli: %s", argv)
|
||||
|
||||
last_out = ""
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
argv,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired as ex:
|
||||
last_out = ex.stdout.decode("utf-8", errors="replace") if ex.stdout else ""
|
||||
if attempt < retries:
|
||||
time.sleep(retry_delay)
|
||||
continue
|
||||
return 124, last_out
|
||||
|
||||
out = proc.stdout.decode("utf-8", errors="replace")
|
||||
rc = proc.returncode
|
||||
|
||||
# Retry on transient connection-refused / timed-out errors that
|
||||
# are common right after a sim node spins up.
|
||||
transient = (
|
||||
"Error connecting" in out
|
||||
or "Timed out waiting for connection" in out
|
||||
or "Connection reset by peer" in out
|
||||
)
|
||||
if rc != 0 and transient and attempt < retries:
|
||||
logger.debug("run_cli: transient failure, retry %d/%d", attempt + 1, retries)
|
||||
time.sleep(retry_delay)
|
||||
last_out = out
|
||||
continue
|
||||
return rc, out
|
||||
return rc, last_out
|
||||
|
||||
|
||||
def _shlex_split(cmd: str) -> List[str]:
|
||||
"""Split a shell string into argv, honoring quotes."""
|
||||
return shlex.split(cmd)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fresh-connection state verification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _wait_for_port(port: int, timeout: float = 30.0) -> None:
|
||||
"""Wait until *port* accepts a TCP connection (firmware sim comes up
|
||||
or comes back after a config-commit reboot)."""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
s = socket.create_connection(("localhost", port), timeout=0.5)
|
||||
s.close()
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(0.2)
|
||||
raise TimeoutError(
|
||||
f"port {port} did not accept connections within {timeout}s"
|
||||
)
|
||||
|
||||
|
||||
def connect_iface(
|
||||
port: int,
|
||||
no_nodes: bool = False,
|
||||
retries: int = 4,
|
||||
wait_timeout: float = 30.0,
|
||||
) -> TCPInterface:
|
||||
"""Open a fresh ``TCPInterface`` to *port* and block on the config exchange.
|
||||
|
||||
Firmware config writes (``writeChannel``, ``--seturl``, ``--factory_reset``)
|
||||
can briefly restart the sim's TCP listener. We wait for the port to
|
||||
come up first, then retry the connect+config-exchange a few times.
|
||||
"""
|
||||
last_exc: Optional[Exception] = None
|
||||
for attempt in range(retries + 1):
|
||||
try:
|
||||
_wait_for_port(port, timeout=wait_timeout)
|
||||
return TCPInterface(
|
||||
hostname="localhost",
|
||||
portNumber=port,
|
||||
connectNow=True,
|
||||
noNodes=no_nodes,
|
||||
)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
last_exc = ex
|
||||
if attempt < retries:
|
||||
logger.debug(
|
||||
"connect_iface attempt %d/%d failed: %s",
|
||||
attempt + 1, retries, ex,
|
||||
)
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
raise
|
||||
assert last_exc is not None # type guard; unreachable
|
||||
raise last_exc # pragma: no cover
|
||||
|
||||
|
||||
def verify_state(
|
||||
port: int,
|
||||
verifier: Callable[[TCPInterface], None],
|
||||
*,
|
||||
no_nodes: bool = False,
|
||||
) -> None:
|
||||
"""Open a fresh interface and run *verifier(iface)*, then close.
|
||||
|
||||
Used after a CLI mutation to verify firmware state through the
|
||||
library. Always closes the interface so the next test starts clean.
|
||||
"""
|
||||
iface = connect_iface(port, no_nodes=no_nodes)
|
||||
try:
|
||||
verifier(iface)
|
||||
finally:
|
||||
try:
|
||||
iface.close()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
time.sleep(PAUSE_AFTER_CLI)
|
||||
|
||||
|
||||
def cli_then_verify(
|
||||
port: int,
|
||||
cli_args: List[str],
|
||||
verifier: Optional[Callable[[TCPInterface], None]],
|
||||
*,
|
||||
expect_rc: Optional[int] = 0,
|
||||
no_nodes: bool = False,
|
||||
cli_timeout: int = 60,
|
||||
) -> str:
|
||||
"""Run *cli_args* against *port*, optionally asserting *expect_rc*,
|
||||
then (if *verifier* is not None) open a fresh interface and run
|
||||
*verifier(iface)* against the just-mutated firmware state.
|
||||
|
||||
Returns the CLI stdout.
|
||||
"""
|
||||
rc, out = run_cli(port, *cli_args, timeout=cli_timeout)
|
||||
if expect_rc is not None:
|
||||
assert rc == expect_rc, f"CLI rc={rc} (expected {expect_rc}): {out}"
|
||||
time.sleep(PAUSE_AFTER_CLI)
|
||||
if verifier is not None:
|
||||
verify_state(port, verifier, no_nodes=no_nodes)
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Packet collectors (used by smokemesh receive-verification tests)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
RECEIVE_TIMEOUT = 15.0
|
||||
|
||||
|
||||
class PacketCollector:
|
||||
"""Collect packets received on a specific interface via pubsub.
|
||||
|
||||
``Listener`` (pubsub 4.x) wraps handlers with a weak reference, so
|
||||
we keep a strong reference to ``handler`` on the instance to prevent
|
||||
garbage collection before the publishing thread gets to call it.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.packets: List[dict] = []
|
||||
self._handler: Optional[Callable] = None
|
||||
|
||||
def on_receive(self, packet, interface): # pylint: disable=unused-argument
|
||||
"""Append a received packet to the internal list."""
|
||||
self.packets.append(packet)
|
||||
|
||||
def wait_for(self, count: int, timeout: float = RECEIVE_TIMEOUT) -> bool:
|
||||
"""Wait until *count* packets have been collected or *timeout* expires."""
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if len(self.packets) >= count:
|
||||
return True
|
||||
time.sleep(0.2)
|
||||
return len(self.packets) >= count
|
||||
|
||||
@property
|
||||
def texts(self) -> List[str]:
|
||||
"""Return text payloads from TEXT_MESSAGE_APP packets."""
|
||||
return [
|
||||
p.get("decoded", {}).get("text", "")
|
||||
for p in self.packets
|
||||
if p.get("decoded", {}).get("portnum") == "TEXT_MESSAGE_APP"
|
||||
]
|
||||
|
||||
@property
|
||||
def traceroutes(self) -> List[dict]:
|
||||
"""Return TRACEROUTE_APP packets."""
|
||||
return [
|
||||
p for p in self.packets
|
||||
if p.get("decoded", {}).get("portnum") == "TRACEROUTE_APP"
|
||||
]
|
||||
|
||||
@property
|
||||
def telemetries(self) -> List[dict]:
|
||||
"""Return TELEMETRY_APP packets."""
|
||||
return [
|
||||
p for p in self.packets
|
||||
if p.get("decoded", {}).get("portnum") == "TELEMETRY_APP"
|
||||
]
|
||||
|
||||
@property
|
||||
def positions(self) -> List[dict]:
|
||||
"""Return POSITION_APP packets."""
|
||||
return [
|
||||
p for p in self.packets
|
||||
if p.get("decoded", {}).get("portnum") == "POSITION_APP"
|
||||
]
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Clear all collected packets."""
|
||||
self.packets.clear()
|
||||
|
||||
|
||||
def _subscribe_topic(
|
||||
iface: TCPInterface, topic: str
|
||||
) -> PacketCollector:
|
||||
"""Internal: subscribe a fully-qualified *topic* and return a collector.
|
||||
|
||||
Filters by *interface* so multi-node tests can subscribe several
|
||||
collectors concurrently without cross-talk.
|
||||
"""
|
||||
collector = PacketCollector()
|
||||
|
||||
def handler(packet, interface):
|
||||
if interface is iface:
|
||||
collector.on_receive(packet, interface)
|
||||
|
||||
pub.subscribe(handler, topic)
|
||||
collector._handler = handler # strong ref; see PacketCollector docstring
|
||||
return collector
|
||||
|
||||
|
||||
def subscribe_texts(iface: TCPInterface) -> PacketCollector:
|
||||
"""Subscribe to ``meshtastic.receive.text`` filtered to *iface*."""
|
||||
return _subscribe_topic(iface, "meshtastic.receive.text")
|
||||
|
||||
|
||||
def subscribe_traceroutes(iface: TCPInterface) -> PacketCollector:
|
||||
"""Subscribe to ``meshtastic.receive.traceroute`` filtered to *iface*."""
|
||||
return _subscribe_topic(iface, "meshtastic.receive.traceroute")
|
||||
|
||||
|
||||
def subscribe_telemetries(iface: TCPInterface) -> PacketCollector:
|
||||
"""Subscribe to ``meshtastic.receive.telemetry`` filtered to *iface*."""
|
||||
return _subscribe_topic(iface, "meshtastic.receive.telemetry")
|
||||
|
||||
|
||||
def subscribe_positions(iface: TCPInterface) -> PacketCollector:
|
||||
"""Subscribe to ``meshtastic.receive.position`` filtered to *iface*."""
|
||||
return _subscribe_topic(iface, "meshtastic.receive.position")
|
||||
|
||||
|
||||
def set_region(port: int, region: str = "US") -> None:
|
||||
"""Set the LoRa region on a sim node via the CLI.
|
||||
|
||||
The node briefly restarts its TCP listener while committing the radio
|
||||
config; ``run_cli()`` handles the reconnect retry, and we sleep long
|
||||
enough for the new region to take effect before callers continue.
|
||||
"""
|
||||
rc, out = run_cli(port, "--set", "lora.region", region)
|
||||
if rc != 0:
|
||||
raise RuntimeError(f"Failed to set lora.region={region}: {out}")
|
||||
time.sleep(PAUSE_AFTER_REGION_CHANGE)
|
||||
|
||||
|
||||
def unsubscribe_all(topic: str) -> None:
|
||||
"""Drop every handler currently registered on *topic*.
|
||||
|
||||
Tests use this in a ``finally`` block to keep pubsub clean across
|
||||
the function-scoped mesh fixtures.
|
||||
"""
|
||||
try:
|
||||
pub.unsubAll(topic)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PAUSE_AFTER_CLI",
|
||||
"PAUSE_AFTER_REGION_CHANGE",
|
||||
"PacketCollector",
|
||||
"RECEIVE_TIMEOUT",
|
||||
"cli_then_verify",
|
||||
"connect_iface",
|
||||
"resolve_cli",
|
||||
"run_cli",
|
||||
"set_region",
|
||||
"subscribe_positions",
|
||||
"subscribe_telemetries",
|
||||
"subscribe_texts",
|
||||
"subscribe_traceroutes",
|
||||
"unsubscribe_all",
|
||||
"verify_state",
|
||||
]
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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):
|
||||
@@ -388,6 +669,78 @@ def test_getChannelByChannelIndex():
|
||||
assert anode.getChannelByChannelIndex(9) is None
|
||||
|
||||
|
||||
def _build_channels(highest_secondary_index: int):
|
||||
"""Build an 8-slot channel table with contiguous active channels.
|
||||
|
||||
Slot 0 is PRIMARY. Slots 1..highest_secondary_index are SECONDARY.
|
||||
Remaining slots are DISABLED.
|
||||
"""
|
||||
channels = []
|
||||
for idx in range(8):
|
||||
ch = Channel()
|
||||
ch.index = idx
|
||||
if idx == 0:
|
||||
ch.role = Channel.Role.PRIMARY
|
||||
ch.settings.name = "primary"
|
||||
elif idx <= highest_secondary_index:
|
||||
ch.role = Channel.Role.SECONDARY
|
||||
ch.settings.name = f"ch{idx}"
|
||||
else:
|
||||
ch.role = Channel.Role.DISABLED
|
||||
channels.append(ch)
|
||||
return channels
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"highest_secondary_index,delete_index,expected_writes",
|
||||
[
|
||||
pytest.param(1, 1, [1], id="active-0-1-del-1"),
|
||||
pytest.param(2, 1, [1, 2], id="active-0-2-del-1"),
|
||||
pytest.param(3, 1, [1, 2, 3], id="active-0-3-del-1"),
|
||||
pytest.param(3, 2, [2, 3], id="active-0-3-del-2"),
|
||||
],
|
||||
)
|
||||
def test_delete_channel_writes_only_changed_suffix(
|
||||
highest_secondary_index, delete_index, expected_writes
|
||||
):
|
||||
"""deleteChannel should only write slots whose payload changed."""
|
||||
iface = MagicMock()
|
||||
anode = Node(iface, "bar", noProto=True)
|
||||
iface.localNode = anode
|
||||
anode.channels = _build_channels(highest_secondary_index)
|
||||
|
||||
writes = []
|
||||
|
||||
def fake_write(channel_index, adminIndex=0):
|
||||
writes.append((channel_index, adminIndex))
|
||||
|
||||
anode.writeChannel = fake_write
|
||||
|
||||
anode.deleteChannel(delete_index)
|
||||
|
||||
written_indices = [idx for idx, _ in writes]
|
||||
assert written_indices == expected_writes
|
||||
assert all(admin_idx == 0 for _, admin_idx in writes)
|
||||
assert 0 not in written_indices
|
||||
assert all(idx < 4 for idx in written_indices)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_delete_channel_rejects_primary():
|
||||
"""deleteChannel should refuse deleting PRIMARY slot 0."""
|
||||
iface = MagicMock()
|
||||
anode = Node(iface, "bar", noProto=True)
|
||||
iface.localNode = anode
|
||||
anode.channels = _build_channels(3)
|
||||
|
||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||
anode.deleteChannel(0)
|
||||
|
||||
assert pytest_wrapped_e.type is SystemExit
|
||||
assert pytest_wrapped_e.value.code == 1
|
||||
|
||||
|
||||
# TODO
|
||||
# @pytest.mark.unit
|
||||
# def test_deleteChannel_try_to_delete_primary_channel(capsys):
|
||||
@@ -794,6 +1147,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 +1631,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 +1856,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)
|
||||
|
||||
202
meshtastic/tests/test_showNodes_favorite.py
Normal file
202
meshtastic/tests/test_showNodes_favorite.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""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_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 == ""
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user