mirror of
https://github.com/meshtastic/python.git
synced 2026-07-31 17:16:45 -04:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c8b1b8ea6f | ||
|
|
d9057c0aaf | ||
|
|
77db30125a | ||
|
|
1214d5010b | ||
|
|
76418b8e57 | ||
|
|
3be73b42e2 | ||
|
|
2245ac8d97 | ||
|
|
040f332078 | ||
|
|
f3f17a7d50 | ||
|
|
79334e83e6 | ||
|
|
38a13f300f | ||
|
|
eef8a37703 | ||
|
|
63b940defb | ||
|
|
e5efa94264 | ||
|
|
efa841b08c | ||
|
|
6ff13eb95c | ||
|
|
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
|
||||
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
|
||||
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@@ -17,6 +17,8 @@ jobs:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python 3
|
||||
@@ -56,12 +58,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.9"
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install Python 3
|
||||
uses: actions/setup-python@v5
|
||||
- name: Install meshtastic from local
|
||||
@@ -70,3 +71,36 @@ jobs:
|
||||
pip3 install poetry
|
||||
poetry install
|
||||
poetry run meshtastic --version
|
||||
|
||||
simradio_testing:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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 --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
|
||||
|
||||
1
Containerfile
Symbolic link
1
Containerfile
Symbolic link
@@ -0,0 +1 @@
|
||||
Containerfile.alpine
|
||||
40
Containerfile.alpine
Normal file
40
Containerfile.alpine
Normal file
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
|
||||
|
||||
ARG TARGET_VERSION="3.11-alpine"
|
||||
ARG TARGET_ARCH="library"
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY pyproject.toml poetry.lock /tmp/build/
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps build-base libffi-dev && \
|
||||
pip install --no-cache-dir 'poetry==2.4.1' && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
|
||||
|
||||
COPY . /tmp/build/
|
||||
|
||||
RUN poetry build --format wheel --no-interaction
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
|
||||
|
||||
RUN apk add --no-cache libffi && \
|
||||
addgroup -S meshtastic && \
|
||||
adduser -S -G meshtastic -h /home/meshtastic meshtastic
|
||||
|
||||
COPY --from=builder /tmp/build/dist/*.whl /tmp/
|
||||
|
||||
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
|
||||
rm -f /tmp/meshtastic-*.whl
|
||||
|
||||
COPY "./bin/container-entrypoint.sh" "/init"
|
||||
RUN chmod 0755 /init
|
||||
|
||||
WORKDIR /home/meshtastic
|
||||
USER meshtastic
|
||||
|
||||
ENTRYPOINT [ "/init" ]
|
||||
37
Containerfile.debian
Normal file
37
Containerfile.debian
Normal file
@@ -0,0 +1,37 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
|
||||
|
||||
ARG TARGET_VERSION="3.11"
|
||||
ARG TARGET_ARCH="library"
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION} AS builder
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY pyproject.toml poetry.lock /tmp/build/
|
||||
|
||||
RUN pip install --no-cache-dir 'poetry==2.4.1' && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --without dev --extras cli --extras tunnel --no-interaction --no-ansi --no-root
|
||||
|
||||
COPY . /tmp/build/
|
||||
|
||||
RUN poetry build --format wheel --no-interaction
|
||||
|
||||
FROM docker.io/${TARGET_ARCH}/python:${TARGET_VERSION}
|
||||
|
||||
RUN useradd --system --create-home --home-dir /home/meshtastic meshtastic
|
||||
|
||||
COPY --from=builder /tmp/build/dist/*.whl /tmp/
|
||||
|
||||
RUN wheel=$(echo /tmp/meshtastic-*.whl) && pip install --no-cache-dir "${wheel}[cli,tunnel]" && \
|
||||
rm -f /tmp/meshtastic-*.whl
|
||||
|
||||
COPY "./bin/container-entrypoint.sh" "/init"
|
||||
RUN chmod 0755 /init
|
||||
|
||||
WORKDIR /home/meshtastic
|
||||
USER meshtastic
|
||||
|
||||
ENTRYPOINT [ "/init" ]
|
||||
1
Dockerfile
Symbolic link
1
Dockerfile
Symbolic link
@@ -0,0 +1 @@
|
||||
Containerfile
|
||||
18
README.md
18
README.md
@@ -27,6 +27,24 @@ This small library (and example application) provides an easy API for sending an
|
||||
It also provides access to any of the operations/data available in the device user interface or the Android application.
|
||||
Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in.
|
||||
|
||||
## Container usage
|
||||
|
||||
Container images are published to GHCR for this repository. The container entrypoint defaults to running `meshtastic`,
|
||||
so CLI flags can be passed directly:
|
||||
|
||||
```bash
|
||||
docker run --rm ghcr.io/meshtastic/python --help
|
||||
```
|
||||
|
||||
To run another command, pass it explicitly (for example, a shell):
|
||||
|
||||
```bash
|
||||
docker run --rm -it --entrypoint /bin/sh ghcr.io/meshtastic/python
|
||||
```
|
||||
|
||||
The container runs as a non-root user by default. When talking to local hardware, pass the serial device through
|
||||
explicitly (for example `--device /dev/ttyUSB0:/dev/ttyUSB0`) and ensure host device permissions allow access.
|
||||
|
||||
## Call for Contributors
|
||||
|
||||
This library and CLI has gone without a consistent maintainer for a while, and there's many improvements that could be made. We're all volunteers here and help is extremely appreciated, whether in implementing your own needs or helping maintain the library and CLI in general.
|
||||
|
||||
24
bin/container-entrypoint.sh
Executable file
24
bin/container-entrypoint.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Copyright (C) 2025 Olliver Schinagl <oliver@schinagl.nl>
|
||||
#
|
||||
# A beginning user should be able to docker run image bash (or sh) without
|
||||
# needing to learn about --entrypoint
|
||||
# https://github.com/docker-library/official-images#consistency
|
||||
|
||||
set -eu
|
||||
|
||||
bin='meshtastic'
|
||||
|
||||
# run command if it is not starting with a "-" and is an executable in PATH
|
||||
if [ "${#}" -le 0 ] || \
|
||||
[ "${1#-}" != "${1}" ] || \
|
||||
[ -d "${1}" ] || \
|
||||
! command -v "${1}" > '/dev/null' 2>&1; then
|
||||
entrypoint='true'
|
||||
fi
|
||||
|
||||
exec ${entrypoint:+${bin:?}} "${@}"
|
||||
|
||||
exit 0
|
||||
332
bin/inject_nanopb_options.py
Normal file
332
bin/inject_nanopb_options.py
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inject nanopb .options constraints as inline field options into a .proto file.
|
||||
|
||||
The nanopb .options file format is specific to the nanopb C generator and is
|
||||
ignored by standard protoc (including --python_out). By injecting the options
|
||||
directly into the proto file's field declarations, protoc will embed them in
|
||||
the serialized descriptor, making them accessible in Python via:
|
||||
|
||||
from meshtastic.protobuf import mesh_pb2, nanopb_pb2
|
||||
field = mesh_pb2.DESCRIPTOR.message_types_by_name['User'].fields_by_name['long_name']
|
||||
opts = field.GetOptions().Extensions[nanopb_pb2.nanopb]
|
||||
print(opts.max_size) # 40
|
||||
|
||||
Usage:
|
||||
inject_nanopb_options.py <options_file> <proto_file>
|
||||
|
||||
The proto_file is modified in-place. Intended to operate on temporary copies
|
||||
generated by regen-protobufs.sh, not the source .proto files.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
# IntSize enum values from nanopb.proto
|
||||
INT_SIZE_ENUM = {8: "IS_8", 16: "IS_16", 32: "IS_32", 64: "IS_64"}
|
||||
|
||||
# Options that are valid proto FieldOptions and useful outside of C code generation.
|
||||
# We skip C-only options (anonymous_oneof, no_unions, skip_message, packed_struct,
|
||||
# packed_enum, mangle_names, callback_datatype, callback_function, descriptorsize,
|
||||
# type_override) since they either don't apply to proto fields or are C-specific.
|
||||
FIELD_OPTIONS = frozenset(
|
||||
{
|
||||
"max_size",
|
||||
"max_length",
|
||||
"max_count",
|
||||
"int_size",
|
||||
"fixed_length",
|
||||
"fixed_count",
|
||||
"long_names",
|
||||
"proto3",
|
||||
"default_has",
|
||||
"sort_by_tag",
|
||||
"msgid",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def parse_value(s: str) -> Any:
|
||||
"""Convert an option value string to an appropriate Python type."""
|
||||
if re.fullmatch(r"-?[0-9]+", s):
|
||||
return int(s)
|
||||
if s.lower() == "true":
|
||||
return True
|
||||
if s.lower() == "false":
|
||||
return False
|
||||
return s
|
||||
|
||||
|
||||
def parse_options_file(
|
||||
path: Path,
|
||||
) -> Tuple[Dict[Tuple[str, ...], Dict[str, Any]], Dict[str, Dict[str, Any]]]:
|
||||
"""Parse a nanopb .options file.
|
||||
|
||||
Returns:
|
||||
specific: maps (message_path..., field_name) -> {option: value}
|
||||
e.g. ('User', 'long_name') or ('Route', 'Link', 'uid')
|
||||
wildcard: maps field_name -> {option: value}
|
||||
applies to any field with this name in the same proto file
|
||||
"""
|
||||
specific: Dict[Tuple[str, ...], Dict[str, Any]] = {}
|
||||
wildcard: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
# Strip inline comments
|
||||
comment_pos = line.find("#")
|
||||
if comment_pos >= 0:
|
||||
line = line[:comment_pos]
|
||||
line = line.strip().lstrip("*").strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
tokens = line.split()
|
||||
if len(tokens) < 2:
|
||||
continue
|
||||
|
||||
pattern = tokens[0]
|
||||
opts: Dict[str, Any] = {}
|
||||
for tok in tokens[1:]:
|
||||
if ":" in tok:
|
||||
k, v = tok.split(":", 1)
|
||||
if k in FIELD_OPTIONS:
|
||||
opts[k] = parse_value(v)
|
||||
|
||||
if not opts:
|
||||
continue
|
||||
|
||||
if "." in pattern:
|
||||
# e.g. "User.long_name" -> key=('User', 'long_name')
|
||||
# or "Route.Link.uid" -> key=('Route', 'Link', 'uid')
|
||||
parts = tuple(pattern.split("."))
|
||||
if parts in specific:
|
||||
specific[parts].update(opts)
|
||||
else:
|
||||
specific[parts] = opts
|
||||
else:
|
||||
# wildcard: applies to any field with this name
|
||||
if pattern in wildcard:
|
||||
wildcard[pattern].update(opts)
|
||||
else:
|
||||
wildcard[pattern] = opts
|
||||
|
||||
return specific, wildcard
|
||||
|
||||
|
||||
def format_nanopb_opts(opts: Dict[str, Any]) -> str:
|
||||
"""Format a dict of nanopb options as a proto field options annotation string."""
|
||||
parts = []
|
||||
for k, v in opts.items():
|
||||
if k == "int_size":
|
||||
enum_val = INT_SIZE_ENUM.get(v, f"IS_{v}")
|
||||
parts.append(f"(nanopb).int_size = {enum_val}")
|
||||
elif isinstance(v, bool):
|
||||
parts.append(f"(nanopb).{k} = {'true' if v else 'false'}")
|
||||
else:
|
||||
parts.append(f"(nanopb).{k} = {v}")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def message_path_matches(
|
||||
context_stack: List[Tuple[str, str]], msg_path: Tuple[str, ...]
|
||||
) -> bool:
|
||||
"""Check if the current message context ends with msg_path.
|
||||
|
||||
context_stack entries are ('message'|'oneof'|'enum', name).
|
||||
msg_path is the tuple of message names from the options pattern,
|
||||
e.g. ('User',) or ('Route', 'Link').
|
||||
"""
|
||||
msg_names = [name for kind, name in context_stack if kind == "message"]
|
||||
n = len(msg_path)
|
||||
return len(msg_names) >= n and tuple(msg_names[-n:]) == msg_path
|
||||
|
||||
|
||||
def inject_into_proto(
|
||||
content: str,
|
||||
specific: Dict[Tuple[str, ...], Dict[str, Any]],
|
||||
wildcard: Dict[str, Dict[str, Any]],
|
||||
nanopb_import_path: str,
|
||||
) -> str:
|
||||
"""Inject nanopb field options into a proto file's text content.
|
||||
|
||||
Adds an import for nanopb.proto if not already present.
|
||||
Returns the modified content.
|
||||
"""
|
||||
if not specific and not wildcard:
|
||||
return content
|
||||
|
||||
lines = content.split("\n")
|
||||
|
||||
# Check if nanopb is already imported (after sed fixup, it will be
|
||||
# 'meshtastic/protobuf/nanopb.proto')
|
||||
nanopb_already_imported = any(
|
||||
"nanopb.proto" in line
|
||||
for line in lines
|
||||
if line.strip().startswith("import")
|
||||
)
|
||||
|
||||
# Track the index of the last import line so we can insert after it
|
||||
last_import_idx = max(
|
||||
(
|
||||
i
|
||||
for i, line in enumerate(lines)
|
||||
if line.strip().startswith("import ") and line.strip().endswith(";")
|
||||
),
|
||||
default=-1,
|
||||
)
|
||||
|
||||
# State
|
||||
context_stack: List[Tuple[str, str]] = [] # ('message'|'oneof'|'enum', name)
|
||||
result: List[str] = []
|
||||
import_added = nanopb_already_imported
|
||||
|
||||
# Patterns for proto structural elements
|
||||
message_re = re.compile(r"^(\s*)message\s+(\w+)\s*\{")
|
||||
oneof_re = re.compile(r"^(\s*)oneof\s+(\w+)\s*\{")
|
||||
enum_re = re.compile(r"^(\s*)enum\s+(\w+)\s*\{")
|
||||
close_re = re.compile(r"^\s*\}")
|
||||
|
||||
# Pattern for field declarations:
|
||||
# indent [optional|repeated] type name = number [options] ;
|
||||
# We exclude map<> fields (different syntax, nanopb handles them differently)
|
||||
# and enum value lines (no type keyword before the name).
|
||||
field_re = re.compile(
|
||||
r"^(\s*)" # (1) indent
|
||||
r"(optional\s+|repeated\s+)?" # (2) optional qualifier
|
||||
r"([\w.]+)\s+" # (3) field type (possibly qualified like google.protobuf.Any)
|
||||
r"(\w+)\s*" # (4) field name
|
||||
r"=\s*(\d+)" # (5) field number
|
||||
r"(?:\s*\[([^\]]*)\])?" # (6) existing options, without brackets
|
||||
r"\s*;" # trailing semicolon
|
||||
)
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Insert nanopb import right after the last existing import line.
|
||||
# Only do this when there IS an existing import (last_import_idx >= 0);
|
||||
# if there are no imports we fall through to the syntax-line fallback below.
|
||||
if not import_added and last_import_idx >= 0 and i == last_import_idx + 1:
|
||||
result.append(f'import "{nanopb_import_path}";')
|
||||
import_added = True
|
||||
|
||||
# --- Track message/oneof/enum nesting ---
|
||||
m = message_re.match(line)
|
||||
if m:
|
||||
context_stack.append(("message", m.group(2)))
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
m = oneof_re.match(line)
|
||||
if m:
|
||||
context_stack.append(("oneof", m.group(2)))
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
m = enum_re.match(line)
|
||||
if m:
|
||||
context_stack.append(("enum", m.group(2)))
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
if close_re.match(line) and context_stack:
|
||||
context_stack.pop()
|
||||
result.append(line)
|
||||
continue
|
||||
|
||||
# Skip field injection inside enum bodies (enum values look like fields
|
||||
# but should not have nanopb options added)
|
||||
in_enum = bool(context_stack) and context_stack[-1][0] == "enum"
|
||||
|
||||
# --- Try to match and modify a field declaration ---
|
||||
m = field_re.match(line)
|
||||
if m and not in_enum:
|
||||
indent = m.group(1)
|
||||
qualifier = m.group(2) or ""
|
||||
ftype = m.group(3)
|
||||
fname = m.group(4)
|
||||
fnum = m.group(5)
|
||||
existing_opts = m.group(6) or ""
|
||||
|
||||
# Collect applicable nanopb options (wildcard < specific)
|
||||
extra: Dict[str, Any] = {}
|
||||
|
||||
# 1. Wildcard: any field with this name in this proto file
|
||||
if fname in wildcard:
|
||||
extra.update(wildcard[fname])
|
||||
|
||||
# 2. Specific: check all keys whose last element is fname and whose
|
||||
# preceding path matches the current message context
|
||||
for key, opts in specific.items():
|
||||
if key[-1] == fname:
|
||||
msg_path = key[:-1]
|
||||
if message_path_matches(context_stack, msg_path):
|
||||
extra.update(opts)
|
||||
break
|
||||
|
||||
if extra:
|
||||
nanopb_str = format_nanopb_opts(extra)
|
||||
if existing_opts.strip():
|
||||
opts_block = f"[{existing_opts}, {nanopb_str}]"
|
||||
else:
|
||||
opts_block = f"[{nanopb_str}]"
|
||||
qual = qualifier.rstrip()
|
||||
sep = " " if qual else ""
|
||||
line = f"{indent}{qual}{sep}{ftype} {fname} = {fnum} {opts_block};"
|
||||
|
||||
result.append(line)
|
||||
|
||||
# Edge case: if there were no import lines, add nanopb import after syntax line
|
||||
if not import_added:
|
||||
for i, line in enumerate(result):
|
||||
if line.strip().startswith("syntax") and line.strip().endswith(";"):
|
||||
result.insert(i + 1, f'import "{nanopb_import_path}";')
|
||||
break
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Parse an .options file and inject its constraints into a .proto file in-place."""
|
||||
if len(sys.argv) != 3:
|
||||
print(
|
||||
f"Usage: {sys.argv[0]} <options_file> <proto_file>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
opts_path = Path(sys.argv[1])
|
||||
proto_path = Path(sys.argv[2])
|
||||
|
||||
if not opts_path.exists():
|
||||
print(f"Options file not found: {opts_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not proto_path.exists():
|
||||
print(f"Proto file not found: {proto_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
specific, wildcard = parse_options_file(opts_path)
|
||||
total = len(specific) + len(wildcard)
|
||||
|
||||
if total == 0:
|
||||
print(f" [{opts_path.name}] No injectable options found, skipping.")
|
||||
return 0
|
||||
|
||||
content = proto_path.read_text(encoding="utf-8")
|
||||
|
||||
# After regen-protobufs.sh's sed fixup, the nanopb import path is:
|
||||
nanopb_import_path = "meshtastic/protobuf/nanopb.proto"
|
||||
|
||||
modified = inject_into_proto(content, specific, wildcard, nanopb_import_path)
|
||||
proto_path.write_text(modified, encoding="utf-8")
|
||||
|
||||
print(
|
||||
f" [{opts_path.name}] Injected {len(specific)} specific + "
|
||||
f"{len(wildcard)} wildcard option(s) into {proto_path.name}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -28,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
|
||||
@@ -45,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
|
||||
|
||||
|
||||
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,26 +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('icon', help="icon 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
|
||||
@@ -40,18 +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()
|
||||
|
||||
@@ -179,8 +179,30 @@ def _onPositionReceive(iface, asDict):
|
||||
logger.debug(f"p:{p}")
|
||||
p = iface._fixupPosition(p)
|
||||
logger.debug(f"after fixup p:{p}")
|
||||
# update node DB as needed
|
||||
iface._getOrCreateByNum(asDict["from"])["position"] = 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):
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
# later we can have a separate changelist to refactor main.py into smaller files
|
||||
# pylint: disable=R0917,C0302
|
||||
|
||||
from typing import List, Optional, Union
|
||||
from typing import Any, Callable, List, Optional, Union
|
||||
from types import ModuleType
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import argparse
|
||||
|
||||
argcomplete: Union[None, ModuleType] = None
|
||||
@@ -37,6 +39,7 @@ try:
|
||||
except ImportError as e:
|
||||
have_test = False
|
||||
|
||||
import meshtastic.ota
|
||||
import meshtastic.util
|
||||
import meshtastic.serial_interface
|
||||
import meshtastic.tcp_interface
|
||||
@@ -60,11 +63,19 @@ except ImportError as e:
|
||||
have_powermon = False
|
||||
powermon_exception = e
|
||||
meter = None
|
||||
from meshtastic.protobuf import channel_pb2, config_pb2, portnums_pb2, mesh_pb2
|
||||
from meshtastic.protobuf import admin_pb2, channel_pb2, clientonly_pb2, config_pb2, portnums_pb2, mesh_pb2
|
||||
from meshtastic.version import get_active_version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Map dotted preference paths to the protobuf enum that defines their flags.
|
||||
# These fields are stored as uint32 bitmasks in the protobuf but have an
|
||||
# associated enum that names the individual flags.
|
||||
BITFIELD_ENUMS = {
|
||||
"network.enabled_protocols": config_pb2.Config.NetworkConfig.ProtocolFlags,
|
||||
"position.position_flags": config_pb2.Config.PositionConfig.PositionFlags,
|
||||
}
|
||||
|
||||
def onReceive(packet, interface) -> None:
|
||||
"""Callback invoked when a packet arrives"""
|
||||
args = mt_config.args
|
||||
@@ -85,12 +96,17 @@ def onReceive(packet, interface) -> None:
|
||||
if d is not None and args and args.reply:
|
||||
msg = d.get("text")
|
||||
if msg:
|
||||
rxSnr = packet["rxSnr"]
|
||||
hopLimit = packet["hopLimit"]
|
||||
print(f"message: {msg}")
|
||||
reply = f"got msg '{msg}' with rxSnr: {rxSnr} and hopLimit: {hopLimit}"
|
||||
print("Sending reply: ", reply)
|
||||
interface.sendText(reply)
|
||||
rxChannel = packet.get("channel", 0)
|
||||
targetChannel = int(args.ch_index or 0)
|
||||
if rxChannel == targetChannel:
|
||||
rxSnr = packet["rxSnr"]
|
||||
hopLimit = packet["hopLimit"]
|
||||
print(f"message: {msg}")
|
||||
reply = f"got msg '{msg}' with rxSnr: {rxSnr} and hopLimit: {hopLimit}"
|
||||
print(f"Received channel {rxChannel}. Sending reply: {reply}")
|
||||
interface.sendText(reply,channelIndex=rxChannel)
|
||||
else:
|
||||
print(f"Ignored message on channel {rxChannel} (waiting for channel {targetChannel})")
|
||||
|
||||
except Exception as ex:
|
||||
print(f"Warning: Error processing received packet: {ex}.")
|
||||
@@ -158,11 +174,11 @@ def getPref(node, comp_name) -> bool:
|
||||
config_values = getattr(config, config_type.name)
|
||||
if not wholeField:
|
||||
pref_value = getattr(config_values, pref.name)
|
||||
repeated = pref.label == pref.LABEL_REPEATED
|
||||
repeated = _is_repeated_field(pref)
|
||||
_printSetting(config_type, uni_name, pref_value, repeated)
|
||||
else:
|
||||
for field in config_values.ListFields():
|
||||
repeated = field[0].label == field[0].LABEL_REPEATED
|
||||
repeated = _is_repeated_field(field[0])
|
||||
_printSetting(config_type, field[0].name, field[1], repeated)
|
||||
else:
|
||||
# Always show whole field for remote node
|
||||
@@ -232,13 +248,28 @@ def setPref(config, comp_name, raw_val) -> bool:
|
||||
print("Warning: network.wifi_psk must be 8 or more characters.")
|
||||
return False
|
||||
|
||||
# Handle uint32 bitfields that have an associated enum of flag names.
|
||||
bitfield_enum = None
|
||||
if config_type.message_type is not None:
|
||||
bitfield_path = f"{config_type.name}.{pref.name}"
|
||||
bitfield_enum = BITFIELD_ENUMS.get(bitfield_path)
|
||||
if bitfield_enum and isinstance(val, str):
|
||||
# At this point fromStr() could not parse val as int/float/bool/bytes,
|
||||
# so treat it as a comma-separated list of bitfield flag names.
|
||||
flag_names = [name.strip() for name in val.split(",") if name.strip()]
|
||||
try:
|
||||
val = meshtastic.util.flags_from_list(bitfield_enum, flag_names)
|
||||
except ValueError as e:
|
||||
print(f"ERROR: {e}")
|
||||
return False
|
||||
|
||||
enumType = pref.enum_type
|
||||
# pylint: disable=C0123
|
||||
if enumType and type(val) == str:
|
||||
# We've failed so far to convert this string into an enum, try to find it by reflection
|
||||
e = enumType.values_by_name.get(val)
|
||||
if e:
|
||||
val = e.number
|
||||
ev = enumType.values_by_name.get(val)
|
||||
if ev:
|
||||
val = ev.number
|
||||
else:
|
||||
print(
|
||||
f"{name[0]}.{uni_name} does not have an enum called {val}, so you can not set it."
|
||||
@@ -253,7 +284,7 @@ def setPref(config, comp_name, raw_val) -> bool:
|
||||
return False
|
||||
|
||||
# repeating fields need to be handled with append, not setattr
|
||||
if pref.label != pref.LABEL_REPEATED:
|
||||
if not _is_repeated_field(pref):
|
||||
try:
|
||||
if config_type.message_type is not None:
|
||||
config_values = getattr(config_part, config_type.name)
|
||||
@@ -452,6 +483,41 @@ def onConnected(interface):
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).rebootOTA()
|
||||
|
||||
if args.ota_update:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
|
||||
if not isinstance(interface, meshtastic.tcp_interface.TCPInterface):
|
||||
meshtastic.util.our_exit(
|
||||
"Error: OTA update currently requires a TCP connection to the node (use --host)."
|
||||
)
|
||||
|
||||
ota = meshtastic.ota.ESP32WiFiOTA(args.ota_update, interface.hostname)
|
||||
|
||||
print(f"Triggering OTA update on {interface.hostname}...")
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).startOTA(
|
||||
ota_mode=admin_pb2.OTAMode.OTA_WIFI,
|
||||
ota_file_hash=ota.hash_bytes()
|
||||
)
|
||||
|
||||
print("Waiting for device to reboot into OTA mode...")
|
||||
time.sleep(5)
|
||||
|
||||
retries = 5
|
||||
while retries > 0:
|
||||
try:
|
||||
ota.update()
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
retries -= 1
|
||||
if retries == 0:
|
||||
meshtastic.util.our_exit(f"\nOTA update failed: {e}")
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
print("\nOTA update completed successfully!")
|
||||
|
||||
if args.enter_dfu:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
@@ -511,6 +577,13 @@ def onConnected(interface):
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).resetNodeDb()
|
||||
|
||||
if args.add_contact:
|
||||
closeNow = True
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).addContactURL(
|
||||
args.add_contact
|
||||
)
|
||||
|
||||
if args.sendtext:
|
||||
closeNow = True
|
||||
channelIndex = mt_config.channel_index or 0
|
||||
@@ -671,115 +744,105 @@ def onConnected(interface):
|
||||
printConfig(node.moduleConfig)
|
||||
|
||||
if args.configure:
|
||||
with open(args.configure[0], encoding="utf8") as file:
|
||||
configuration = yaml.safe_load(file)
|
||||
closeNow = True
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
print("Configuring remote nodes is not supported.")
|
||||
return
|
||||
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction()
|
||||
filename = args.configure[0]
|
||||
fmt = getattr(args, "export_format", "auto")
|
||||
|
||||
if "owner" in configuration:
|
||||
# Validate owner name before setting
|
||||
owner_name = str(configuration["owner"]).strip()
|
||||
if not owner_name:
|
||||
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
|
||||
print(f"Setting device owner to {configuration['owner']}")
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(configuration["owner"])
|
||||
time.sleep(0.5)
|
||||
def _seed_config(section: str, is_module: bool) -> Optional[Any]:
|
||||
"""Return the current local-node config section, if present."""
|
||||
config_obj = (
|
||||
interface.localNode.moduleConfig
|
||||
if is_module
|
||||
else interface.localNode.localConfig
|
||||
)
|
||||
return getattr(config_obj, section) if config_obj.HasField(section) else None
|
||||
|
||||
if "owner_short" in configuration:
|
||||
# Validate owner short name before setting
|
||||
owner_short_name = str(configuration["owner_short"]).strip()
|
||||
if not owner_short_name:
|
||||
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
print(
|
||||
f"Setting device owner short to {configuration['owner_short']}"
|
||||
)
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
|
||||
long_name=None, short_name=configuration["owner_short"]
|
||||
)
|
||||
time.sleep(0.5)
|
||||
profile = _read_profile(filename, fmt, seed_fn=_seed_config)
|
||||
|
||||
if "ownerShort" in configuration:
|
||||
# Validate owner short name before setting
|
||||
owner_short_name = str(configuration["ownerShort"]).strip()
|
||||
if not owner_short_name:
|
||||
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
print(
|
||||
f"Setting device owner short to {configuration['ownerShort']}"
|
||||
)
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
|
||||
long_name=None, short_name=configuration["ownerShort"]
|
||||
)
|
||||
time.sleep(0.5)
|
||||
closeNow = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).beginSettingsTransaction()
|
||||
|
||||
if "channel_url" in configuration:
|
||||
print("Setting channel url to", configuration["channel_url"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channel_url"])
|
||||
time.sleep(0.5)
|
||||
# Owner: combine long_name and short_name into a single setOwner call.
|
||||
# NOTE: is_licensed and is_unmessagable are not yet in DeviceProfile;
|
||||
# ref: https://github.com/meshtastic/protobufs/pull/971
|
||||
long_name = str(profile.long_name).strip() if profile.long_name else None
|
||||
short_name = str(profile.short_name).strip() if profile.short_name else None
|
||||
|
||||
if "channelUrl" in configuration:
|
||||
print("Setting channel url to", configuration["channelUrl"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).setURL(configuration["channelUrl"])
|
||||
time.sleep(0.5)
|
||||
if long_name is not None and not long_name:
|
||||
meshtastic.util.our_exit("ERROR: Long Name cannot be empty or contain only whitespace characters")
|
||||
if short_name is not None and not short_name:
|
||||
meshtastic.util.our_exit("ERROR: Short Name cannot be empty or contain only whitespace characters")
|
||||
|
||||
if "canned_messages" in configuration:
|
||||
print("Setting canned message messages to", configuration["canned_messages"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).set_canned_message(configuration["canned_messages"])
|
||||
time.sleep(0.5)
|
||||
if long_name or short_name:
|
||||
if long_name and short_name:
|
||||
print(f"Setting device owner to {long_name} and short name to {short_name}")
|
||||
elif long_name:
|
||||
print(f"Setting device owner to {long_name}")
|
||||
else:
|
||||
print(f"Setting device owner short to {short_name}")
|
||||
waitForAckNak = True
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setOwner(
|
||||
long_name=long_name, short_name=short_name
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "ringtone" in configuration:
|
||||
print("Setting ringtone to", configuration["ringtone"])
|
||||
interface.getNode(args.dest, **getNode_kwargs).set_ringtone(configuration["ringtone"])
|
||||
time.sleep(0.5)
|
||||
if profile.channel_url:
|
||||
print(f"Setting channel url to {profile.channel_url}")
|
||||
interface.getNode(args.dest, **getNode_kwargs).setURL(profile.channel_url)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "location" in configuration:
|
||||
alt = 0
|
||||
lat = 0.0
|
||||
lon = 0.0
|
||||
localConfig = interface.localNode.localConfig
|
||||
if profile.canned_messages:
|
||||
print(f"Setting canned message messages to {profile.canned_messages}")
|
||||
interface.getNode(args.dest, **getNode_kwargs).set_canned_message(profile.canned_messages)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "alt" in configuration["location"]:
|
||||
alt = int(configuration["location"]["alt"] or 0)
|
||||
print(f"Fixing altitude at {alt} meters")
|
||||
if "lat" in configuration["location"]:
|
||||
lat = float(configuration["location"]["lat"] or 0)
|
||||
print(f"Fixing latitude at {lat} degrees")
|
||||
if "lon" in configuration["location"]:
|
||||
lon = float(configuration["location"]["lon"] or 0)
|
||||
print(f"Fixing longitude at {lon} degrees")
|
||||
if profile.ringtone:
|
||||
print(f"Setting ringtone to {profile.ringtone}")
|
||||
interface.getNode(args.dest, **getNode_kwargs).set_ringtone(profile.ringtone)
|
||||
time.sleep(0.5)
|
||||
|
||||
if profile.HasField("fixed_position"):
|
||||
# Only send the admin message when the position config
|
||||
# explicitly opts into fixed_position. The admin message
|
||||
# unconditionally enables the flag on the device, so we
|
||||
# require an explicit opt-in from the profile's config.
|
||||
if (
|
||||
profile.HasField("config")
|
||||
and profile.config.HasField("position")
|
||||
and profile.config.position.fixed_position
|
||||
):
|
||||
pos = profile.fixed_position
|
||||
lat = float(pos.latitude_i * Decimal("1e-7")) if pos.latitude_i else 0.0
|
||||
lon = float(pos.longitude_i * Decimal("1e-7")) if pos.longitude_i else 0.0
|
||||
alt = pos.altitude if pos.altitude else 0
|
||||
print(f"Fixing altitude at {alt} meters")
|
||||
print(f"Fixing latitude at {lat} degrees")
|
||||
print(f"Fixing longitude at {lon} degrees")
|
||||
print("Setting device position")
|
||||
interface.localNode.setFixedPosition(lat, lon, alt)
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).setFixedPosition(lat, lon, alt)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "config" in configuration:
|
||||
localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig
|
||||
for section in configuration["config"]:
|
||||
traverseConfig(
|
||||
section, configuration["config"][section], localConfig
|
||||
)
|
||||
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
|
||||
meshtastic.util.camel_to_snake(section)
|
||||
)
|
||||
if profile.HasField("config"):
|
||||
localConfig = interface.getNode(args.dest, **getNode_kwargs).localConfig
|
||||
for field in profile.config.DESCRIPTOR.fields:
|
||||
if field.message_type is not None and profile.config.HasField(field.name):
|
||||
getattr(localConfig, field.name).CopyFrom(getattr(profile.config, field.name))
|
||||
interface.getNode(args.dest, **getNode_kwargs).writeConfig(field.name)
|
||||
time.sleep(0.5)
|
||||
|
||||
if "module_config" in configuration:
|
||||
moduleConfig = interface.getNode(args.dest, **getNode_kwargs).moduleConfig
|
||||
for section in configuration["module_config"]:
|
||||
traverseConfig(
|
||||
section,
|
||||
configuration["module_config"][section],
|
||||
moduleConfig,
|
||||
)
|
||||
interface.getNode(args.dest, **getNode_kwargs).writeConfig(
|
||||
meshtastic.util.camel_to_snake(section)
|
||||
)
|
||||
if profile.HasField("module_config"):
|
||||
moduleConfig = interface.getNode(args.dest, **getNode_kwargs).moduleConfig
|
||||
for field in profile.module_config.DESCRIPTOR.fields:
|
||||
if field.message_type is not None and profile.module_config.HasField(field.name):
|
||||
getattr(moduleConfig, field.name).CopyFrom(getattr(profile.module_config, field.name))
|
||||
interface.getNode(args.dest, **getNode_kwargs).writeConfig(field.name)
|
||||
time.sleep(0.5)
|
||||
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction()
|
||||
print("Writing modified configuration to device")
|
||||
interface.getNode(args.dest, False, **getNode_kwargs).commitSettingsTransaction()
|
||||
print("Writing modified configuration to device")
|
||||
|
||||
if args.export_config:
|
||||
if args.dest != BROADCAST_ADDR:
|
||||
@@ -787,18 +850,40 @@ def onConnected(interface):
|
||||
return
|
||||
|
||||
closeNow = True
|
||||
config_txt = export_config(interface)
|
||||
|
||||
if args.export_config == "-":
|
||||
# Output to stdout (preserves legacy use of `> file.yaml`)
|
||||
print(config_txt)
|
||||
is_binary = False
|
||||
fmt = getattr(args, "export_format", "auto")
|
||||
if fmt in ("binary", "protobuf"):
|
||||
is_binary = True
|
||||
elif fmt == "yaml":
|
||||
is_binary = False
|
||||
else:
|
||||
try:
|
||||
with open(args.export_config, "w", encoding="utf-8") as f:
|
||||
f.write(config_txt)
|
||||
print(f"Exported configuration to {args.export_config}")
|
||||
except Exception as e:
|
||||
meshtastic.util.our_exit(f"ERROR: Failed to write config file: {e}")
|
||||
is_binary = args.export_config.endswith(".cfg")
|
||||
|
||||
if is_binary:
|
||||
config_bytes = export_profile(interface)
|
||||
if args.export_config == "-":
|
||||
sys.stdout.buffer.write(config_bytes)
|
||||
else:
|
||||
try:
|
||||
with open(args.export_config, "wb") as f:
|
||||
f.write(config_bytes)
|
||||
print(f"Exported profile to {args.export_config}")
|
||||
except Exception as e:
|
||||
meshtastic.util.our_exit(f"ERROR: Failed to write profile file: {e}")
|
||||
else:
|
||||
config_txt = export_config(interface)
|
||||
|
||||
if args.export_config == "-":
|
||||
# Output to stdout (preserves legacy use of `> file.yaml`)
|
||||
print(config_txt)
|
||||
else:
|
||||
try:
|
||||
with open(args.export_config, "w", encoding="utf-8") as f:
|
||||
f.write(config_txt)
|
||||
print(f"Exported configuration to {args.export_config}")
|
||||
except Exception as e:
|
||||
meshtastic.util.our_exit(f"ERROR: Failed to write config file: {e}")
|
||||
|
||||
if args.ch_set_url:
|
||||
closeNow = True
|
||||
@@ -884,9 +969,15 @@ def onConnected(interface):
|
||||
if args.ch_longslow:
|
||||
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_SLOW)
|
||||
|
||||
if args.ch_longmod:
|
||||
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_MODERATE)
|
||||
|
||||
if args.ch_longfast:
|
||||
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_FAST)
|
||||
|
||||
if args.ch_longturbo:
|
||||
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_TURBO)
|
||||
|
||||
if args.ch_medslow:
|
||||
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.MEDIUM_SLOW)
|
||||
|
||||
@@ -899,6 +990,9 @@ def onConnected(interface):
|
||||
if args.ch_shortfast:
|
||||
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_FAST)
|
||||
|
||||
if args.ch_shortturbo:
|
||||
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_TURBO)
|
||||
|
||||
if args.ch_set or args.ch_enable or args.ch_disable:
|
||||
closeNow = True
|
||||
|
||||
@@ -1033,6 +1127,20 @@ def onConnected(interface):
|
||||
else:
|
||||
print("Install pyqrcode to view a QR code printed to terminal.")
|
||||
|
||||
if args.contact_qr:
|
||||
closeNow = True
|
||||
url = interface.getNode(args.dest, True, **getNode_kwargs).getContactURL(
|
||||
args.contact_qr,
|
||||
should_ignore=args.contact_ignore,
|
||||
manually_verified=args.contact_verified,
|
||||
)
|
||||
print(f"Contact URL: {url}")
|
||||
if pyqrcode is not None:
|
||||
qr = pyqrcode.create(url)
|
||||
print(qr.terminal())
|
||||
else:
|
||||
print("Install pyqrcode to view a QR code printed to terminal.")
|
||||
|
||||
log_set: Optional = None # type: ignore[annotation-unchecked]
|
||||
# we need to keep a reference to the logset so it doesn't get GCed early
|
||||
|
||||
@@ -1131,6 +1239,14 @@ def subscribe() -> None:
|
||||
|
||||
# pub.subscribe(onNode, "meshtastic.node")
|
||||
|
||||
def _is_repeated_field(field_desc) -> bool:
|
||||
"""Return True if the protobuf field is repeated.
|
||||
Protobuf 6.31.0 and later use an is_repeated property, while older versions compare against the label field.
|
||||
"""
|
||||
if hasattr(field_desc, "is_repeated"):
|
||||
return bool(field_desc.is_repeated)
|
||||
return field_desc.label == field_desc.LABEL_REPEATED
|
||||
|
||||
def set_missing_flags_false(config_dict: dict, true_defaults: set[tuple[str, str]]) -> None:
|
||||
"""Ensure that missing default=True keys are present in the config_dict and set to False."""
|
||||
for path in true_defaults:
|
||||
@@ -1170,7 +1286,7 @@ def export_config(interface) -> str:
|
||||
lat = None
|
||||
lon = None
|
||||
alt = None
|
||||
if pos:
|
||||
if pos and interface.localNode.localConfig.position.fixed_position:
|
||||
lat = pos.get("latitude")
|
||||
lon = pos.get("longitude")
|
||||
alt = pos.get("altitude")
|
||||
@@ -1214,7 +1330,7 @@ def export_config(interface) -> str:
|
||||
for i in range(len(prefs[pref]['adminKey'])):
|
||||
prefs[pref]['adminKey'][i] = 'base64:' + prefs[pref]['adminKey'][i]
|
||||
if mt_config.camel_case:
|
||||
configObj["config"] = config #Identical command here and 2 lines below?
|
||||
configObj["config"] = prefs
|
||||
else:
|
||||
configObj["config"] = config
|
||||
|
||||
@@ -1239,6 +1355,162 @@ def export_config(interface) -> str:
|
||||
config_txt += yaml.dump(configObj)
|
||||
return config_txt
|
||||
|
||||
def _set_if_populated(profile, field_name, value):
|
||||
if value is None:
|
||||
return
|
||||
val = str(value).strip()
|
||||
if val:
|
||||
setattr(profile, field_name, val)
|
||||
|
||||
|
||||
def _profile_from_yaml(
|
||||
configuration: dict,
|
||||
seed_fn: Optional[Callable[[str, bool], Optional[Any]]] = None,
|
||||
) -> clientonly_pb2.DeviceProfile:
|
||||
"""Convert a YAML config dict to a DeviceProfile protobuf for uniform import.
|
||||
|
||||
If seed_fn is provided, it is called for each config/module_config section
|
||||
before applying YAML values. It should return the current protobuf message
|
||||
for that section (or None) so that unmentioned fields are preserved.
|
||||
"""
|
||||
profile = clientonly_pb2.DeviceProfile()
|
||||
if "owner" in configuration:
|
||||
_set_if_populated(profile, "long_name", configuration["owner"])
|
||||
if "owner_short" in configuration:
|
||||
_set_if_populated(profile, "short_name", configuration["owner_short"])
|
||||
elif "ownerShort" in configuration:
|
||||
_set_if_populated(profile, "short_name", configuration["ownerShort"])
|
||||
if "channel_url" in configuration:
|
||||
_set_if_populated(profile, "channel_url", configuration["channel_url"])
|
||||
elif "channelUrl" in configuration:
|
||||
_set_if_populated(profile, "channel_url", configuration["channelUrl"])
|
||||
if "canned_messages" in configuration:
|
||||
_set_if_populated(profile, "canned_messages", configuration["canned_messages"])
|
||||
if "ringtone" in configuration:
|
||||
_set_if_populated(profile, "ringtone", configuration["ringtone"])
|
||||
loc = configuration.get("location")
|
||||
if loc:
|
||||
lat = float(loc.get("lat", 0) or 0)
|
||||
lon = float(loc.get("lon", 0) or 0)
|
||||
alt = int(loc.get("alt", 0) or 0)
|
||||
if lat or lon or alt:
|
||||
profile.fixed_position.latitude_i = int(Decimal(str(lat)) * Decimal("1e7"))
|
||||
profile.fixed_position.longitude_i = int(Decimal(str(lon)) * Decimal("1e7"))
|
||||
profile.fixed_position.altitude = alt
|
||||
if "config" in configuration:
|
||||
for section in configuration["config"]:
|
||||
section_snake = meshtastic.util.camel_to_snake(section)
|
||||
if seed_fn is not None:
|
||||
seeded = seed_fn(section_snake, False)
|
||||
if seeded is not None:
|
||||
getattr(profile.config, section_snake).CopyFrom(seeded)
|
||||
traverseConfig(section, configuration["config"][section], profile.config)
|
||||
if "module_config" in configuration:
|
||||
for section in configuration["module_config"]:
|
||||
section_snake = meshtastic.util.camel_to_snake(section)
|
||||
if seed_fn is not None:
|
||||
seeded = seed_fn(section_snake, True)
|
||||
if seeded is not None:
|
||||
getattr(profile.module_config, section_snake).CopyFrom(seeded)
|
||||
traverseConfig(section, configuration["module_config"][section], profile.module_config)
|
||||
return profile
|
||||
|
||||
|
||||
def _read_profile(
|
||||
filename: str,
|
||||
fmt: str,
|
||||
seed_fn: Optional[Callable[[str, bool], Optional[Any]]] = None,
|
||||
) -> clientonly_pb2.DeviceProfile:
|
||||
"""Read a config file and return a DeviceProfile, autodetecting format by content."""
|
||||
with open(filename, "rb") as f:
|
||||
raw = f.read()
|
||||
|
||||
if fmt in ("binary", "protobuf"):
|
||||
try:
|
||||
return _parse_profile_bytes(raw)
|
||||
except Exception as e:
|
||||
meshtastic.util.our_exit(
|
||||
f"ERROR: {filename} is not a valid DeviceProfile (.cfg) file: {e}"
|
||||
)
|
||||
|
||||
if fmt == "yaml":
|
||||
try:
|
||||
configuration = yaml.safe_load(raw.decode("utf8"))
|
||||
except (UnicodeDecodeError, yaml.YAMLError) as e:
|
||||
meshtastic.util.our_exit(
|
||||
f"ERROR: {filename} is not a valid YAML config (expected UTF-8 YAML): {e}"
|
||||
)
|
||||
if not isinstance(configuration, dict):
|
||||
meshtastic.util.our_exit(
|
||||
f"ERROR: {filename} is not a valid YAML config (expected a mapping)."
|
||||
)
|
||||
return _profile_from_yaml(configuration, seed_fn=seed_fn)
|
||||
|
||||
# Auto: try YAML first (text files), fall back to protobuf (binary files).
|
||||
try:
|
||||
configuration = yaml.safe_load(raw.decode("utf8"))
|
||||
if isinstance(configuration, dict):
|
||||
return _profile_from_yaml(configuration, seed_fn=seed_fn)
|
||||
except (UnicodeDecodeError, yaml.YAMLError):
|
||||
pass
|
||||
|
||||
try:
|
||||
return _parse_profile_bytes(raw)
|
||||
except Exception as e:
|
||||
meshtastic.util.our_exit(
|
||||
f"ERROR: {filename} is not a valid YAML config or DeviceProfile (.cfg) file: {e}"
|
||||
)
|
||||
|
||||
|
||||
def _parse_profile_bytes(raw: bytes) -> clientonly_pb2.DeviceProfile:
|
||||
"""Parse raw bytes as a DeviceProfile protobuf, raising on failure."""
|
||||
profile = clientonly_pb2.DeviceProfile()
|
||||
profile.ParseFromString(raw)
|
||||
return profile
|
||||
|
||||
|
||||
def export_profile(interface) -> bytes:
|
||||
"""used in --export-config for binary .cfg files"""
|
||||
profile = clientonly_pb2.DeviceProfile()
|
||||
|
||||
owner = interface.getLongName()
|
||||
owner_short = interface.getShortName()
|
||||
channel_url = interface.localNode.getURL()
|
||||
myinfo = interface.getMyNodeInfo()
|
||||
canned_messages = interface.getCannedMessage()
|
||||
ringtone = interface.getRingtone()
|
||||
|
||||
if owner:
|
||||
profile.long_name = owner
|
||||
if owner_short:
|
||||
profile.short_name = owner_short
|
||||
if channel_url:
|
||||
profile.channel_url = channel_url
|
||||
if canned_messages:
|
||||
profile.canned_messages = canned_messages
|
||||
if ringtone:
|
||||
profile.ringtone = ringtone
|
||||
|
||||
profile.config.CopyFrom(interface.localNode.localConfig)
|
||||
profile.module_config.CopyFrom(interface.localNode.moduleConfig)
|
||||
|
||||
if interface.localNode.localConfig.position.fixed_position:
|
||||
pos = myinfo.get("position")
|
||||
if pos:
|
||||
lat = pos.get("latitude")
|
||||
lon = pos.get("longitude")
|
||||
alt = pos.get("altitude")
|
||||
|
||||
if lat or lon or alt:
|
||||
if lat:
|
||||
profile.fixed_position.latitude_i = int(Decimal(str(lat)) * Decimal("1e7"))
|
||||
if lon:
|
||||
profile.fixed_position.longitude_i = int(Decimal(str(lon)) * Decimal("1e7"))
|
||||
if alt:
|
||||
profile.fixed_position.altitude = int(alt)
|
||||
|
||||
return profile.SerializeToString()
|
||||
|
||||
|
||||
def create_power_meter():
|
||||
"""Setup the power meter."""
|
||||
@@ -1313,6 +1585,11 @@ def common():
|
||||
if not stripped_ham_name:
|
||||
meshtastic.util.our_exit("ERROR: Ham radio callsign cannot be empty or contain only whitespace characters")
|
||||
|
||||
# Early validation for OTA firmware file before attempting device connection
|
||||
if hasattr(args, 'ota_update') and args.ota_update is not None:
|
||||
if not os.path.isfile(args.ota_update):
|
||||
meshtastic.util.our_exit(f"Error: OTA firmware file not found: {args.ota_update}", 1)
|
||||
|
||||
if have_powermon:
|
||||
create_power_meter()
|
||||
|
||||
@@ -1365,13 +1642,59 @@ def common():
|
||||
print(f"Found: name='{x.name}' address='{x.address}'")
|
||||
meshtastic.util.our_exit("BLE scan finished", 0)
|
||||
elif args.ble:
|
||||
client = BLEInterface(
|
||||
args.ble if args.ble != "any" else None,
|
||||
debugOut=logfile,
|
||||
noProto=args.noproto,
|
||||
noNodes=args.no_nodes,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
try:
|
||||
client = BLEInterface(
|
||||
args.ble if args.ble != "any" else None,
|
||||
debugOut=logfile,
|
||||
noProto=args.noproto,
|
||||
noNodes=args.no_nodes,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
except BLEInterface.BLEError as e:
|
||||
if e.kind == BLEInterface.BLEError.DEVICE_NOT_FOUND:
|
||||
meshtastic.util.our_exit(
|
||||
"BLE device not found.\n\n"
|
||||
"Possible causes:\n"
|
||||
" - Bluetooth is disabled on the Meshtastic device\n"
|
||||
" - Device is in deep sleep mode\n"
|
||||
" - Device is out of range\n\n"
|
||||
"Try:\n"
|
||||
" - Press the reset button on your device\n"
|
||||
" - Run 'meshtastic --ble-scan' to see available devices",
|
||||
1,
|
||||
)
|
||||
elif e.kind == BLEInterface.BLEError.MULTIPLE_DEVICES:
|
||||
meshtastic.util.our_exit(
|
||||
"Multiple Meshtastic BLE devices found.\n\n"
|
||||
"Please specify which device to connect to:\n"
|
||||
" - Run 'meshtastic --ble-scan' to list devices\n"
|
||||
" - Use 'meshtastic --ble <name_or_address>' to connect",
|
||||
1,
|
||||
)
|
||||
elif e.kind == BLEInterface.BLEError.WRITE_ERROR:
|
||||
meshtastic.util.our_exit(
|
||||
"Failed to write to BLE device.\n\n"
|
||||
"Possible causes:\n"
|
||||
" - Device requires pairing PIN (check device screen)\n"
|
||||
" - On Linux: user not in 'bluetooth' group\n"
|
||||
" - Connection was interrupted\n\n"
|
||||
"Try:\n"
|
||||
" - Restart Bluetooth on your computer\n"
|
||||
" - Reset the Meshtastic device",
|
||||
1,
|
||||
)
|
||||
elif e.kind == BLEInterface.BLEError.READ_ERROR:
|
||||
meshtastic.util.our_exit(
|
||||
"Failed to read from BLE device.\n\n"
|
||||
"The device may have disconnected unexpectedly.\n\n"
|
||||
"Try:\n"
|
||||
" - Move closer to the device\n"
|
||||
" - Reset the Meshtastic device\n"
|
||||
" - Restart Bluetooth on your computer",
|
||||
1,
|
||||
)
|
||||
else:
|
||||
meshtastic.util.our_exit(f"BLE error: {e}", 1)
|
||||
elif args.host:
|
||||
try:
|
||||
if ":" in args.host:
|
||||
@@ -1427,6 +1750,23 @@ def common():
|
||||
message += " Please close any applications or webpages that may be using the device and try again.\n"
|
||||
message += f"\nOriginal error: {ex}"
|
||||
meshtastic.util.our_exit(message)
|
||||
except MeshInterface.MeshInterfaceError as ex:
|
||||
msg = str(ex)
|
||||
if "Timed out" in msg:
|
||||
meshtastic.util.our_exit(
|
||||
"Connection timed out.\n\n"
|
||||
"Possible causes:\n"
|
||||
" - Device is rebooting\n"
|
||||
" - Device firmware is updating\n"
|
||||
" - Serial connection was interrupted\n\n"
|
||||
"Try:\n"
|
||||
" - Wait a few seconds and try again\n"
|
||||
" - Check if device is fully booted (LED patterns)\n"
|
||||
" - Reconnect the USB cable",
|
||||
1,
|
||||
)
|
||||
else:
|
||||
meshtastic.util.our_exit(f"Connection error: {ex}", 1)
|
||||
if client.devPath is None:
|
||||
try:
|
||||
client = meshtastic.tcp_interface.TCPInterface(
|
||||
@@ -1483,10 +1823,12 @@ def addConnectionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParse
|
||||
"--host",
|
||||
"--tcp",
|
||||
"-t",
|
||||
help="Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s')",
|
||||
help=("Connect to a device using TCP, optionally passing hostname or IP address to use. (defaults to '%(const)s'). "
|
||||
"A port number may be specified as well, e.g. meshtastic.local:4404. The default port is 4403."),
|
||||
nargs="?",
|
||||
default=None,
|
||||
const="localhost",
|
||||
metavar="HOST[:PORT]",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
@@ -1539,7 +1881,9 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
|
||||
|
||||
group.add_argument(
|
||||
"--configure",
|
||||
help="Specify a path to a yaml(.yml) file containing the desired settings for the connected device.",
|
||||
"--import-config",
|
||||
dest="configure",
|
||||
help="Specify a path to a configuration file to import. Autodetects format (yaml or binary protobuf).",
|
||||
action="append",
|
||||
)
|
||||
group.add_argument(
|
||||
@@ -1547,7 +1891,13 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
|
||||
nargs="?",
|
||||
const="-", # default to "-" if no value provided
|
||||
metavar="FILE",
|
||||
help="Export device config as YAML (to stdout if no file given)"
|
||||
help="Export device config (to stdout if no file given). Autodetects format by extension if possible."
|
||||
)
|
||||
group.add_argument(
|
||||
"--export-format",
|
||||
choices=["auto", "yaml", "binary", "protobuf"],
|
||||
default="auto",
|
||||
help="Format for export or import. 'auto' uses file extension or contents. 'binary' or 'protobuf' forces binary format. 'yaml' forces yaml."
|
||||
)
|
||||
return parser
|
||||
|
||||
@@ -1619,43 +1969,61 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
||||
|
||||
group.add_argument(
|
||||
"--ch-vlongslow",
|
||||
help="Change to the very long-range and slow modem preset",
|
||||
help="Change to the VERY_LONG_SLOW modem preset. Deprecated since 2.5 firmware.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-longslow",
|
||||
help="Change to the long-range and slow modem preset",
|
||||
help="Change to the LONG_SLOW modem preset. Deprecated since 2.7 firmware.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-longmod", "--ch-longmoderate",
|
||||
help="Change to the LONG_MODERATE modem preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-longfast",
|
||||
help="Change to the long-range and fast modem preset",
|
||||
help="Change to the LONG_FAST modem preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-longturbo",
|
||||
help="Change to the LONG_TURBO preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-medslow",
|
||||
help="Change to the med-range and slow modem preset",
|
||||
help="Change to the MEDIUM_SLOW modem preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-medfast",
|
||||
help="Change to the med-range and fast modem preset",
|
||||
help="Change to the MEDIUM_FAST modem preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-shortslow",
|
||||
help="Change to the short-range and slow modem preset",
|
||||
help="Change to the SHORT_SLOW modem preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-shortfast",
|
||||
help="Change to the short-range and fast modem preset",
|
||||
help="Change to the SHORT_FAST modem preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-shortturbo",
|
||||
help="Change to the SHORT_TURBO modem preset",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
@@ -1746,6 +2114,24 @@ def addChannelConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPa
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--contact-qr",
|
||||
help="Display a QR code for a node's contact data. "
|
||||
"Use the node ID with a '!' or '0x' prefix or the node number. "
|
||||
"Also shows the shareable contact URL.",
|
||||
metavar="!xxxxxxxx",
|
||||
)
|
||||
group.add_argument(
|
||||
"--contact-verified",
|
||||
help="Set the IS_KEY_MANUALLY_VERIFIED bit in the generated contact URL",
|
||||
action="store_true",
|
||||
)
|
||||
group.add_argument(
|
||||
"--contact-ignore",
|
||||
help="Mark this contact as blocked/ignored in the generated contact URL",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ch-enable",
|
||||
help="Enable the specified channel. Use --ch-add instead whenever possible.",
|
||||
@@ -1798,7 +2184,8 @@ def addPositionConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentP
|
||||
|
||||
group.add_argument(
|
||||
"--pos-fields",
|
||||
help="Specify fields to send when sending a position. Use no argument for a list of valid values. "
|
||||
help="Deprecated: use '--set position.position_flags FLAG1,FLAG2' instead. "
|
||||
"Specify fields to send when sending a position. Use no argument for a list of valid values. "
|
||||
"Can pass multiple values as a space separated list like "
|
||||
"this: '--pos-fields ALTITUDE HEADING SPEED'",
|
||||
nargs="*",
|
||||
@@ -1883,7 +2270,10 @@ def addRemoteActionArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--reply", help="Reply to received messages", action="store_true"
|
||||
"--reply",
|
||||
help="Reply to received messages on the channel they were received. "
|
||||
"If '--ch-index' is set, only messages on that channel are replied to.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
return parser
|
||||
@@ -1904,10 +2294,18 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
||||
|
||||
group.add_argument(
|
||||
"--reboot-ota",
|
||||
help="Tell the destination node to reboot into factory firmware (ESP32)",
|
||||
help="Tell the destination node to reboot into factory firmware (ESP32, firmware version <2.7.18)",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--ota-update",
|
||||
help="Perform an OTA update on the local node (ESP32, firmware version >=2.7.18, WiFi/TCP only for now). "
|
||||
"Specify the path to the firmware file.",
|
||||
metavar="FIRMWARE_FILE",
|
||||
action="store",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--enter-dfu",
|
||||
help="Tell the destination node to enter DFU mode (NRF52)",
|
||||
@@ -1972,6 +2370,13 @@ def addRemoteAdminArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPars
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--add-contact",
|
||||
help="Add a contact (User) to the NodeDB from a shareable URL. "
|
||||
"Example: https://meshtastic.org/v/#<base64>",
|
||||
metavar="URL",
|
||||
)
|
||||
|
||||
group.add_argument(
|
||||
"--set-time",
|
||||
help="Set the time to the provided unix epoch timestamp, or the system's current time if omitted or 0.",
|
||||
|
||||
@@ -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
|
||||
@@ -32,6 +33,16 @@ class BLEInterface(MeshInterface):
|
||||
class BLEError(Exception):
|
||||
"""An exception class for BLE errors."""
|
||||
|
||||
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],
|
||||
@@ -156,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]
|
||||
|
||||
@@ -203,7 +216,10 @@ class BLEInterface(MeshInterface):
|
||||
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)
|
||||
@@ -226,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)
|
||||
@@ -258,11 +275,13 @@ 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:
|
||||
logger.debug("No address provided - only discover method will work.")
|
||||
@@ -306,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()
|
||||
|
||||
@@ -253,6 +253,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
"channel": "Channel",
|
||||
"lastHeard": "LastHeard",
|
||||
"since": "Since",
|
||||
"isFavorite": "Fav",
|
||||
|
||||
}
|
||||
|
||||
@@ -300,7 +301,7 @@ 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")
|
||||
@@ -342,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":
|
||||
@@ -416,6 +419,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
channelIndex: int = 0,
|
||||
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.
|
||||
@@ -433,6 +437,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
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.
|
||||
@@ -446,7 +451,8 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
replyId=replyId
|
||||
replyId=replyId,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
|
||||
|
||||
@@ -456,6 +462,7 @@ 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,
|
||||
but carries a higher priority and is capable of generating special notifications
|
||||
@@ -467,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.
|
||||
@@ -480,7 +488,8 @@ 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):
|
||||
@@ -582,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)
|
||||
@@ -618,6 +628,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForPosition()
|
||||
@@ -670,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()
|
||||
@@ -723,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()
|
||||
@@ -770,6 +790,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForTelemetry()
|
||||
@@ -839,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)
|
||||
@@ -879,6 +901,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForWaypoint()
|
||||
@@ -891,6 +914,7 @@ 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)
|
||||
@@ -917,6 +941,7 @@ class MeshInterface: # pylint: disable=R0902
|
||||
wantResponse=wantResponse,
|
||||
onResponse=onResponse,
|
||||
channelIndex=channelIndex,
|
||||
hopLimit=hopLimit,
|
||||
)
|
||||
if wantResponse:
|
||||
self.waitForWaypoint()
|
||||
@@ -1452,6 +1477,10 @@ 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:
|
||||
logger.debug("Unexpected FromRadio payload")
|
||||
|
||||
@@ -170,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:
|
||||
@@ -245,6 +244,8 @@ 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}")
|
||||
|
||||
@@ -286,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
|
||||
@@ -379,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:
|
||||
@@ -444,6 +495,32 @@ 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"""
|
||||
logger.debug(f"onResponseRequestRingtone() p:{p}")
|
||||
@@ -654,7 +731,7 @@ 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
|
||||
@@ -667,6 +744,22 @@ 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()
|
||||
@@ -711,10 +804,10 @@ class Node:
|
||||
self.ensureSessionKey()
|
||||
p = admin_pb2.AdminMessage()
|
||||
if full:
|
||||
p.factory_reset_device = True
|
||||
p.factory_reset_device = 1
|
||||
logger.info(f"Telling node to factory reset (full device reset)")
|
||||
else:
|
||||
p.factory_reset_config = True
|
||||
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
|
||||
|
||||
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
|
||||
91
meshtastic/protobuf/admin_pb2.py
generated
91
meshtastic/protobuf/admin_pb2.py
generated
File diff suppressed because one or more lines are too long
387
meshtastic/protobuf/admin_pb2.pyi
generated
387
meshtastic/protobuf/admin_pb2.pyi
generated
@@ -224,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):
|
||||
"""
|
||||
@@ -282,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)
|
||||
@@ -346,6 +370,34 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
) -> 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
|
||||
@@ -390,11 +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
|
||||
REBOOT_OTA_MODE_FIELD_NUMBER: builtins.int
|
||||
FACTORY_RESET_DEVICE_FIELD_NUMBER: builtins.int
|
||||
REBOOT_OTA_SECONDS_FIELD_NUMBER: builtins.int
|
||||
EXIT_SIMULATOR_FIELD_NUMBER: builtins.int
|
||||
@@ -402,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.
|
||||
@@ -519,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
|
||||
@@ -528,10 +587,6 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
Commits an open transaction for any edits made to config, module config, owner, and channel settings
|
||||
"""
|
||||
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)
|
||||
"""
|
||||
factory_reset_device: builtins.int
|
||||
"""
|
||||
Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared.
|
||||
@@ -677,6 +732,31 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
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,
|
||||
*,
|
||||
@@ -724,11 +804,11 @@ 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 = ...,
|
||||
reboot_ota_mode: global___OTAMode.ValueType = ...,
|
||||
factory_reset_device: builtins.int = ...,
|
||||
reboot_ota_seconds: builtins.int = ...,
|
||||
exit_simulator: builtins.bool = ...,
|
||||
@@ -736,13 +816,78 @@ class AdminMessage(google.protobuf.message.Message):
|
||||
shutdown_seconds: builtins.int = ...,
|
||||
factory_reset_config: 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["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", "nodedb_reset", b"nodedb_reset", "payload_variant", b"payload_variant", "reboot_ota_mode", b"reboot_ota_mode", "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", "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["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", "nodedb_reset", b"nodedb_reset", "payload_variant", b"payload_variant", "reboot_ota_mode", b"reboot_ota_mode", "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", "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", "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", "begin_edit_settings", "commit_edit_settings", "add_contact", "key_verification", "reboot_ota_mode", "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):
|
||||
"""
|
||||
@@ -933,3 +1078,227 @@ class KeyVerificationAdmin(google.protobuf.message.Message):
|
||||
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
|
||||
|
||||
9
meshtastic/protobuf/apponly_pb2.py
generated
9
meshtastic/protobuf/apponly_pb2.py
generated
@@ -13,9 +13,10 @@ _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.LoRaConfigBc\n\x14org.meshtastic.protoB\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)
|
||||
@@ -23,6 +24,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.apponly
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\rAppOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_CHANNELSET']._serialized_start=128
|
||||
_globals['_CHANNELSET']._serialized_end=257
|
||||
_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)
|
||||
|
||||
247
meshtastic/protobuf/atak_pb2.py
generated
247
meshtastic/protobuf/atak_pb2.py
generated
File diff suppressed because one or more lines are too long
3380
meshtastic/protobuf/atak_pb2.pyi
generated
3380
meshtastic/protobuf/atak_pb2.pyi
generated
File diff suppressed because it is too large
Load Diff
9
meshtastic/protobuf/cannedmessages_pb2.py
generated
9
meshtastic/protobuf/cannedmessages_pb2.py
generated
@@ -11,9 +11,10 @@ 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(\tBo\n\x14org.meshtastic.protoB\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)
|
||||
@@ -21,6 +22,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cannedm
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\031CannedMessageConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_start=65
|
||||
_globals['_CANNEDMESSAGEMODULECONFIG']._serialized_end=110
|
||||
_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)
|
||||
|
||||
25
meshtastic/protobuf/channel_pb2.py
generated
25
meshtastic/protobuf/channel_pb2.py
generated
@@ -11,9 +11,10 @@ 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\">\n\x0eModuleSettings\x12\x1a\n\x12position_precision\x18\x01 \x01(\r\x12\x10\n\x08is_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\x63\n\x14org.meshtastic.protoB\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)
|
||||
@@ -23,12 +24,18 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
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=316
|
||||
_globals['_CHANNEL']._serialized_start=319
|
||||
_globals['_CHANNEL']._serialized_end=498
|
||||
_globals['_CHANNEL_ROLE']._serialized_start=450
|
||||
_globals['_CHANNEL_ROLE']._serialized_end=498
|
||||
_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)
|
||||
|
||||
15
meshtastic/protobuf/clientonly_pb2.py
generated
15
meshtastic/protobuf/clientonly_pb2.py
generated
@@ -13,9 +13,10 @@ _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_messagesBf\n\x14org.meshtastic.protoB\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)
|
||||
@@ -23,6 +24,14 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.cliento
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020ClientOnlyProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_DEVICEPROFILE']._serialized_start=131
|
||||
_globals['_DEVICEPROFILE']._serialized_end=583
|
||||
_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)
|
||||
|
||||
139
meshtastic/protobuf/config_pb2.py
generated
139
meshtastic/protobuf/config_pb2.py
generated
File diff suppressed because one or more lines are too long
167
meshtastic/protobuf/config_pb2.pyi
generated
167
meshtastic/protobuf/config_pb2.pyi
generated
@@ -119,7 +119,7 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
CLIENT_BASE: Config.DeviceConfig._Role.ValueType # 12
|
||||
"""
|
||||
Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
|
||||
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.
|
||||
@@ -211,7 +211,7 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
CLIENT_BASE: Config.DeviceConfig.Role.ValueType # 12
|
||||
"""
|
||||
Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
|
||||
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.
|
||||
@@ -1010,6 +1010,10 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@@ -1036,6 +1040,10 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
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)
|
||||
@@ -1164,6 +1172,7 @@ class Config(google.protobuf.message.Message):
|
||||
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
|
||||
@@ -1222,6 +1231,10 @@ class Config(google.protobuf.message.Message):
|
||||
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,
|
||||
*,
|
||||
@@ -1238,8 +1251,9 @@ class Config(google.protobuf.message.Message):
|
||||
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", "use_long_node_name", b"use_long_node_name", "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):
|
||||
@@ -1363,6 +1377,31 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
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
|
||||
@@ -1473,6 +1512,31 @@ class Config(google.protobuf.message.Message):
|
||||
"""
|
||||
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)
|
||||
@@ -1525,6 +1589,31 @@ class Config(google.protobuf.message.Message):
|
||||
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):
|
||||
"""
|
||||
@@ -1577,6 +1666,64 @@ class Config(google.protobuf.message.Message):
|
||||
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
|
||||
@@ -1596,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`
|
||||
@@ -1693,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]:
|
||||
"""
|
||||
@@ -1722,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):
|
||||
|
||||
29
meshtastic/protobuf/connection_status_pb2.py
generated
29
meshtastic/protobuf/connection_status_pb2.py
generated
@@ -11,9 +11,10 @@ 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\x66\n\x14org.meshtastic.protoB\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)
|
||||
@@ -21,16 +22,18 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.connect
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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
|
||||
_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)
|
||||
|
||||
63
meshtastic/protobuf/device_ui_pb2.py
generated
63
meshtastic/protobuf/device_ui_pb2.py
generated
@@ -11,9 +11,10 @@ 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\"\xff\x05\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\x12\x36\n\x0c\x63ompass_mode\x18\x10 \x01(\x0e\x32 .meshtastic.protobuf.CompassMode\x12\x18\n\x10screen_rgb_color\x18\x11 \x01(\r\x12\x1b\n\x13is_clockface_analog\x18\x12 \x01(\x08\x12K\n\ngps_format\x18\x13 \x01(\x0e\x32\x37.meshtastic.protobuf.DeviceUIConfig.GpsCoordinateFormat\"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\"\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\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')
|
||||
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)
|
||||
@@ -21,22 +22,46 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.device_
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\016DeviceUIProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_COMPASSMODE']._serialized_start=1278
|
||||
_globals['_COMPASSMODE']._serialized_end=1340
|
||||
_globals['_THEME']._serialized_start=1342
|
||||
_globals['_THEME']._serialized_end=1379
|
||||
_globals['_LANGUAGE']._serialized_start=1382
|
||||
_globals['_LANGUAGE']._serialized_end=1702
|
||||
_globals['_DEVICEUICONFIG']._serialized_start=61
|
||||
_globals['_DEVICEUICONFIG']._serialized_end=828
|
||||
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_start=742
|
||||
_globals['_DEVICEUICONFIG_GPSCOORDINATEFORMAT']._serialized_end=828
|
||||
_globals['_NODEFILTER']._serialized_start=831
|
||||
_globals['_NODEFILTER']._serialized_end=998
|
||||
_globals['_NODEHIGHLIGHT']._serialized_start=1000
|
||||
_globals['_NODEHIGHLIGHT']._serialized_end=1126
|
||||
_globals['_GEOPOINT']._serialized_start=1128
|
||||
_globals['_GEOPOINT']._serialized_end=1189
|
||||
_globals['_MAP']._serialized_start=1191
|
||||
_globals['_MAP']._serialized_end=1276
|
||||
_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)
|
||||
|
||||
44
meshtastic/protobuf/deviceonly_pb2.py
generated
44
meshtastic/protobuf/deviceonly_pb2.py
generated
@@ -19,7 +19,7 @@ from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_tel
|
||||
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 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\"\x94\x02\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\x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x00\x88\x01\x01\x42\x12\n\x10_is_unmessagable\"\xf0\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(\r\x12\x10\n\x08\x62itfield\x18\r \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.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')
|
||||
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)
|
||||
@@ -28,25 +28,43 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
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=703
|
||||
_globals['_NODEINFOLITE']._serialized_start=706
|
||||
_globals['_NODEINFOLITE']._serialized_end=1074
|
||||
_globals['_DEVICESTATE']._serialized_start=1077
|
||||
_globals['_DEVICESTATE']._serialized_end=1494
|
||||
_globals['_NODEDATABASE']._serialized_start=1496
|
||||
_globals['_NODEDATABASE']._serialized_end=1621
|
||||
_globals['_CHANNELFILE']._serialized_start=1623
|
||||
_globals['_CHANNELFILE']._serialized_end=1701
|
||||
_globals['_BACKUPPREFERENCES']._serialized_start=1704
|
||||
_globals['_BACKUPPREFERENCES']._serialized_end=1966
|
||||
_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)
|
||||
|
||||
1
meshtastic/protobuf/deviceonly_pb2.pyi
generated
1
meshtastic/protobuf/deviceonly_pb2.pyi
generated
@@ -197,6 +197,7 @@ class NodeInfoLite(google.protobuf.message.Message):
|
||||
"""
|
||||
Bitfield for storing booleans.
|
||||
LSB 0 is_key_manually_verified
|
||||
LSB 1 is_muted
|
||||
"""
|
||||
@property
|
||||
def user(self) -> global___UserLite:
|
||||
|
||||
17
meshtastic/protobuf/interdevice_pb2.py
generated
17
meshtastic/protobuf/interdevice_pb2.py
generated
@@ -11,9 +11,10 @@ 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\x42g\n\x14org.meshtastic.protoB\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)
|
||||
@@ -21,10 +22,12 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.interde
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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
|
||||
_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)
|
||||
|
||||
4
meshtastic/protobuf/localonly_pb2.py
generated
4
meshtastic/protobuf/localonly_pb2.py
generated
@@ -15,7 +15,7 @@ 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(\rBe\n\x14org.meshtastic.protoB\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)
|
||||
@@ -26,5 +26,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
_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
|
||||
|
||||
309
meshtastic/protobuf/mesh_pb2.py
generated
309
meshtastic/protobuf/mesh_pb2.py
generated
File diff suppressed because one or more lines are too long
461
meshtastic/protobuf/mesh_pb2.pyi
generated
461
meshtastic/protobuf/mesh_pb2.pyi
generated
@@ -168,9 +168,9 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
||||
Less common/prototype boards listed here (needs one more byte over the air)
|
||||
---------------------------------------------------------------------------
|
||||
"""
|
||||
NRF52840DK: _HardwareModel.ValueType # 33
|
||||
T_ECHO_PLUS: _HardwareModel.ValueType # 33
|
||||
"""
|
||||
TODO: REPLACE
|
||||
T-Echo Plus device from LilyGo
|
||||
"""
|
||||
PPR: _HardwareModel.ValueType # 34
|
||||
"""
|
||||
@@ -535,6 +535,81 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
|
||||
"""
|
||||
Elecrow ThinkNode M6
|
||||
"""
|
||||
MESHSTICK_1262: _HardwareModel.ValueType # 121
|
||||
"""
|
||||
Elecrow Meshstick 1262
|
||||
"""
|
||||
TBEAM_1_WATT: _HardwareModel.ValueType # 122
|
||||
"""
|
||||
LilyGo T-Beam 1W
|
||||
"""
|
||||
T5_S3_EPAPER_PRO: _HardwareModel.ValueType # 123
|
||||
"""
|
||||
LilyGo T5 S3 ePaper Pro (V1 and V2)
|
||||
"""
|
||||
TBEAM_BPF: _HardwareModel.ValueType # 124
|
||||
"""
|
||||
LilyGo T-Beam BPF (144-148Mhz)
|
||||
"""
|
||||
MINI_EPAPER_S3: _HardwareModel.ValueType # 125
|
||||
"""
|
||||
LilyGo T-Mini E-paper S3 Kit
|
||||
"""
|
||||
TDISPLAY_S3_PRO: _HardwareModel.ValueType # 126
|
||||
"""
|
||||
LilyGo T-Display S3 Pro LR1121
|
||||
"""
|
||||
HELTEC_MESH_NODE_T096: _HardwareModel.ValueType # 127
|
||||
"""
|
||||
Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen.
|
||||
"""
|
||||
TRACKER_T1000_E_PRO: _HardwareModel.ValueType # 128
|
||||
"""
|
||||
Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio,
|
||||
GPS, button, buzzer, and sensors.
|
||||
"""
|
||||
THINKNODE_M7: _HardwareModel.ValueType # 129
|
||||
"""
|
||||
Elecrow ThinkNode M7, M8 and M9
|
||||
"""
|
||||
THINKNODE_M8: _HardwareModel.ValueType # 130
|
||||
THINKNODE_M9: _HardwareModel.ValueType # 131
|
||||
HELTEC_V4_R8: _HardwareModel.ValueType # 132
|
||||
"""
|
||||
The Heltec-V4-R8 uses an ESP32S3R8 chip, plus an SX1262.
|
||||
"""
|
||||
HELTEC_MESH_NODE_T1: _HardwareModel.ValueType # 133
|
||||
"""
|
||||
The HELTEC_MESH_NODE_T1 uses an NRF52840 chip, plus an SX1262.
|
||||
"""
|
||||
STATION_G3: _HardwareModel.ValueType # 134
|
||||
"""
|
||||
B&Q Consulting Station G3: TBD
|
||||
"""
|
||||
T_IMPULSE_PLUS: _HardwareModel.ValueType # 135
|
||||
"""
|
||||
Lilygo T-Impulse-Plus
|
||||
"""
|
||||
T_ECHO_CARD: _HardwareModel.ValueType # 136
|
||||
"""
|
||||
Lilygo T-Echo Card
|
||||
"""
|
||||
SEEED_WIO_TRACKER_L2: _HardwareModel.ValueType # 137
|
||||
"""
|
||||
Seeed Tracker L2
|
||||
"""
|
||||
CROWPANEL_P4: _HardwareModel.ValueType # 138
|
||||
"""
|
||||
Elecrow CrowPanel Advance P4 models, ESP32-P4 and TFT with SX1262 radio plugin
|
||||
"""
|
||||
HELTEC_MESH_TOWER_V2: _HardwareModel.ValueType # 139
|
||||
"""
|
||||
Heltec Mesh Tower V2
|
||||
"""
|
||||
MESHNOLOGY_W10: _HardwareModel.ValueType # 140
|
||||
"""
|
||||
Meshnology W10
|
||||
"""
|
||||
PRIVATE_HW: _HardwareModel.ValueType # 255
|
||||
"""
|
||||
------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -686,9 +761,9 @@ LORA_RELAY_V1: HardwareModel.ValueType # 32
|
||||
Less common/prototype boards listed here (needs one more byte over the air)
|
||||
---------------------------------------------------------------------------
|
||||
"""
|
||||
NRF52840DK: HardwareModel.ValueType # 33
|
||||
T_ECHO_PLUS: HardwareModel.ValueType # 33
|
||||
"""
|
||||
TODO: REPLACE
|
||||
T-Echo Plus device from LilyGo
|
||||
"""
|
||||
PPR: HardwareModel.ValueType # 34
|
||||
"""
|
||||
@@ -1053,6 +1128,81 @@ THINKNODE_M6: HardwareModel.ValueType # 120
|
||||
"""
|
||||
Elecrow ThinkNode M6
|
||||
"""
|
||||
MESHSTICK_1262: HardwareModel.ValueType # 121
|
||||
"""
|
||||
Elecrow Meshstick 1262
|
||||
"""
|
||||
TBEAM_1_WATT: HardwareModel.ValueType # 122
|
||||
"""
|
||||
LilyGo T-Beam 1W
|
||||
"""
|
||||
T5_S3_EPAPER_PRO: HardwareModel.ValueType # 123
|
||||
"""
|
||||
LilyGo T5 S3 ePaper Pro (V1 and V2)
|
||||
"""
|
||||
TBEAM_BPF: HardwareModel.ValueType # 124
|
||||
"""
|
||||
LilyGo T-Beam BPF (144-148Mhz)
|
||||
"""
|
||||
MINI_EPAPER_S3: HardwareModel.ValueType # 125
|
||||
"""
|
||||
LilyGo T-Mini E-paper S3 Kit
|
||||
"""
|
||||
TDISPLAY_S3_PRO: HardwareModel.ValueType # 126
|
||||
"""
|
||||
LilyGo T-Display S3 Pro LR1121
|
||||
"""
|
||||
HELTEC_MESH_NODE_T096: HardwareModel.ValueType # 127
|
||||
"""
|
||||
Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen.
|
||||
"""
|
||||
TRACKER_T1000_E_PRO: HardwareModel.ValueType # 128
|
||||
"""
|
||||
Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio,
|
||||
GPS, button, buzzer, and sensors.
|
||||
"""
|
||||
THINKNODE_M7: HardwareModel.ValueType # 129
|
||||
"""
|
||||
Elecrow ThinkNode M7, M8 and M9
|
||||
"""
|
||||
THINKNODE_M8: HardwareModel.ValueType # 130
|
||||
THINKNODE_M9: HardwareModel.ValueType # 131
|
||||
HELTEC_V4_R8: HardwareModel.ValueType # 132
|
||||
"""
|
||||
The Heltec-V4-R8 uses an ESP32S3R8 chip, plus an SX1262.
|
||||
"""
|
||||
HELTEC_MESH_NODE_T1: HardwareModel.ValueType # 133
|
||||
"""
|
||||
The HELTEC_MESH_NODE_T1 uses an NRF52840 chip, plus an SX1262.
|
||||
"""
|
||||
STATION_G3: HardwareModel.ValueType # 134
|
||||
"""
|
||||
B&Q Consulting Station G3: TBD
|
||||
"""
|
||||
T_IMPULSE_PLUS: HardwareModel.ValueType # 135
|
||||
"""
|
||||
Lilygo T-Impulse-Plus
|
||||
"""
|
||||
T_ECHO_CARD: HardwareModel.ValueType # 136
|
||||
"""
|
||||
Lilygo T-Echo Card
|
||||
"""
|
||||
SEEED_WIO_TRACKER_L2: HardwareModel.ValueType # 137
|
||||
"""
|
||||
Seeed Tracker L2
|
||||
"""
|
||||
CROWPANEL_P4: HardwareModel.ValueType # 138
|
||||
"""
|
||||
Elecrow CrowPanel Advance P4 models, ESP32-P4 and TFT with SX1262 radio plugin
|
||||
"""
|
||||
HELTEC_MESH_TOWER_V2: HardwareModel.ValueType # 139
|
||||
"""
|
||||
Heltec Mesh Tower V2
|
||||
"""
|
||||
MESHNOLOGY_W10: HardwareModel.ValueType # 140
|
||||
"""
|
||||
Meshnology W10
|
||||
"""
|
||||
PRIVATE_HW: HardwareModel.ValueType # 255
|
||||
"""
|
||||
------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -1972,6 +2122,11 @@ class Routing(google.protobuf.message.Message):
|
||||
Airtime fairness rate limit exceeded for a packet
|
||||
This typically enforced per portnum and is used to prevent a single node from monopolizing airtime
|
||||
"""
|
||||
PKI_SEND_FAIL_PUBLIC_KEY: Routing._Error.ValueType # 39
|
||||
"""
|
||||
PKI encryption failed, due to no public key for the remote node
|
||||
This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet
|
||||
"""
|
||||
|
||||
class Error(_Error, metaclass=_ErrorEnumTypeWrapper):
|
||||
"""
|
||||
@@ -2050,6 +2205,11 @@ class Routing(google.protobuf.message.Message):
|
||||
Airtime fairness rate limit exceeded for a packet
|
||||
This typically enforced per portnum and is used to prevent a single node from monopolizing airtime
|
||||
"""
|
||||
PKI_SEND_FAIL_PUBLIC_KEY: Routing.Error.ValueType # 39
|
||||
"""
|
||||
PKI encryption failed, due to no public key for the remote node
|
||||
This is different from PKI_UNKNOWN_PUBKEY which indicates a failure upon receiving a packet
|
||||
"""
|
||||
|
||||
ROUTE_REQUEST_FIELD_NUMBER: builtins.int
|
||||
ROUTE_REPLY_FIELD_NUMBER: builtins.int
|
||||
@@ -2281,6 +2441,7 @@ class StoreForwardPlusPlus(google.protobuf.message.Message):
|
||||
ENCAPSULATED_TO_FIELD_NUMBER: builtins.int
|
||||
ENCAPSULATED_FROM_FIELD_NUMBER: builtins.int
|
||||
ENCAPSULATED_RXTIME_FIELD_NUMBER: builtins.int
|
||||
CHAIN_COUNT_FIELD_NUMBER: builtins.int
|
||||
sfpp_message_type: global___StoreForwardPlusPlus.SFPP_message_type.ValueType
|
||||
"""
|
||||
Which message type is this
|
||||
@@ -2317,6 +2478,10 @@ class StoreForwardPlusPlus(google.protobuf.message.Message):
|
||||
"""
|
||||
The receive time of the message in question
|
||||
"""
|
||||
chain_count: builtins.int
|
||||
"""
|
||||
Used in a LINK_REQUEST to specify the message X spots back from head
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -2329,11 +2494,132 @@ class StoreForwardPlusPlus(google.protobuf.message.Message):
|
||||
encapsulated_to: builtins.int = ...,
|
||||
encapsulated_from: builtins.int = ...,
|
||||
encapsulated_rxtime: builtins.int = ...,
|
||||
chain_count: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["commit_hash", b"commit_hash", "encapsulated_from", b"encapsulated_from", "encapsulated_id", b"encapsulated_id", "encapsulated_rxtime", b"encapsulated_rxtime", "encapsulated_to", b"encapsulated_to", "message", b"message", "message_hash", b"message_hash", "root_hash", b"root_hash", "sfpp_message_type", b"sfpp_message_type"]) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["chain_count", b"chain_count", "commit_hash", b"commit_hash", "encapsulated_from", b"encapsulated_from", "encapsulated_id", b"encapsulated_id", "encapsulated_rxtime", b"encapsulated_rxtime", "encapsulated_to", b"encapsulated_to", "message", b"message", "message_hash", b"message_hash", "root_hash", b"root_hash", "sfpp_message_type", b"sfpp_message_type"]) -> None: ...
|
||||
|
||||
global___StoreForwardPlusPlus = StoreForwardPlusPlus
|
||||
|
||||
@typing.final
|
||||
class RemoteShell(google.protobuf.message.Message):
|
||||
"""
|
||||
The actual over-the-mesh message doing RemoteShell
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _OpCode:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _OpCodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[RemoteShell._OpCode.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
OP_UNSET: RemoteShell._OpCode.ValueType # 0
|
||||
OPEN: RemoteShell._OpCode.ValueType # 1
|
||||
"""Client -> server"""
|
||||
INPUT: RemoteShell._OpCode.ValueType # 2
|
||||
RESIZE: RemoteShell._OpCode.ValueType # 3
|
||||
CLOSE: RemoteShell._OpCode.ValueType # 4
|
||||
PING: RemoteShell._OpCode.ValueType # 5
|
||||
ACK: RemoteShell._OpCode.ValueType # 6
|
||||
OPEN_OK: RemoteShell._OpCode.ValueType # 64
|
||||
"""Server -> client"""
|
||||
OUTPUT: RemoteShell._OpCode.ValueType # 65
|
||||
CLOSED: RemoteShell._OpCode.ValueType # 66
|
||||
ERROR: RemoteShell._OpCode.ValueType # 67
|
||||
PONG: RemoteShell._OpCode.ValueType # 68
|
||||
|
||||
class OpCode(_OpCode, metaclass=_OpCodeEnumTypeWrapper):
|
||||
"""
|
||||
Frame op code for PTY session control and stream transport.
|
||||
|
||||
Values 1-63 are client->server requests.
|
||||
Values 64-127 are server->client responses/events.
|
||||
"""
|
||||
|
||||
OP_UNSET: RemoteShell.OpCode.ValueType # 0
|
||||
OPEN: RemoteShell.OpCode.ValueType # 1
|
||||
"""Client -> server"""
|
||||
INPUT: RemoteShell.OpCode.ValueType # 2
|
||||
RESIZE: RemoteShell.OpCode.ValueType # 3
|
||||
CLOSE: RemoteShell.OpCode.ValueType # 4
|
||||
PING: RemoteShell.OpCode.ValueType # 5
|
||||
ACK: RemoteShell.OpCode.ValueType # 6
|
||||
OPEN_OK: RemoteShell.OpCode.ValueType # 64
|
||||
"""Server -> client"""
|
||||
OUTPUT: RemoteShell.OpCode.ValueType # 65
|
||||
CLOSED: RemoteShell.OpCode.ValueType # 66
|
||||
ERROR: RemoteShell.OpCode.ValueType # 67
|
||||
PONG: RemoteShell.OpCode.ValueType # 68
|
||||
|
||||
OP_FIELD_NUMBER: builtins.int
|
||||
SESSION_ID_FIELD_NUMBER: builtins.int
|
||||
SEQ_FIELD_NUMBER: builtins.int
|
||||
ACK_SEQ_FIELD_NUMBER: builtins.int
|
||||
PAYLOAD_FIELD_NUMBER: builtins.int
|
||||
COLS_FIELD_NUMBER: builtins.int
|
||||
ROWS_FIELD_NUMBER: builtins.int
|
||||
FLAGS_FIELD_NUMBER: builtins.int
|
||||
LAST_TX_SEQ_FIELD_NUMBER: builtins.int
|
||||
LAST_RX_SEQ_FIELD_NUMBER: builtins.int
|
||||
op: global___RemoteShell.OpCode.ValueType
|
||||
"""
|
||||
Structured frame operation.
|
||||
"""
|
||||
session_id: builtins.int
|
||||
"""
|
||||
Logical PTY session identifier.
|
||||
"""
|
||||
seq: builtins.int
|
||||
"""
|
||||
Monotonic sequence number for this frame.
|
||||
"""
|
||||
ack_seq: builtins.int
|
||||
"""
|
||||
Cumulative ack sequence number.
|
||||
"""
|
||||
payload: builtins.bytes
|
||||
"""
|
||||
Opaque bytes payload for INPUT/OUTPUT/ERROR and other frame bodies.
|
||||
"""
|
||||
cols: builtins.int
|
||||
"""
|
||||
Terminal size columns used for OPEN/RESIZE signaling.
|
||||
"""
|
||||
rows: builtins.int
|
||||
"""
|
||||
Terminal size rows used for OPEN/RESIZE signaling.
|
||||
"""
|
||||
flags: builtins.int
|
||||
"""
|
||||
Bit flags for protocol extensions.
|
||||
"""
|
||||
last_tx_seq: builtins.int
|
||||
"""
|
||||
The last sequence number TX'd.
|
||||
"""
|
||||
last_rx_seq: builtins.int
|
||||
"""
|
||||
The last sequence number RX'd.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
op: global___RemoteShell.OpCode.ValueType = ...,
|
||||
session_id: builtins.int = ...,
|
||||
seq: builtins.int = ...,
|
||||
ack_seq: builtins.int = ...,
|
||||
payload: builtins.bytes = ...,
|
||||
cols: builtins.int = ...,
|
||||
rows: builtins.int = ...,
|
||||
flags: builtins.int = ...,
|
||||
last_tx_seq: builtins.int = ...,
|
||||
last_rx_seq: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["ack_seq", b"ack_seq", "cols", b"cols", "flags", b"flags", "last_rx_seq", b"last_rx_seq", "last_tx_seq", b"last_tx_seq", "op", b"op", "payload", b"payload", "rows", b"rows", "seq", b"seq", "session_id", b"session_id"]) -> None: ...
|
||||
|
||||
global___RemoteShell = RemoteShell
|
||||
|
||||
@typing.final
|
||||
class Waypoint(google.protobuf.message.Message):
|
||||
"""
|
||||
@@ -2404,6 +2690,25 @@ class Waypoint(google.protobuf.message.Message):
|
||||
|
||||
global___Waypoint = Waypoint
|
||||
|
||||
@typing.final
|
||||
class StatusMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
Message for node status
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
STATUS_FIELD_NUMBER: builtins.int
|
||||
status: builtins.str
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
status: builtins.str = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["status", b"status"]) -> None: ...
|
||||
|
||||
global___StatusMessage = StatusMessage
|
||||
|
||||
@typing.final
|
||||
class MqttClientProxyMessage(google.protobuf.message.Message):
|
||||
"""
|
||||
@@ -2895,6 +3200,7 @@ class NodeInfo(google.protobuf.message.Message):
|
||||
IS_FAVORITE_FIELD_NUMBER: builtins.int
|
||||
IS_IGNORED_FIELD_NUMBER: builtins.int
|
||||
IS_KEY_MANUALLY_VERIFIED_FIELD_NUMBER: builtins.int
|
||||
IS_MUTED_FIELD_NUMBER: builtins.int
|
||||
num: builtins.int
|
||||
"""
|
||||
The node number
|
||||
@@ -2942,6 +3248,11 @@ class NodeInfo(google.protobuf.message.Message):
|
||||
Persists between NodeDB internal clean ups
|
||||
LSB 0 of the bitfield
|
||||
"""
|
||||
is_muted: builtins.bool
|
||||
"""
|
||||
True if node has been muted
|
||||
Persistes between NodeDB internal clean ups
|
||||
"""
|
||||
@property
|
||||
def user(self) -> global___User:
|
||||
"""
|
||||
@@ -2976,9 +3287,10 @@ class NodeInfo(google.protobuf.message.Message):
|
||||
is_favorite: builtins.bool = ...,
|
||||
is_ignored: builtins.bool = ...,
|
||||
is_key_manually_verified: builtins.bool = ...,
|
||||
is_muted: builtins.bool = ...,
|
||||
) -> 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", "is_key_manually_verified", b"is_key_manually_verified", "last_heard", b"last_heard", "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", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "is_key_manually_verified", b"is_key_manually_verified", "is_muted", b"is_muted", "last_heard", b"last_heard", "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___NodeInfo = NodeInfo
|
||||
@@ -3216,6 +3528,7 @@ class FromRadio(google.protobuf.message.Message):
|
||||
FILEINFO_FIELD_NUMBER: builtins.int
|
||||
CLIENTNOTIFICATION_FIELD_NUMBER: builtins.int
|
||||
DEVICEUICONFIG_FIELD_NUMBER: builtins.int
|
||||
LOCKDOWN_STATUS_FIELD_NUMBER: builtins.int
|
||||
id: builtins.int
|
||||
"""
|
||||
The packet id, used to allow the phone to request missing read packets from the FIFO,
|
||||
@@ -3321,6 +3634,16 @@ class FromRadio(google.protobuf.message.Message):
|
||||
Persistent data for device-ui
|
||||
"""
|
||||
|
||||
@property
|
||||
def lockdown_status(self) -> global___LockdownStatus:
|
||||
"""
|
||||
Lockdown state notification for hardened firmware builds.
|
||||
Sent post-config (so unauthorized clients learn they must
|
||||
provision/unlock) and after each LockdownAuth admin command
|
||||
to report success or failure. Replaces the earlier scheme of
|
||||
encoding state as magic-string prefixes inside ClientNotification.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -3341,13 +3664,133 @@ class FromRadio(google.protobuf.message.Message):
|
||||
fileInfo: global___FileInfo | None = ...,
|
||||
clientNotification: global___ClientNotification | None = ...,
|
||||
deviceuiConfig: meshtastic.protobuf.device_ui_pb2.DeviceUIConfig | None = ...,
|
||||
lockdown_status: global___LockdownStatus | None = ...,
|
||||
) -> None: ...
|
||||
def HasField(self, field_name: typing.Literal["channel", b"channel", "clientNotification", b"clientNotification", "config", b"config", "config_complete_id", b"config_complete_id", "deviceuiConfig", b"deviceuiConfig", "fileInfo", b"fileInfo", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["channel", b"channel", "clientNotification", b"clientNotification", "config", b"config", "config_complete_id", b"config_complete_id", "deviceuiConfig", b"deviceuiConfig", "fileInfo", b"fileInfo", "id", b"id", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["packet", "my_info", "node_info", "config", "log_record", "config_complete_id", "rebooted", "moduleConfig", "channel", "queueStatus", "xmodemPacket", "metadata", "mqttClientProxyMessage", "fileInfo", "clientNotification", "deviceuiConfig"] | None: ...
|
||||
def HasField(self, field_name: typing.Literal["channel", b"channel", "clientNotification", b"clientNotification", "config", b"config", "config_complete_id", b"config_complete_id", "deviceuiConfig", b"deviceuiConfig", "fileInfo", b"fileInfo", "lockdown_status", b"lockdown_status", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> builtins.bool: ...
|
||||
def ClearField(self, field_name: typing.Literal["channel", b"channel", "clientNotification", b"clientNotification", "config", b"config", "config_complete_id", b"config_complete_id", "deviceuiConfig", b"deviceuiConfig", "fileInfo", b"fileInfo", "id", b"id", "lockdown_status", b"lockdown_status", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> None: ...
|
||||
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["packet", "my_info", "node_info", "config", "log_record", "config_complete_id", "rebooted", "moduleConfig", "channel", "queueStatus", "xmodemPacket", "metadata", "mqttClientProxyMessage", "fileInfo", "clientNotification", "deviceuiConfig", "lockdown_status"] | None: ...
|
||||
|
||||
global___FromRadio = FromRadio
|
||||
|
||||
@typing.final
|
||||
class LockdownStatus(google.protobuf.message.Message):
|
||||
"""
|
||||
Lockdown state report from firmware to client (for hardened builds
|
||||
with MESHTASTIC_LOCKDOWN). Sent immediately after config_complete_id
|
||||
to inform a freshly-connected unauthorized client what it must do,
|
||||
and again in response to each LockdownAuth admin command.
|
||||
"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _State:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _StateEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[LockdownStatus._State.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
STATE_UNSPECIFIED: LockdownStatus._State.ValueType # 0
|
||||
"""Default; should not be sent."""
|
||||
NEEDS_PROVISION: LockdownStatus._State.ValueType # 1
|
||||
"""
|
||||
No passphrase has ever been provisioned on this device.
|
||||
Client should prompt the operator to set one.
|
||||
"""
|
||||
LOCKED: LockdownStatus._State.ValueType # 2
|
||||
"""
|
||||
Storage is locked or this client has not authenticated yet.
|
||||
lock_reason carries a machine-readable detail string.
|
||||
Client should present (or auto-replay) a passphrase via
|
||||
AdminMessage.lockdown_auth.
|
||||
"""
|
||||
UNLOCKED: LockdownStatus._State.ValueType # 3
|
||||
"""
|
||||
Passphrase accepted; client is now authorized for this connection.
|
||||
boots_remaining and valid_until_epoch describe the active session
|
||||
token's TTL.
|
||||
"""
|
||||
UNLOCK_FAILED: LockdownStatus._State.ValueType # 4
|
||||
"""
|
||||
Passphrase rejected. backoff_seconds is non-zero when rate-limited.
|
||||
"""
|
||||
|
||||
class State(_State, metaclass=_StateEnumTypeWrapper): ...
|
||||
STATE_UNSPECIFIED: LockdownStatus.State.ValueType # 0
|
||||
"""Default; should not be sent."""
|
||||
NEEDS_PROVISION: LockdownStatus.State.ValueType # 1
|
||||
"""
|
||||
No passphrase has ever been provisioned on this device.
|
||||
Client should prompt the operator to set one.
|
||||
"""
|
||||
LOCKED: LockdownStatus.State.ValueType # 2
|
||||
"""
|
||||
Storage is locked or this client has not authenticated yet.
|
||||
lock_reason carries a machine-readable detail string.
|
||||
Client should present (or auto-replay) a passphrase via
|
||||
AdminMessage.lockdown_auth.
|
||||
"""
|
||||
UNLOCKED: LockdownStatus.State.ValueType # 3
|
||||
"""
|
||||
Passphrase accepted; client is now authorized for this connection.
|
||||
boots_remaining and valid_until_epoch describe the active session
|
||||
token's TTL.
|
||||
"""
|
||||
UNLOCK_FAILED: LockdownStatus.State.ValueType # 4
|
||||
"""
|
||||
Passphrase rejected. backoff_seconds is non-zero when rate-limited.
|
||||
"""
|
||||
|
||||
STATE_FIELD_NUMBER: builtins.int
|
||||
LOCK_REASON_FIELD_NUMBER: builtins.int
|
||||
BOOTS_REMAINING_FIELD_NUMBER: builtins.int
|
||||
VALID_UNTIL_EPOCH_FIELD_NUMBER: builtins.int
|
||||
BACKOFF_SECONDS_FIELD_NUMBER: builtins.int
|
||||
state: global___LockdownStatus.State.ValueType
|
||||
"""Current lockdown state being reported."""
|
||||
lock_reason: builtins.str
|
||||
"""
|
||||
For LOCKED: machine-readable reason. Known values:
|
||||
"needs_auth" — storage already unlocked, client must auth
|
||||
"token_missing" — no boot token on flash
|
||||
"token_expired" — boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" — boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" — token tampered or wrong device
|
||||
"token_dek_fail" — token DEK decrypt failed
|
||||
"token_wrong_size" — token file corrupted
|
||||
"token_bad_magic" — token file corrupted
|
||||
"not_provisioned" — should generally use NEEDS_PROVISION state instead
|
||||
Other values may be added; clients should treat unknown values as
|
||||
"locked, ask for passphrase".
|
||||
"""
|
||||
boots_remaining: builtins.int
|
||||
"""
|
||||
For UNLOCKED: remaining boots on the issued session token.
|
||||
Decrements by 1 on each subsequent boot.
|
||||
"""
|
||||
valid_until_epoch: builtins.int
|
||||
"""
|
||||
For UNLOCKED: wall-clock expiry of the issued session token,
|
||||
absolute Unix-epoch seconds. 0 = no time limit.
|
||||
"""
|
||||
backoff_seconds: builtins.int
|
||||
"""
|
||||
For UNLOCK_FAILED: seconds the client must wait before another
|
||||
passphrase attempt will be accepted. 0 = wrong passphrase, no
|
||||
backoff (immediate retry allowed but advisable to prompt user).
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
state: global___LockdownStatus.State.ValueType = ...,
|
||||
lock_reason: builtins.str = ...,
|
||||
boots_remaining: builtins.int = ...,
|
||||
valid_until_epoch: builtins.int = ...,
|
||||
backoff_seconds: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing.Literal["backoff_seconds", b"backoff_seconds", "boots_remaining", b"boots_remaining", "lock_reason", b"lock_reason", "state", b"state", "valid_until_epoch", b"valid_until_epoch"]) -> None: ...
|
||||
|
||||
global___LockdownStatus = LockdownStatus
|
||||
|
||||
@typing.final
|
||||
class ClientNotification(google.protobuf.message.Message):
|
||||
"""
|
||||
|
||||
148
meshtastic/protobuf/module_config_pb2.py
generated
148
meshtastic/protobuf/module_config_pb2.py
generated
File diff suppressed because one or more lines are too long
200
meshtastic/protobuf/module_config_pb2.pyi
generated
200
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
|
||||
"""
|
||||
@@ -492,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):
|
||||
"""
|
||||
@@ -568,6 +668,12 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
"""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):
|
||||
"""
|
||||
@@ -591,6 +697,12 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
"""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
|
||||
@@ -875,6 +987,7 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
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
|
||||
@@ -940,6 +1053,10 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
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,
|
||||
*,
|
||||
@@ -957,8 +1074,9 @@ class ModuleConfig(google.protobuf.message.Message):
|
||||
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_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: ...
|
||||
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):
|
||||
@@ -1164,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
|
||||
@@ -1177,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:
|
||||
"""
|
||||
@@ -1255,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,
|
||||
*,
|
||||
@@ -1271,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
|
||||
|
||||
|
||||
19
meshtastic/protobuf/mqtt_pb2.py
generated
19
meshtastic/protobuf/mqtt_pb2.py
generated
@@ -13,9 +13,10 @@ _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\"\x83\x04\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(\r\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')
|
||||
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)
|
||||
@@ -23,8 +24,16 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.mqtt_pb
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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=745
|
||||
_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)
|
||||
|
||||
4
meshtastic/protobuf/portnums_pb2.py
generated
4
meshtastic/protobuf/portnums_pb2.py
generated
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\x96\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\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\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\x0f\n\x0b\x43\x41YENNE_APP\x10M\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')
|
||||
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)
|
||||
@@ -22,5 +22,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
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=722
|
||||
_globals['_PORTNUM']._serialized_end=825
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
|
||||
56
meshtastic/protobuf/portnums_pb2.pyi
generated
56
meshtastic/protobuf/portnums_pb2.pyi
generated
@@ -101,6 +101,10 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
|
||||
"""
|
||||
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.
|
||||
@@ -124,6 +128,13 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
|
||||
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.
|
||||
@@ -190,6 +201,11 @@ 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
|
||||
@@ -201,6 +217,18 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
|
||||
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.
|
||||
@@ -312,6 +340,10 @@ 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.
|
||||
@@ -335,6 +367,13 @@ 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.
|
||||
@@ -401,6 +440,11 @@ 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
|
||||
@@ -412,6 +456,18 @@ App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes
|
||||
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.
|
||||
|
||||
9
meshtastic/protobuf/rtttl_pb2.py
generated
9
meshtastic/protobuf/rtttl_pb2.py
generated
@@ -11,9 +11,10 @@ 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(\tBg\n\x14org.meshtastic.protoB\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)
|
||||
@@ -21,6 +22,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.rtttl_p
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\021RTTTLConfigProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
|
||||
_globals['_RTTTLCONFIG']._serialized_start=56
|
||||
_globals['_RTTTLCONFIG']._serialized_end=87
|
||||
_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
|
||||
25
meshtastic/protobuf/storeforward_pb2.py
generated
25
meshtastic/protobuf/storeforward_pb2.py
generated
@@ -11,9 +11,10 @@ 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\x07variantBk\n\x14org.meshtastic.protoB\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)
|
||||
@@ -21,14 +22,16 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.storefo
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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
|
||||
_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
227
meshtastic/protobuf/telemetry_pb2.pyi
generated
227
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
|
||||
"""
|
||||
@@ -207,6 +209,38 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@@ -243,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
|
||||
"""
|
||||
@@ -263,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
|
||||
"""
|
||||
@@ -283,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
|
||||
"""
|
||||
@@ -397,6 +431,38 @@ 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
|
||||
@@ -486,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
|
||||
@@ -576,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,
|
||||
*,
|
||||
@@ -601,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
|
||||
@@ -1035,6 +1109,7 @@ class LocalStats(google.protobuf.message.Message):
|
||||
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)
|
||||
@@ -1093,6 +1168,10 @@ class LocalStats(google.protobuf.message.Message):
|
||||
"""
|
||||
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,
|
||||
*,
|
||||
@@ -1110,11 +1189,70 @@ class LocalStats(google.protobuf.message.Message):
|
||||
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", "heap_free_bytes", b"heap_free_bytes", "heap_total_bytes", b"heap_total_bytes", "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: ...
|
||||
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):
|
||||
"""
|
||||
@@ -1250,6 +1388,7 @@ class Telemetry(google.protobuf.message.Message):
|
||||
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
|
||||
@@ -1296,6 +1435,12 @@ class Telemetry(google.protobuf.message.Message):
|
||||
Linux host metrics
|
||||
"""
|
||||
|
||||
@property
|
||||
def traffic_management_stats(self) -> global___TrafficManagementStats:
|
||||
"""
|
||||
Traffic management statistics
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -1307,10 +1452,11 @@ class Telemetry(google.protobuf.message.Message):
|
||||
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", "host_metrics", b"host_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", "host_metrics", b"host_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", "host_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
|
||||
|
||||
@@ -1341,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
|
||||
|
||||
17
meshtastic/protobuf/xmodem_pb2.py
generated
17
meshtastic/protobuf/xmodem_pb2.py
generated
@@ -11,9 +11,10 @@ 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\x62\n\x14org.meshtastic.protoB\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)
|
||||
@@ -21,8 +22,14 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.xmodem_
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\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
|
||||
_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)
|
||||
|
||||
@@ -35,8 +35,6 @@ class SerialInterface(StreamInterface):
|
||||
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:
|
||||
@@ -52,22 +50,28 @@ class SerialInterface(StreamInterface):
|
||||
else:
|
||||
self.devPath = ports[0]
|
||||
|
||||
StreamInterface.__init__(
|
||||
self, debugOut=debugOut, noProto=noProto, connectNow=connectNow, noNodes=noNodes, timeout=timeout
|
||||
)
|
||||
|
||||
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(self.devPath, encoding="utf8") as f:
|
||||
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, timeout=timeout
|
||||
)
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Stream Interface base class
|
||||
"""
|
||||
import contextlib
|
||||
import io
|
||||
import logging
|
||||
import threading
|
||||
@@ -39,15 +40,14 @@ class StreamInterface(MeshInterface):
|
||||
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
|
||||
|
||||
@@ -61,9 +61,17 @@ class StreamInterface(MeshInterface):
|
||||
|
||||
# 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
|
||||
@@ -136,7 +144,16 @@ class StreamInterface(MeshInterface):
|
||||
# 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."""
|
||||
|
||||
@@ -4,14 +4,21 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
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"""
|
||||
|
||||
@@ -19,10 +26,10 @@ 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
|
||||
@@ -31,20 +38,19 @@ class TCPInterface(StreamInterface):
|
||||
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,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
rep = f"TCPInterface({self.hostname!r}"
|
||||
@@ -68,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"""
|
||||
logger.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"""
|
||||
"""Close a connection to the device."""
|
||||
logger.debug("Closing TCP stream")
|
||||
super().close()
|
||||
# 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"""
|
||||
@@ -99,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'':
|
||||
logger.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()
|
||||
|
||||
@@ -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
|
||||
|
||||
355
meshtastic/tests/firmware_harness.py
Normal file
355
meshtastic/tests/firmware_harness.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""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"]
|
||||
|
||||
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
@@ -757,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):
|
||||
@@ -1550,6 +1927,41 @@ def test_setOwner_valid_names(caplog):
|
||||
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)
|
||||
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 == ""
|
||||
128
meshtastic/tests/test_smokemesh.py
Normal file
128
meshtastic/tests/test_smokemesh.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Meshtastic smoke tests with multiple meshtasticd sim instances.
|
||||
|
||||
Uses the ``firmware_mesh`` session fixture which provides a 3-node chain
|
||||
topology (A-B-C) where A hears B, B hears A and C, and C hears B.
|
||||
The SIMULATOR_APP packet bridge forwards transmissions between nodes
|
||||
according to this topology.
|
||||
"""
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from .fw_helpers import (
|
||||
RECEIVE_TIMEOUT,
|
||||
subscribe_texts,
|
||||
subscribe_traceroutes,
|
||||
unsubscribe_all,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.smokemesh
|
||||
def test_smokemesh_node_db_convergence(firmware_mesh):
|
||||
"""Each node should see all 3 nodes in its node DB after convergence."""
|
||||
counts = [len(n.iface.nodes) for n in firmware_mesh.nodes if n.iface]
|
||||
if any(c < 3 for c in counts):
|
||||
pytest.skip(f"Mesh did not converge (counts={counts})")
|
||||
for i, node in enumerate(firmware_mesh.nodes):
|
||||
iface = node.iface
|
||||
assert iface is not None
|
||||
assert len(iface.nodes) >= 3, f"node {i} only sees {len(iface.nodes)} nodes"
|
||||
|
||||
|
||||
@pytest.mark.smokemesh
|
||||
def test_smokemesh_broadcast_text(firmware_mesh):
|
||||
"""A broadcast from node A should arrive on node B."""
|
||||
collector = subscribe_texts(firmware_mesh.get_iface(1))
|
||||
try:
|
||||
firmware_mesh.get_iface(0).sendText("hello mesh", wantAck=False)
|
||||
assert collector.wait_for(1)
|
||||
assert "hello mesh" in collector.texts
|
||||
finally:
|
||||
unsubscribe_all("meshtastic.receive.text")
|
||||
|
||||
|
||||
@pytest.mark.smokemesh
|
||||
def test_smokemesh_dm(firmware_mesh):
|
||||
"""A DM from node A to node B should arrive on B."""
|
||||
dest = firmware_mesh.get_node(1).node_num
|
||||
collector = subscribe_texts(firmware_mesh.get_iface(1))
|
||||
try:
|
||||
firmware_mesh.get_iface(0).sendText(
|
||||
"hey B", destinationId=dest, wantAck=False
|
||||
)
|
||||
assert collector.wait_for(1)
|
||||
assert "hey B" in collector.texts
|
||||
finally:
|
||||
unsubscribe_all("meshtastic.receive.text")
|
||||
|
||||
|
||||
@pytest.mark.smokemesh
|
||||
def test_smokemesh_dm_across_relay(firmware_mesh):
|
||||
"""A DM from node A to node C must relay through B (chain topology)."""
|
||||
dest = firmware_mesh.get_node(2).node_num
|
||||
collector = subscribe_texts(firmware_mesh.get_iface(2))
|
||||
try:
|
||||
firmware_mesh.get_iface(0).sendText(
|
||||
"relay test", destinationId=dest, wantAck=False
|
||||
)
|
||||
assert collector.wait_for(1), "node C did not receive the DM within timeout"
|
||||
assert "relay test" in collector.texts
|
||||
finally:
|
||||
unsubscribe_all("meshtastic.receive.text")
|
||||
|
||||
|
||||
@pytest.mark.smokemesh
|
||||
def test_smokemesh_hop_limit_prevents_relay(firmware_mesh):
|
||||
"""A broadcast with hopLimit=0 from A reaches B but B does not relay to C."""
|
||||
col_b = subscribe_texts(firmware_mesh.get_iface(1))
|
||||
col_c = subscribe_texts(firmware_mesh.get_iface(2))
|
||||
try:
|
||||
firmware_mesh.get_iface(0).sendText(
|
||||
"hop0", wantAck=False, hopLimit=0
|
||||
)
|
||||
assert col_b.wait_for(1), "B should receive A's broadcast"
|
||||
assert "hop0" in col_b.texts, "B got wrong text"
|
||||
|
||||
time.sleep(RECEIVE_TIMEOUT)
|
||||
assert "hop0" not in col_c.texts, (
|
||||
"C should NOT receive — B must not relay hopLimit=0"
|
||||
)
|
||||
finally:
|
||||
unsubscribe_all("meshtastic.receive.text")
|
||||
|
||||
|
||||
@pytest.mark.smokemesh
|
||||
def test_smokemesh_show_nodes(firmware_mesh):
|
||||
"""showNodes should report the other nodes in the mesh."""
|
||||
for i in range(3):
|
||||
iface = firmware_mesh.get_iface(i)
|
||||
iface.showNodes()
|
||||
|
||||
|
||||
@pytest.mark.smokemesh
|
||||
def test_smokemesh_traceroute_across_relay(firmware_mesh):
|
||||
"""Traceroute from A to C should show route via B in both directions."""
|
||||
col_a = subscribe_traceroutes(firmware_mesh.get_iface(0))
|
||||
col_c = subscribe_traceroutes(firmware_mesh.get_iface(2))
|
||||
try:
|
||||
src_a = firmware_mesh.get_node(0).node_num
|
||||
dest_c = firmware_mesh.get_node(2).node_num
|
||||
node_b = firmware_mesh.get_node(1).node_num
|
||||
|
||||
firmware_mesh.get_iface(0).sendTraceRoute(dest=dest_c, hopLimit=3)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
assert len(col_a.traceroutes) >= 1, "A did not receive traceroute response"
|
||||
a_resp = col_a.traceroutes[0]
|
||||
assert a_resp["from"] == dest_c, "response source should be C"
|
||||
|
||||
route = a_resp["decoded"]["traceroute"]
|
||||
assert route.get("route") == [node_b], "forward route should be A→B→C"
|
||||
assert route.get("routeBack") == [node_b], "return route should be C→B→A"
|
||||
|
||||
assert len(col_c.traceroutes) >= 1, "C did not receive traceroute request"
|
||||
c_req = col_c.traceroutes[0]
|
||||
assert c_req["from"] == src_a, "request source should be A"
|
||||
finally:
|
||||
unsubscribe_all("meshtastic.receive.traceroute")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,115 @@ def test_StreamInterface():
|
||||
"""Test that we cannot instantiate a StreamInterface based on noProto"""
|
||||
with pytest.raises(Exception) as pytest_wrapped_e:
|
||||
StreamInterface()
|
||||
assert pytest_wrapped_e.type == Exception
|
||||
assert pytest_wrapped_e.type == RuntimeError
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_close_safe_when_thread_never_started():
|
||||
"""close() must not raise RuntimeError when called before connect() has started the reader.
|
||||
|
||||
Hits the cleanup path used by __init__ when the handshake raises before the
|
||||
reader thread is started.
|
||||
"""
|
||||
iface = StreamInterface(noProto=True, connectNow=False)
|
||||
iface.stream = MagicMock()
|
||||
# _rxThread was created in __init__ but never .start()'d. close() should
|
||||
# detect that and skip join() instead of raising RuntimeError.
|
||||
iface.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_close_when_thread_never_started_closes_stream():
|
||||
"""If no reader thread was started, close() should still close the stream."""
|
||||
iface = StreamInterface(noProto=True, connectNow=False)
|
||||
stream = MagicMock()
|
||||
iface.stream = stream
|
||||
iface.close()
|
||||
stream.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_init_cleans_up_when_connect_raises():
|
||||
"""If connect() raises during __init__, close() runs and the original exception propagates."""
|
||||
|
||||
cleanup_calls = []
|
||||
|
||||
class FailingConnectStream(StreamInterface):
|
||||
"""Subclass whose connect() raises, to exercise the __init__ cleanup path."""
|
||||
|
||||
def __init__(self):
|
||||
self.stream = MagicMock() # bypass StreamInterface abstract check
|
||||
super().__init__(noProto=False, connectNow=True)
|
||||
|
||||
def connect(self):
|
||||
raise RuntimeError("simulated handshake failure")
|
||||
|
||||
def close(self):
|
||||
cleanup_calls.append("close")
|
||||
super().close()
|
||||
|
||||
with pytest.raises(RuntimeError, match="simulated handshake failure"):
|
||||
FailingConnectStream()
|
||||
assert cleanup_calls == ["close"], "close() should be invoked exactly once on handshake failure"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_init_cleans_up_when_waitForConfig_raises():
|
||||
"""If waitForConfig() raises after a successful connect(), close() runs and exception propagates."""
|
||||
|
||||
cleanup_calls = []
|
||||
|
||||
class FailingWaitStream(StreamInterface):
|
||||
"""Subclass whose waitForConfig() raises, to exercise the second leg of cleanup."""
|
||||
|
||||
def __init__(self):
|
||||
self.stream = MagicMock()
|
||||
super().__init__(noProto=False, connectNow=True)
|
||||
|
||||
def connect(self):
|
||||
# No-op connect — we are simulating handshake-stage failure, not connect-stage.
|
||||
pass
|
||||
|
||||
def waitForConfig(self):
|
||||
raise TimeoutError("simulated config-handshake timeout")
|
||||
|
||||
def close(self):
|
||||
cleanup_calls.append("close")
|
||||
super().close()
|
||||
|
||||
with pytest.raises(TimeoutError, match="simulated config-handshake timeout"):
|
||||
FailingWaitStream()
|
||||
assert cleanup_calls == ["close"], "close() should be invoked exactly once on handshake timeout"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.usefixtures("reset_mt_config")
|
||||
def test_StreamInterface_init_cleanup_does_not_shadow_original_exception():
|
||||
"""If close() itself raises during __init__ cleanup, the original exception still propagates.
|
||||
|
||||
The cleanup uses contextlib.suppress(Exception) so that a secondary failure
|
||||
in close() doesn't replace the real reason for the failed handshake.
|
||||
"""
|
||||
|
||||
class CleanupRaisesStream(StreamInterface):
|
||||
"""Helper stream that raises during close() to verify exception precedence."""
|
||||
|
||||
def __init__(self):
|
||||
self.stream = MagicMock()
|
||||
super().__init__(noProto=False, connectNow=True)
|
||||
|
||||
def connect(self):
|
||||
raise RuntimeError("original handshake failure")
|
||||
|
||||
def close(self):
|
||||
raise RuntimeError("secondary close failure — should be suppressed")
|
||||
|
||||
with pytest.raises(RuntimeError, match="original handshake failure"):
|
||||
CleanupRaisesStream()
|
||||
|
||||
|
||||
# Note: This takes a bit, so moving from unit to slow
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Meshtastic unit tests for tcp_interface.py"""
|
||||
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
import socket
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -54,3 +55,116 @@ def test_TCPInterface_without_connecting():
|
||||
with patch("socket.socket"):
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
assert iface.socket is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_close_shutdowns_socket_before_super_close():
|
||||
"""Close should unblock socket reads before waiting on StreamInterface.close()."""
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
sock = MagicMock()
|
||||
iface.socket = sock
|
||||
call_order = []
|
||||
|
||||
with patch.object(TCPInterface, "_socket_shutdown", autospec=True) as mock_shutdown:
|
||||
with patch(
|
||||
"meshtastic.stream_interface.StreamInterface.close", autospec=True
|
||||
) as mock_super_close:
|
||||
mock_shutdown.side_effect = lambda _self: call_order.append("shutdown")
|
||||
mock_super_close.side_effect = lambda _self: call_order.append("super_close")
|
||||
|
||||
iface.close()
|
||||
|
||||
assert call_order == ["shutdown", "super_close"]
|
||||
sock.close.assert_called_once()
|
||||
assert iface.socket is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_close_half_closes_before_shutdown():
|
||||
"""Close should send FIN and let the device drain before forcing the link down.
|
||||
|
||||
Going straight to shutdown(SHUT_RDWR)/close() with data still unread makes the
|
||||
stack send RST, and Winsock then discards data the device had received but not
|
||||
yet read, silently losing writes such as `--set`.
|
||||
"""
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
sock = MagicMock()
|
||||
iface.socket = sock
|
||||
call_order = []
|
||||
|
||||
with patch.object(TCPInterface, "_socket_shutdown", autospec=True) as mock_shutdown:
|
||||
with patch.object(
|
||||
TCPInterface, "_wait_for_reader_exit", autospec=True
|
||||
) as mock_wait:
|
||||
with patch(
|
||||
"meshtastic.stream_interface.StreamInterface.close", autospec=True
|
||||
) as mock_super_close:
|
||||
sock.shutdown.side_effect = lambda how: call_order.append(f"shutdown_{how}")
|
||||
mock_wait.side_effect = lambda _self, _t: call_order.append("wait")
|
||||
mock_shutdown.side_effect = lambda _self: call_order.append("full_shutdown")
|
||||
mock_super_close.side_effect = lambda _self: call_order.append("super_close")
|
||||
|
||||
iface.close()
|
||||
|
||||
assert call_order == [
|
||||
f"shutdown_{socket.SHUT_WR}",
|
||||
"wait",
|
||||
"full_shutdown",
|
||||
"super_close",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_close_survives_half_close_failure():
|
||||
"""A peer that already vanished must not break close()."""
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
sock = MagicMock()
|
||||
sock.shutdown.side_effect = OSError("already gone")
|
||||
iface.socket = sock
|
||||
|
||||
with patch("meshtastic.stream_interface.StreamInterface.close", autospec=True):
|
||||
iface.close() # must not raise
|
||||
|
||||
sock.close.assert_called_once()
|
||||
assert iface.socket is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_reconnect():
|
||||
"""Test that _reconnect correctly reconnects"""
|
||||
with patch("socket.socket") as mock_socket:
|
||||
with patch("time.sleep"):
|
||||
iface = TCPInterface(hostname="localhost", noProto=True)
|
||||
old_socket = iface.socket
|
||||
assert old_socket is not None
|
||||
|
||||
iface._reconnect()
|
||||
|
||||
assert old_socket.close.called
|
||||
# We expect socket class to be instantiated at least twice (init + reconnect)
|
||||
assert mock_socket.call_count >= 2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_writeBytes_reconnects():
|
||||
"""Test that _writeBytes reconnects and re-raises on OSError."""
|
||||
with patch("socket.socket"):
|
||||
iface = TCPInterface(hostname="localhost", noProto=True)
|
||||
iface.socket.sendall.side_effect = OSError("Broken pipe")
|
||||
|
||||
with patch.object(iface, "_reconnect") as mock_reconnect:
|
||||
with pytest.raises(OSError, match="Broken pipe"):
|
||||
iface._writeBytes(b"some data")
|
||||
mock_reconnect.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_TCPInterface_readBytes_reconnects():
|
||||
"""Test that _readBytes calls _reconnect on empty bytes"""
|
||||
iface = TCPInterface(hostname="localhost", noProto=True, connectNow=False)
|
||||
iface.socket = MagicMock()
|
||||
iface.socket.recv.return_value = b""
|
||||
|
||||
with patch.object(iface, "_reconnect") as mock_reconnect:
|
||||
iface._readBytes(10)
|
||||
mock_reconnect.assert_called_once()
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from meshtastic.supported_device import SupportedDevice
|
||||
from meshtastic.protobuf import mesh_pb2
|
||||
from meshtastic.protobuf import mesh_pb2, config_pb2
|
||||
from meshtastic.util import (
|
||||
DEFAULT_KEY,
|
||||
Timeout,
|
||||
@@ -20,6 +22,8 @@ from meshtastic.util import (
|
||||
convert_mac_addr,
|
||||
eliminate_duplicate_port,
|
||||
findPorts,
|
||||
flags_from_list,
|
||||
flags_to_list,
|
||||
fixme,
|
||||
fromPSK,
|
||||
fromStr,
|
||||
@@ -37,6 +41,7 @@ from meshtastic.util import (
|
||||
stripnl,
|
||||
support_info,
|
||||
message_to_json,
|
||||
to_node_num,
|
||||
Acknowledgment
|
||||
)
|
||||
|
||||
@@ -234,20 +239,111 @@ def test_remove_keys_from_dict_nested():
|
||||
assert remove_keys_from_dict(keys, adict) == exp
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
def test_Timeout_not_found():
|
||||
"""Test Timeout()"""
|
||||
to = Timeout(0.2)
|
||||
attrs = "foo"
|
||||
to.waitForSet("bar", attrs)
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_waitForSet_found():
|
||||
"""waitForSet returns True when all required attrs are truthy on target."""
|
||||
to = Timeout(0.01)
|
||||
target = SimpleNamespace(foo=1, bar="ok")
|
||||
assert to.waitForSet(target, ("foo", "bar")) is True
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
def test_Timeout_found():
|
||||
"""Test Timeout()"""
|
||||
to = Timeout(0.2)
|
||||
attrs = ()
|
||||
to.waitForSet("bar", attrs)
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.util.time.sleep")
|
||||
def test_Timeout_waitForSet_not_found(mock_sleep): # pylint: disable=unused-argument
|
||||
"""waitForSet returns False when attrs remain falsy until timeout."""
|
||||
to = Timeout(0.01)
|
||||
target = SimpleNamespace(foo=None, bar=0)
|
||||
assert to.waitForSet(target, ("foo", "bar")) is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_waitForSet_empty_attrs():
|
||||
"""waitForSet returns True immediately when attrs is empty (vacuous truth)."""
|
||||
to = Timeout(0.01)
|
||||
assert to.waitForSet(object(), ()) is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_reset():
|
||||
"""reset() sets expireTime to now + expireTimeout."""
|
||||
to = Timeout(maxSecs=5)
|
||||
before = time.time()
|
||||
to.reset()
|
||||
assert to.expireTime == pytest.approx(before + 5, abs=0.1)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_reset_custom():
|
||||
"""reset(expireTimeout) overrides the stored expireTimeout."""
|
||||
to = Timeout(maxSecs=5)
|
||||
before = time.time()
|
||||
to.reset(expireTimeout=10)
|
||||
assert to.expireTime == pytest.approx(before + 10, abs=0.1)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_waitForAckNak_found():
|
||||
"""waitForAckNak returns True when any acknowledgment attr is set, and clears it."""
|
||||
to = Timeout(0.01)
|
||||
ack = Acknowledgment()
|
||||
ack.receivedAck = True
|
||||
assert to.waitForAckNak(ack) is True
|
||||
assert ack.receivedAck is False # cleared after detection
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.util.time.sleep")
|
||||
def test_Timeout_waitForAckNak_not_found(mock_sleep): # pylint: disable=unused-argument
|
||||
"""waitForAckNak returns False when no acknowledgment attr is set."""
|
||||
to = Timeout(0.01)
|
||||
ack = Acknowledgment()
|
||||
assert to.waitForAckNak(ack) is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_waitForTraceRoute_found():
|
||||
"""waitForTraceRoute returns True on receivedTraceRoute, and clears it."""
|
||||
to = Timeout(0.01)
|
||||
ack = Acknowledgment()
|
||||
ack.receivedTraceRoute = True
|
||||
assert to.waitForTraceRoute(3.0, ack) is True
|
||||
assert ack.receivedTraceRoute is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@patch("meshtastic.util.time.sleep")
|
||||
def test_Timeout_waitForTraceRoute_not_found(mock_sleep): # pylint: disable=unused-argument
|
||||
"""waitForTraceRoute returns False on timeout."""
|
||||
to = Timeout(0.01)
|
||||
ack = Acknowledgment()
|
||||
assert to.waitForTraceRoute(3.0, ack) is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_waitForTelemetry_found():
|
||||
"""waitForTelemetry returns True when receivedTelemetry is set."""
|
||||
to = Timeout(0.01)
|
||||
ack = Acknowledgment()
|
||||
ack.receivedTelemetry = True
|
||||
assert to.waitForTelemetry(ack) is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_waitForPosition_found():
|
||||
"""waitForPosition returns True when receivedPosition is set."""
|
||||
to = Timeout(0.01)
|
||||
ack = Acknowledgment()
|
||||
ack.receivedPosition = True
|
||||
assert to.waitForPosition(ack) is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_Timeout_waitForWaypoint_found():
|
||||
"""waitForWaypoint returns True when receivedWaypoint is set."""
|
||||
to = Timeout(0.01)
|
||||
ack = Acknowledgment()
|
||||
ack.receivedWaypoint = True
|
||||
assert to.waitForWaypoint(ack) is True
|
||||
|
||||
|
||||
@pytest.mark.unitslow
|
||||
@@ -715,3 +811,162 @@ def test_generate_channel_hash_fuzz_aes256(channel_name, key_bytes):
|
||||
"Test generate_channel_hash with fuzzed channel names and 256-bit keys, ensuring it produces single-byte values"
|
||||
hashed = generate_channel_hash(channel_name, key_bytes)
|
||||
assert 0 <= hashed <= 0xFF
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("input_val,expected", [
|
||||
# int passthrough
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(6, 6),
|
||||
(502009325, 502009325),
|
||||
(2198819370, 2198819370),
|
||||
(0xFFFFFFFF, 0xFFFFFFFF),
|
||||
# !hex format (always treated as hex)
|
||||
("!00000000", 0x00000000),
|
||||
("!00000001", 0x00000001),
|
||||
("!00000010", 0x00000010),
|
||||
("!000000ff", 0x000000FF),
|
||||
("!830f522a", 0x830F522A),
|
||||
("!1dec0ded", 0x1DEC0DED),
|
||||
("!ffffffff", 0xFFFFFFFF),
|
||||
("!FFFFFFFF", 0xFFFFFFFF),
|
||||
# 0xhex format
|
||||
("0x00000000", 0x00000000),
|
||||
("0x00000010", 0x00000010),
|
||||
("0x830f522a", 0x830F522A),
|
||||
("0x1dec0ded", 0x1DEC0DED),
|
||||
("0xFFFFFFFF", 0xFFFFFFFF),
|
||||
# Unprefixed hex string (falls back to hex when decimal fails)
|
||||
("830f522a", 0x830F522A),
|
||||
("1dec0ded", 0x1DEC0DED),
|
||||
# Decimal string
|
||||
("42", 42),
|
||||
("12345678", 12345678),
|
||||
("0", 0),
|
||||
("1", 1),
|
||||
# With whitespace
|
||||
(" !830f522a ", 2198819370),
|
||||
(" !00000010 ", 16),
|
||||
(" 0x830f522a ", 2198819370),
|
||||
])
|
||||
def test_to_node_num(input_val, expected):
|
||||
"""Test to_node_num with various valid inputs"""
|
||||
assert to_node_num(input_val) == expected
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("input_val", [
|
||||
"",
|
||||
"!",
|
||||
"!!",
|
||||
"!0x10",
|
||||
"!xyz",
|
||||
])
|
||||
def test_to_node_num_invalid(input_val):
|
||||
"""Test to_node_num raises ValueError for invalid inputs"""
|
||||
with pytest.raises(ValueError):
|
||||
to_node_num(input_val)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(st.integers(min_value=0, max_value=2**32 - 1))
|
||||
def test_to_node_num_hypothesis_roundtrip(n):
|
||||
"""Property: all supported input formats roundtrip for any valid node number"""
|
||||
assert to_node_num(n) == n
|
||||
assert to_node_num(f"!{n:08x}") == n
|
||||
assert to_node_num(f"0x{n:x}") == n
|
||||
assert to_node_num(str(n)) == n
|
||||
|
||||
|
||||
_EXCLUDED_MODULES = mesh_pb2.ExcludedModules
|
||||
_POSITION_FLAGS = config_pb2.Config.PositionConfig.PositionFlags
|
||||
_NETWORK_PROTOCOLS = config_pb2.Config.NetworkConfig.ProtocolFlags
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("flag_type,flags,expected", [
|
||||
(_EXCLUDED_MODULES, 0, []),
|
||||
(_EXCLUDED_MODULES, 1, ["MQTT_CONFIG"]),
|
||||
(_EXCLUDED_MODULES, 3, ["MQTT_CONFIG", "SERIAL_CONFIG"]),
|
||||
(_EXCLUDED_MODULES, 0x7FFF, [
|
||||
"MQTT_CONFIG", "SERIAL_CONFIG", "EXTNOTIF_CONFIG", "STOREFORWARD_CONFIG",
|
||||
"RANGETEST_CONFIG", "TELEMETRY_CONFIG", "CANNEDMSG_CONFIG", "AUDIO_CONFIG",
|
||||
"REMOTEHARDWARE_CONFIG", "NEIGHBORINFO_CONFIG", "AMBIENTLIGHTING_CONFIG",
|
||||
"DETECTIONSENSOR_CONFIG", "PAXCOUNTER_CONFIG", "BLUETOOTH_CONFIG",
|
||||
"NETWORK_CONFIG",
|
||||
]),
|
||||
(_EXCLUDED_MODULES, 0x8000, ["UNKNOWN_ADDITIONAL_FLAGS(32768)"]),
|
||||
(_EXCLUDED_MODULES, 0x8001, ["MQTT_CONFIG", "UNKNOWN_ADDITIONAL_FLAGS(32768)"]),
|
||||
(_POSITION_FLAGS, 0, []),
|
||||
(_POSITION_FLAGS, 0x09, ["ALTITUDE", "DOP"]),
|
||||
(_POSITION_FLAGS, 0x1FF, [
|
||||
"ALTITUDE", "ALTITUDE_MSL", "GEOIDAL_SEPARATION", "DOP", "HVDOP",
|
||||
"SATINVIEW", "SEQ_NO", "TIMESTAMP", "HEADING",
|
||||
]),
|
||||
])
|
||||
def test_flags_to_list(flag_type, flags, expected):
|
||||
"""Test flags_to_list decodes set bits in enum order and reports unknown remainders."""
|
||||
assert flags_to_list(flag_type, flags) == expected
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(st.integers(min_value=0, max_value=0xFFFFF))
|
||||
def test_flags_to_list_conservation(flags):
|
||||
"""Property: flags_to_list partitions `flags` into known names plus an exact unknown remainder.
|
||||
|
||||
Every known bit that is set must appear as a name, and the leftover reported in
|
||||
UNKNOWN_ADDITIONAL_FLAGS(...) must together with the named bits reconstruct the input.
|
||||
"""
|
||||
for flag_type in (_EXCLUDED_MODULES, _POSITION_FLAGS):
|
||||
known_union = 0
|
||||
for key in flag_type.keys():
|
||||
value = flag_type.Value(key)
|
||||
if key != "EXCLUDED_NONE" and value:
|
||||
known_union |= value
|
||||
|
||||
result = flags_to_list(flag_type, flags)
|
||||
|
||||
accounted = 0
|
||||
leftover = 0
|
||||
for name in result:
|
||||
if name.startswith("UNKNOWN_ADDITIONAL_FLAGS("):
|
||||
leftover = int(name[len("UNKNOWN_ADDITIONAL_FLAGS("):-1])
|
||||
else:
|
||||
accounted |= flag_type.Value(name)
|
||||
|
||||
assert accounted == (flags & known_union)
|
||||
assert (accounted | leftover) == flags
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("flag_type, flags, expected", [
|
||||
(_NETWORK_PROTOCOLS, ["UDP_BROADCAST"], 1),
|
||||
(_NETWORK_PROTOCOLS, ["NO_BROADCAST"], 0),
|
||||
(_NETWORK_PROTOCOLS, [], 0),
|
||||
(_POSITION_FLAGS, ["ALTITUDE"], 1),
|
||||
(_POSITION_FLAGS, ["ALTITUDE", "SPEED"], 513),
|
||||
(_POSITION_FLAGS, ["ALTITUDE", " SPEED "], 513),
|
||||
])
|
||||
def test_flags_from_list(flag_type, flags, expected):
|
||||
"""Test flags_from_list combines named flags into the expected bitmask."""
|
||||
assert flags_from_list(flag_type, flags) == expected
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_flags_from_list_unknown_flag():
|
||||
"""Test flags_from_list raises ValueError for an unknown flag name."""
|
||||
with pytest.raises(ValueError, match="Unknown flag 'TCP'"):
|
||||
flags_from_list(_NETWORK_PROTOCOLS, ["UDP_BROADCAST", "TCP"])
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@given(st.lists(st.sampled_from(list(_POSITION_FLAGS.keys())), unique=True))
|
||||
def test_flags_from_list_roundtrip(flags):
|
||||
"""Property: flags_from_list and flags_to_list are inverses for known position flags."""
|
||||
combined = flags_from_list(_POSITION_FLAGS, flags)
|
||||
decoded = flags_to_list(_POSITION_FLAGS, combined)
|
||||
# flags_to_list drops zero-value flags and may report unknown remainders,
|
||||
# but for combinations of known non-zero flags it should return the same set of names.
|
||||
nonzero_flags = {f for f in flags if _POSITION_FLAGS.Value(f)}
|
||||
assert set(decoded) == nonzero_flags
|
||||
|
||||
@@ -728,7 +728,7 @@ def to_node_num(node_id: Union[int, str]) -> int:
|
||||
return node_id
|
||||
s = str(node_id).strip()
|
||||
if s.startswith("!"):
|
||||
s = s[1:]
|
||||
s = "0x" + s[1:]
|
||||
if s.lower().startswith("0x"):
|
||||
return int(s, 16)
|
||||
try:
|
||||
@@ -737,14 +737,41 @@ def to_node_num(node_id: Union[int, str]) -> int:
|
||||
return int(s, 16)
|
||||
|
||||
def flags_to_list(flag_type, flags: int) -> List[str]:
|
||||
"""Given a flag_type that's a protobuf EnumTypeWrapper, and a flag int, give a list of flags enabled."""
|
||||
"""Given a flag_type that's a protobuf EnumTypeWrapper, and a flag int, give a list of flags enabled.
|
||||
|
||||
Zero-valued members (e.g. EXCLUDED_NONE, UNSET, NO_BROADCAST) never appear in the
|
||||
result: they hold no bit, so `flags & value` is always False for them, and a flags
|
||||
value of 0 therefore decodes to an empty list rather than a named "no flags" entry.
|
||||
Any leftover bits not corresponding to a known member are reported via
|
||||
`UNKNOWN_ADDITIONAL_FLAGS(<leftover>)`.
|
||||
"""
|
||||
ret = []
|
||||
for key in flag_type.keys():
|
||||
if key == "EXCLUDED_NONE":
|
||||
continue
|
||||
if flags & flag_type.Value(key):
|
||||
ret.append(key)
|
||||
flags = flags - flag_type.Value(key)
|
||||
if flags > 0:
|
||||
ret.append(f"UNKNOWN_ADDITIONAL_FLAGS({flags})")
|
||||
return ret
|
||||
|
||||
|
||||
def flags_from_list(flag_type, flags: List[str]) -> int:
|
||||
"""Given a flag_type that's a protobuf EnumTypeWrapper, and a list of flag names, return the combined bitmask.
|
||||
|
||||
Zero-valued members (e.g. EXCLUDED_NONE, UNSET, NO_BROADCAST) are accepted but are
|
||||
no-ops: they OR in 0 and thus set nothing. A list consisting solely of such a member
|
||||
(or an empty list) yields 0, which round-trips back through flags_to_list as an
|
||||
empty list rather than the original member name -- see flags_to_list's docstring.
|
||||
"""
|
||||
result = 0
|
||||
valid_names = list(flag_type.keys())
|
||||
for flag_name in flags:
|
||||
flag_name = flag_name.strip()
|
||||
if not flag_name:
|
||||
continue
|
||||
if flag_name not in valid_names:
|
||||
raise ValueError(
|
||||
f"Unknown flag '{flag_name}'. Valid choices: {', '.join(sorted(valid_names))}"
|
||||
)
|
||||
result |= flag_type.Value(flag_name)
|
||||
return result
|
||||
|
||||
438
poetry.lock
generated
438
poetry.lock
generated
@@ -1373,83 +1373,158 @@ dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.60.1"
|
||||
version = "4.60.2"
|
||||
description = "Tools to manipulate font files"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["analysis"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28"},
|
||||
{file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15"},
|
||||
{file = "fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c"},
|
||||
{file = "fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea"},
|
||||
{file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652"},
|
||||
{file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a"},
|
||||
{file = "fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce"},
|
||||
{file = "fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258"},
|
||||
{file = "fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6"},
|
||||
{file = "fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272"},
|
||||
{file = "fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259"},
|
||||
{file = "fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a"},
|
||||
{file = "fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217"},
|
||||
{file = "fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb"},
|
||||
{file = "fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96"},
|
||||
{file = "fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a"},
|
||||
{file = "fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70"},
|
||||
{file = "fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bb5fd231e56ccd7403212636dcccffc96c5ae0d6f9e4721fa0a32cb2e3ca432"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:536b5fab7b6fec78ccf59b5c59489189d9d0a8b0d3a77ed1858be59afb096696"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b9288fc38252ac86a9570f19313ecbc9ff678982e0f27c757a85f1f284d3400"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93fcb420791d839ef592eada2b69997c445d0ce9c969b5190f2e16828ec10607"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7916a381b094db4052ac284255186aebf74c5440248b78860cb41e300036f598"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58c8c393d5e16b15662cfc2d988491940458aa87894c662154f50c7b49440bef"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-win32.whl", hash = "sha256:19c6e0afd8b02008caa0aa08ab896dfce5d0bcb510c49b2c499541d5cb95a963"},
|
||||
{file = "fonttools-4.60.2-cp313-cp313-win_amd64.whl", hash = "sha256:6a500dc59e11b2338c2dba1f8cf11a4ae8be35ec24af8b2628b8759a61457b76"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9387c532acbe323bbf2a920f132bce3c408a609d5f9dcfc6532fbc7e37f8ccbb"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6f1c824185b5b8fb681297f315f26ae55abb0d560c2579242feea8236b1cfef"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:55a3129d1e4030b1a30260f1b32fe76781b585fb2111d04a988e141c09eb6403"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b196e63753abc33b3b97a6fd6de4b7c4fef5552c0a5ba5e562be214d1e9668e0"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de76c8d740fb55745f3b154f0470c56db92ae3be27af8ad6c2e88f1458260c9a"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ba6303225c95998c9fda2d410aa792c3d2c1390a09df58d194b03e17583fa25"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-win32.whl", hash = "sha256:0a89728ce10d7c816fedaa5380c06d2793e7a8a634d7ce16810e536c22047384"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa8446e6ab8bd778b82cb1077058a2addba86f30de27ab9cc18ed32b34bc8667"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4063bc81ac5a4137642865cb63dd270e37b3cd1f55a07c0d6e41d072699ccca2"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebfdb66fa69732ed604ab8e2a0431e6deff35e933a11d73418cbc7823d03b8e1"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50b10b3b1a72d1d54c61b0e59239e1a94c0958f4a06a1febf97ce75388dd91a4"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beae16891a13b4a2ddec9b39b4de76092a3025e4d1c82362e3042b62295d5e4d"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522f017fdb3766fd5d2d321774ef351cc6ce88ad4e6ac9efe643e4a2b9d528db"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee"},
|
||||
{file = "fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37"},
|
||||
{file = "fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501"},
|
||||
{file = "fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a"},
|
||||
{file = "fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
|
||||
all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
|
||||
graphite = ["lz4 (>=1.7.4.2)"]
|
||||
interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""]
|
||||
lxml = ["lxml (>=4.0)"]
|
||||
pathops = ["skia-pathops (>=0.5.0)"]
|
||||
plot = ["matplotlib"]
|
||||
repacker = ["uharfbuzz (>=0.23.0)"]
|
||||
repacker = ["uharfbuzz (>=0.45.0)"]
|
||||
symfont = ["sympy"]
|
||||
type1 = ["xattr ; sys_platform == \"darwin\""]
|
||||
unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""]
|
||||
unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""]
|
||||
woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.63.0"
|
||||
description = "Tools to manipulate font files"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["analysis"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b"},
|
||||
{file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94"},
|
||||
{file = "fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579"},
|
||||
{file = "fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22"},
|
||||
{file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e"},
|
||||
{file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69"},
|
||||
{file = "fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e"},
|
||||
{file = "fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb"},
|
||||
{file = "fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263"},
|
||||
{file = "fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380"},
|
||||
{file = "fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8"},
|
||||
{file = "fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419"},
|
||||
{file = "fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d"},
|
||||
{file = "fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
|
||||
graphite = ["lz4 (>=1.7.4.2)"]
|
||||
interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""]
|
||||
lxml = ["lxml (>=4.0)"]
|
||||
pathops = ["skia-pathops (>=0.5.0)"]
|
||||
plot = ["matplotlib"]
|
||||
repacker = ["uharfbuzz (>=0.45.0)"]
|
||||
symfont = ["sympy"]
|
||||
type1 = ["xattr ; sys_platform == \"darwin\""]
|
||||
unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""]
|
||||
woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"]
|
||||
|
||||
[[package]]
|
||||
@@ -1595,18 +1670,18 @@ zoneinfo = ["tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
version = "3.17"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "analysis"]
|
||||
files = [
|
||||
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
|
||||
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
|
||||
{file = "idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c"},
|
||||
{file = "idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
@@ -2158,14 +2233,15 @@ jupyter_server = ">=1.1.2"
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-server"
|
||||
version = "2.17.0"
|
||||
version = "2.18.2"
|
||||
description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["analysis"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f"},
|
||||
{file = "jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5"},
|
||||
{file = "jupyter_server-2.18.2-py3-none-any.whl", hash = "sha256:fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8"},
|
||||
{file = "jupyter_server-2.18.2.tar.gz", hash = "sha256:06b4f40d8a7a00bb39d5216859c81374a0e7cfefe6d8a5a7facc5a5c37c679a7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2190,9 +2266,47 @@ traitlets = ">=5.6.0"
|
||||
websocket-client = ">=1.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
|
||||
docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx (<9.0)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
|
||||
test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-server"
|
||||
version = "2.19.0"
|
||||
description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["analysis"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "jupyter_server-2.19.0-py3-none-any.whl", hash = "sha256:cb76591b76d7093584c2ad2ae72ac3d58614a4b597507a1bb04e1f9f683cf9ea"},
|
||||
{file = "jupyter_server-2.19.0.tar.gz", hash = "sha256:1731236bc32b680223e1ceb9d68209a845203475012ef68773a81434b46a31a7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.1.0"
|
||||
argon2-cffi = ">=21.1"
|
||||
jinja2 = ">=3.0.3"
|
||||
jupyter-client = ">=7.4.4"
|
||||
jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
|
||||
jupyter-events = ">=0.11.0"
|
||||
jupyter-server-terminals = ">=0.4.4"
|
||||
nbconvert = ">=6.4.4"
|
||||
nbformat = ">=5.3.0"
|
||||
overrides = {version = ">=5.0", markers = "python_version < \"3.12\""}
|
||||
packaging = ">=22.0"
|
||||
prometheus-client = ">=0.9"
|
||||
pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""}
|
||||
pyzmq = ">=24"
|
||||
send2trash = ">=1.8.2"
|
||||
terminado = ">=0.8.3"
|
||||
tornado = ">=6.2.0"
|
||||
traitlets = ">=5.6.0"
|
||||
websocket-client = ">=1.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx (<9.0)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"]
|
||||
test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<10)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-server-terminals"
|
||||
version = "0.5.3"
|
||||
@@ -2215,14 +2329,14 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
|
||||
|
||||
[[package]]
|
||||
name = "jupyterlab"
|
||||
version = "4.4.10"
|
||||
version = "4.5.7"
|
||||
description = "JupyterLab computational environment"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["analysis"]
|
||||
files = [
|
||||
{file = "jupyterlab-4.4.10-py3-none-any.whl", hash = "sha256:65939ab4c8dcd0c42185c2d0d1a9d60b254dc8c46fc4fdb286b63c51e9358e07"},
|
||||
{file = "jupyterlab-4.4.10.tar.gz", hash = "sha256:521c017508af4e1d6d9d8a9d90f47a11c61197ad63b2178342489de42540a615"},
|
||||
{file = "jupyterlab-4.5.7-py3-none-any.whl", hash = "sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d"},
|
||||
{file = "jupyterlab-4.5.7.tar.gz", hash = "sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2234,7 +2348,7 @@ jinja2 = ">=3.0.3"
|
||||
jupyter-core = "*"
|
||||
jupyter-lsp = ">=2.0.0"
|
||||
jupyter-server = ">=2.4.0,<3"
|
||||
jupyterlab-server = ">=2.27.1,<3"
|
||||
jupyterlab-server = ">=2.28.0,<3"
|
||||
notebook-shim = ">=0.2"
|
||||
packaging = "*"
|
||||
setuptools = ">=41.1.0"
|
||||
@@ -2243,9 +2357,9 @@ tornado = ">=6.2.0"
|
||||
traitlets = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.11.4)"]
|
||||
dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.11.12)"]
|
||||
docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.2.0)", "sphinx-copybutton"]
|
||||
docs-screenshots = ["altair (==5.5.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.3.post1)", "matplotlib (==3.10.0)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.15.1)", "vega-datasets (==0.9.0)"]
|
||||
docs-screenshots = ["altair (==6.0.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.3.post1)", "matplotlib (==3.10.0)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.15.1)"]
|
||||
test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"]
|
||||
upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"]
|
||||
|
||||
@@ -2573,14 +2687,14 @@ altgraph = ">=0.17"
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.10"
|
||||
version = "1.3.12"
|
||||
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"},
|
||||
{file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"},
|
||||
{file = "mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9"},
|
||||
{file = "mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2907,14 +3021,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mistune"
|
||||
version = "3.1.4"
|
||||
version = "3.2.1"
|
||||
description = "A sane and fast Markdown parser with useful plugins and renderers"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["analysis"]
|
||||
files = [
|
||||
{file = "mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d"},
|
||||
{file = "mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164"},
|
||||
{file = "mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048"},
|
||||
{file = "mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3075,14 +3189,14 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=
|
||||
|
||||
[[package]]
|
||||
name = "nbconvert"
|
||||
version = "7.16.6"
|
||||
description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)."
|
||||
version = "7.17.1"
|
||||
description = "Convert Jupyter Notebooks (.ipynb files) to other formats."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["analysis"]
|
||||
files = [
|
||||
{file = "nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b"},
|
||||
{file = "nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582"},
|
||||
{file = "nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8"},
|
||||
{file = "nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3103,8 +3217,8 @@ pygments = ">=2.4.1"
|
||||
traitlets = ">=5.1"
|
||||
|
||||
[package.extras]
|
||||
all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"]
|
||||
docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"]
|
||||
all = ["flaky", "intersphinx-registry", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (>=5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"]
|
||||
docs = ["intersphinx-registry", "ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (>=5.0.2)", "sphinxcontrib-spelling"]
|
||||
qtpdf = ["pyqtwebengine (>=5.15)"]
|
||||
qtpng = ["pyqtwebengine (>=5.15)"]
|
||||
serve = ["tornado (>=6.1)"]
|
||||
@@ -3935,22 +4049,42 @@ wcwidth = "*"
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.0"
|
||||
version = "6.33.6"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035"},
|
||||
{file = "protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee"},
|
||||
{file = "protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455"},
|
||||
{file = "protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90"},
|
||||
{file = "protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298"},
|
||||
{file = "protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef"},
|
||||
{file = "protobuf-6.33.0-cp39-cp39-win32.whl", hash = "sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3"},
|
||||
{file = "protobuf-6.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9"},
|
||||
{file = "protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995"},
|
||||
{file = "protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954"},
|
||||
{file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"},
|
||||
{file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"},
|
||||
{file = "protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a"},
|
||||
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2"},
|
||||
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3"},
|
||||
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593"},
|
||||
{file = "protobuf-6.33.6-cp39-cp39-win32.whl", hash = "sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e"},
|
||||
{file = "protobuf-6.33.6-cp39-cp39-win_amd64.whl", hash = "sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf"},
|
||||
{file = "protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901"},
|
||||
{file = "protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "7.35.0"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "protobuf-7.35.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:66be6c513931c794fa92c080ffee41671390da3d79da219cf9c0c0907f035dda"},
|
||||
{file = "protobuf-7.35.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5"},
|
||||
{file = "protobuf-7.35.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4cbf5cc286130e06a6c9bbefac442431173906dfcc979712183d4adcc01b37ee"},
|
||||
{file = "protobuf-7.35.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:6c0f98f10c8a05ea30f8993dfef2de093d27b490fdae78bb60c8343795d55011"},
|
||||
{file = "protobuf-7.35.0-cp310-abi3-win32.whl", hash = "sha256:4c4617b83ade0e279d1d2bfe04025a1adb87f9ed657de038620dc0ff959357f6"},
|
||||
{file = "protobuf-7.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201"},
|
||||
{file = "protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0"},
|
||||
{file = "protobuf-7.35.0.tar.gz", hash = "sha256:a2efd84605f41e559f1881b0912b44099d0a2ac9bf46b3474823f10fb393b0e6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4101,14 +4235,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
version = "2.20.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["analysis", "dev"]
|
||||
files = [
|
||||
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
||||
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
||||
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
|
||||
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -4740,6 +4874,7 @@ description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "analysis"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
|
||||
{file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
|
||||
@@ -4755,6 +4890,29 @@ urllib3 = ">=1.21.1,<3"
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "analysis"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
|
||||
{file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2023.5.7"
|
||||
charset_normalizer = ">=2,<4"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.26,<3"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
|
||||
|
||||
[[package]]
|
||||
name = "retrying"
|
||||
version = "1.4.2"
|
||||
@@ -5145,25 +5303,25 @@ win32 = ["pywin32 ; sys_platform == \"win32\""]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "80.9.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
version = "82.0.1"
|
||||
description = "Most extensible Python build backend with support for C/C++ extension modules"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "analysis", "dev"]
|
||||
files = [
|
||||
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
|
||||
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
|
||||
{file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"},
|
||||
{file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"},
|
||||
]
|
||||
markers = {main = "extra == \"analysis\""}
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
|
||||
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""]
|
||||
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
|
||||
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
|
||||
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
@@ -5357,24 +5515,22 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.2"
|
||||
version = "6.5.6"
|
||||
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["analysis"]
|
||||
files = [
|
||||
{file = "tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f"},
|
||||
{file = "tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af"},
|
||||
{file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:65fcfaafb079435c2c19dc9e07c0f1cf0fa9051759ed0a7d0a3ba7ea7f64919c"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:38bc01b4acacded2de63ae78023548e41ebe6fbed3ec05a796d7ae3ad893887e"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b942e6a137fda31ff54bf8e6e2c8d1c37f1f50583f3ed53fb840b53b9601d104"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8666946e70171b8c3f1fc9b7876fac492e84822c4c7f3746f4e8f8bc9ac92a79"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c34cfab7ad6d104f052f55de06d39bbafc5885cfeb4da688803308dbcfa90b7"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:385f35e4e22fb52551dfcda4cdc8c30c61c2c001aef5ddad99cdfe116952efd3"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-win32.whl", hash = "sha256:db475f1b67b2809b10bb16264829087724ca8d24fe4ed47f7b8675cae453ef86"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-win_amd64.whl", hash = "sha256:6739bf1e8eb09230f1280ddbd3236f0309db70f2c551a8dbc40f62babdf82f79"},
|
||||
{file = "tornado-6.5.6-cp39-abi3-win_arm64.whl", hash = "sha256:2543597b24a695d72338a9a77818362d72387c03ae173f1f169eadc5c91466ac"},
|
||||
{file = "tornado-6.5.6.tar.gz", hash = "sha256:9a365179fe8ff6b8766f602c0f67c185d778193e9bdd828b19f0b6ed7764177d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5512,21 +5668,41 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
version = "2.6.3"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "analysis", "dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
|
||||
{file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
|
||||
{file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
|
||||
{file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "analysis", "dev"]
|
||||
markers = "python_version >= \"3.11\""
|
||||
files = [
|
||||
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
|
||||
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
@@ -5941,4 +6117,4 @@ tunnel = ["pytap2"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9,<3.15"
|
||||
content-hash = "e87e2eaffca4ad13aa7e1b8622ec4b37b23a4efe1f4febe0ca87b92db5fe6d1e"
|
||||
content-hash = "674308d6eb7c3730031cc3e73c98b2413c7f59002a9317bfad387bc34a17c64d"
|
||||
|
||||
Submodule protobufs updated: c474fd3f49...da60cee584
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "meshtastic"
|
||||
version = "2.7.6"
|
||||
version = "2.7.11"
|
||||
description = "Python API & client shell for talking to Meshtastic devices"
|
||||
authors = ["Meshtastic Developers <contact@meshtastic.org>"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -15,7 +15,7 @@ requests = "^2.31.0"
|
||||
pyyaml = "^6.0.1"
|
||||
pypubsub = "^4.0.3"
|
||||
bleak = ">=0.22.3"
|
||||
packaging = "^24.0"
|
||||
packaging = ">=24.0"
|
||||
argcomplete = { version = "^3.5.2", optional = true }
|
||||
pyqrcode = { version = "^1.2.1", optional = true }
|
||||
dotmap = { version = "^1.3.30", optional = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[pytest]
|
||||
|
||||
addopts = -m "not int and not smoke1 and not smoke2 and not smokewifi and not examples and not smokevirt"
|
||||
addopts = -m "not int and not smoke1 and not smoke2 and not smokewifi and not examples and not smokevirt and not smokemesh"
|
||||
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
@@ -13,4 +13,5 @@ markers =
|
||||
smoke1: runs smoke tests on a single device connected via USB
|
||||
smoke2: runs smoke tests on a two devices connected via USB
|
||||
smokewifi: runs smoke test on an esp32 device setup with wifi
|
||||
smokemesh: runs smoke tests against multiple meshtasticd sim instances
|
||||
examples: runs the examples tests which validates the library
|
||||
|
||||
Reference in New Issue
Block a user