Compare commits

..
Author SHA1 Message Date
vidplace7 efb6d1c0e9 protobufs v2.8.0 2026-09-02 13:55:22 -04:00
Ian McEwen 0539a9600f Merge pull request #961 from anton-vinogradov/ota-update-skip-node-db
Skip node DB fetch during --ota-update / --reboot-ota
2026-08-12 12:04:41 -07:00
Ian McEwen 445e17ddc0 Merge pull request #971 from ianmcorvidae/improve-update-protobufs
Improve update_protobufs workflow
2026-08-11 13:30:35 -07:00
Ian McEwen 33840e2cee Fix review comments/zizmor lint 2026-08-11 11:47:18 -07:00
Ian McEwen e45e59ad39 Improve update_protobufs workflow to use release versions, put version in commit, and allow specifying a version, as well as allowing dispatch from other repositories. 2026-08-11 11:32:44 -07:00
Ian McEwen ae710478fb update pyarrow for python 3.14 specifically, separately 2026-07-22 12:46:53 -07:00
Ian McEwen 3b084d7df2 Install with extras and dev/powermon in validate to stop build jobs sooner if there's issues in those 2026-07-22 12:18:42 -07:00
Ian McEwen 3f62c099ae More CI optimization: run tests only once, run pylint/mypy/tests in parallel with make 2026-07-22 12:14:24 -07:00
Ian McEwen a509bca86d Increase pyarrow version for a pkg_resources dependency issue 2026-07-22 11:55:04 -07:00
Ian McEwen dca0b1fced Improve CI job to fail faster/use up fewer runners when things fail 2026-07-22 11:40:34 -07:00
Ian McEwen 11b71e9587 Convey bitfield forward in firmware harness so prehop drop doesn't get mad at us 2026-07-22 11:20:19 -07:00
Anton VinogradovandClaude Opus 4.8 ef9cdc34a4 Skip node DB fetch during --ota-update / --reboot-ota
OTA only needs the local node to send the startOTA admin request and then
streams firmware directly to the loader over WiFi; it never reads the node DB.
On a device with a large node DB the initial dump can stall and the firmware
closes the API connection before it processes the OTA request, so OTA silently
fails. Imply --no-nodes for --ota-update / --reboot-ota.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 02:02:46 +03:00
github-actions 19d066970e bump version to 2.7.11 2026-07-17 17:37:04 +00:00
Ian McEwen 485d860911 use right repository name for daily simradio testing 2026-07-17 10:32:07 -07:00
Ian McEwen e6f07123f6 Merge pull request #958 from meshtastic/tcp-graceful-close
Half-close TCP connections so the last write is not lost
2026-07-17 10:29:15 -07:00
Thomas Göttgens f623732694 Half-close TCP connections so the last write is not lost
close() went straight to shutdown(SHUT_RDWR) + close(). Doing that while either
side still has unread data makes the stack send RST rather than FIN, and the
device's FromRadio stream means there is essentially always unread data.

Winsock discards data that was received but not yet read when an RST arrives, so
an admin message written moments earlier is thrown away before the device reads
it. Linux delivers the buffered bytes before reporting ECONNRESET, which is why
this only surfaced on Windows: every one-shot TCP write there (--set, --seturl,
--sendtext) reported success and did nothing.

writeConfig() does not wait for an ack, so close() runs microseconds after
sendall(). The device cannot compensate; it polls every 5ms and the RST beats its
next read.

Send FIN first with shutdown(SHUT_WR), which lets the device consume what we
wrote, wait briefly for the reader thread to drain and exit, then tear down
exactly as before. The full shutdown is kept so a reader still blocked in recv()
is unblocked for the join in StreamInterface.close(). The wait is bounded: the
device is not obliged to close just because we half-closed.

Fixes #957
2026-07-17 14:34:18 +02:00
Ian McEwen 82220ba49b Merge pull request #946 from ianmcorvidae/sim-based-testing
Initial implementation of simradio-based testing with a multi-node simulated mesh
2026-07-14 09:16:09 -07:00
Ian McEwen 4339cda6bd Run smoke tests daily to find regressions 2026-07-07 14:45:09 -07:00
Ian McEwen 5760fb3ad9 Add reconnect for firmware_node 2026-07-07 12:40:25 -07:00
Ian McEwen 2281ea5b64 Set the region on the simradio nodes so we can test setting presets other than LONG_FAST properly 2026-07-07 12:17:34 -07:00
Ian McEwen 7950798378 quick-win review fixes 2026-07-06 14:36:25 -07:00
Ian McEwen bffaff1c71 Update ch-preset shorthand arguments 2026-07-06 14:16:12 -07:00
Ian McEwen ceca7b3b10 Add a few extra tests of the revamped channel deletion behavior 2026-07-06 11:26:19 -07:00
Ian McEwen 15438c8a6e matrix up daily/alpha/beta 2026-07-04 02:41:21 -07:00
Ian McEwen 2f9b285dd5 Give the simradio testing a better name in the action 2026-07-04 02:38:04 -07:00
Ian McEwen 9b892711df Restructure tests, fix up a few other things, rework smokevirt thoroughly 2026-07-04 02:37:22 -07:00
Ian McEwen 1cd1aed33c Fix pylint/mypy stuff 2026-07-02 23:31:38 -07:00
Ian McEwen ab14fb2a86 Initial implementation of simradio-based testing with a multi-node simulated mesh 2026-07-02 22:48:01 -07:00
Ian McEwen 7a7353ca15 fix auto review skip-certain-authors key in coderabbit config 2026-07-02 09:17:31 -07:00
Ian McEwen e2844cb8d6 Merge pull request #944 from tylerpieper/master
add support for .cfg export/import
2026-07-01 21:17:09 -07:00
Ian McEwen 441c6e80a1 coderabbit yaml, primarily to disable the stupid poem 2026-07-01 16:34:12 -07:00
Ian McEwenandCopilot Autofix powered by AI b05e545396 guard against None
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-01 15:21:25 -07:00
Ian McEwen b6a13d58fe Refactor DeviceProfile import/export and harden handling
- Unify parsing for yaml/DeviceProfile paths
- Improve autodetection
- Ensure partial-config behavior with yaml
- gate configuration to local node (avoiding some pre-existing bugs)
- gate fixed position updates behind explicit opt-in via position settings field
- use setOwner properly
- tests, tests, and more tests
2026-07-01 13:57:42 -07:00
tylerpieper 8c5efb926c add support for .cfg export/import
adds binary import and export to mirror functionality of android (and soon ios) app
2026-06-29 14:41:16 -07:00
48 changed files with 5377 additions and 1370 deletions

No files matched your search

+16
View 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/**"
+54 -17
View File
@@ -1,4 +1,7 @@
name: CI
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
@@ -8,7 +11,29 @@ on:
- master
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.12"
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install --all-extras --with dev,powermon
poetry run meshtastic --version
build:
needs: validate
runs-on: ubuntu-latest
strategy:
matrix:
@@ -17,10 +42,14 @@ jobs:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
steps:
- uses: actions/checkout@v4
- name: Install Python 3
- name: Install Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Uninstall meshtastic
run: |
pip3 uninstall -y meshtastic
@@ -32,15 +61,8 @@ jobs:
run: |
poetry install --all-extras --with dev,powermon
poetry run meshtastic --version
- name: Run pylint
run: poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"
- name: Check types with mypy
run: poetry run mypy meshtastic/
- name: Run tests with pytest
run: poetry run pytest --cov=meshtastic
- name: Generate coverage report
run: |
poetry run pytest --cov=meshtastic --cov-report=xml
- name: Run lint, type check, and tests in parallel
run: make -j3 --output-sync=target ci
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
@@ -51,22 +73,37 @@ jobs:
name: codecov-umbrella
fail_ci_if_error: true
verbose: true
validate:
simradio_testing:
needs: [validate, build]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
channel:
- beta
- alpha
- daily
continue-on-error: ${{ matrix.channel == 'daily' }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Python 3
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install meshtastic from local
run: |
python -m pip install --upgrade pip
pip3 install poetry
poetry install
poetry install --all-extras --with dev
poetry run meshtastic --version
- name: Install meshtasticd (${{ matrix.channel }}) from PPA
run: |
sudo add-apt-repository -y ppa:meshtastic/${{ matrix.channel }}
sudo apt-get update
sudo apt-get install -y meshtasticd
- name: Run firmware smoke tests
run: poetry run pytest -m "smokevirt or smokemesh" -v
timeout-minutes: 15
+36
View 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
+50 -4
View File
@@ -1,5 +1,15 @@
name: "Update protobufs"
on: workflow_dispatch
on:
workflow_dispatch:
inputs:
version:
description: "Protobufs version tag to sync to (e.g., v2.7.26). Defaults to the latest protobufs release."
required: false
default: ""
type: string
repository_dispatch:
types: [protobufs-release]
permissions:
contents: write
@@ -14,6 +24,7 @@ jobs:
with:
fetch-depth: 0
submodules: true
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v5
@@ -25,11 +36,44 @@ jobs:
python -m pip install --upgrade pip
python -m pip install poetry
- name: Update protobuf submodule
- name: Determine target protobufs version
id: version
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
PAYLOAD_VERSION: ${{ github.event.client_payload.version }}
run: |
cd protobufs
git fetch --tags
if [ -n "${INPUT_VERSION}" ]; then
target="${INPUT_VERSION}"
elif [ -n "${PAYLOAD_VERSION}" ]; then
target="${PAYLOAD_VERSION}"
else
target=$(git tag --list 'v*' --sort=-version:refname | head -n1)
if [ -z "${target}" ]; then
echo "Error: no release tags found in meshtastic/protobufs"
exit 1
fi
fi
if ! git show-ref --verify --quiet "refs/tags/${target}"; then
echo "Error: '${target}' is not a valid tag in meshtastic/protobufs"
exit 1
fi
printf 'version=%s\n' "${target}" >> "$GITHUB_OUTPUT"
echo "Target protobufs version: ${target}"
- name: Update protobuf submodule to release
env:
TARGET_VERSION: ${{ steps.version.outputs.version }}
run: |
git submodule sync --recursive
git submodule update --init --recursive
git submodule update --remote --recursive
cd protobufs
git checkout "${TARGET_VERSION}"
cd ..
- name: Download nanopb
run: |
@@ -46,6 +90,8 @@ jobs:
./bin/regen-protobufs.sh
- name: Commit update
env:
TARGET_VERSION: ${{ steps.version.outputs.version }}
run: |
git config --global user.name 'github-actions'
git config --global user.email 'bot@noreply.github.com'
@@ -53,7 +99,7 @@ jobs:
git add protobufs
git add meshtastic/protobuf
if [[ -n "$(git status --porcelain)" ]]; then
git commit -m "Update protobufs"
git commit -m "protobufs: ${TARGET_VERSION}"
git push
else
echo "No changes to commit"
+14
View File
@@ -42,5 +42,19 @@ cov:
examples: FORCE
pytest -mexamples
# CI targets (run via poetry, executed in parallel by the CI workflow)
.PHONY: ci-pylint ci-mypy ci-test ci
ci-pylint:
poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$$"
ci-mypy:
poetry run mypy meshtastic/
ci-test:
poetry run pytest --cov=meshtastic --cov-report=xml
ci: ci-pylint ci-mypy ci-test
# Makefile hack to get the examples to always run
FORCE: ;
+12
View File
@@ -26,6 +26,13 @@ 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"}
# FieldType enum names from nanopb.proto. Only FT_IGNORE carries meaning for a
# Python client (the firmware omits the field entirely); the rest describe C
# storage class, but are passed through so the descriptor mirrors the .options.
FIELD_TYPE_ENUM = frozenset(
{"FT_DEFAULT", "FT_CALLBACK", "FT_POINTER", "FT_STATIC", "FT_IGNORE", "FT_INLINE"}
)
# 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,
@@ -36,6 +43,7 @@ FIELD_OPTIONS = frozenset(
"max_length",
"max_count",
"int_size",
"type",
"fixed_length",
"fixed_count",
"long_names",
@@ -122,6 +130,10 @@ def format_nanopb_opts(opts: Dict[str, Any]) -> str:
if k == "int_size":
enum_val = INT_SIZE_ENUM.get(v, f"IS_{v}")
parts.append(f"(nanopb).int_size = {enum_val}")
elif k == "type":
if v not in FIELD_TYPE_ENUM:
raise ValueError(f"unknown nanopb field type {v!r}")
parts.append(f"(nanopb).type = {v}")
elif isinstance(v, bool):
parts.append(f"(nanopb).{k} = {'true' if v else 'false'}")
else:
+326 -115
View File
@@ -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
@@ -61,7 +63,7 @@ except ImportError as e:
have_powermon = False
powermon_exception = e
meter = None
from meshtastic.protobuf import admin_pb2, 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__)
@@ -742,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:
@@ -858,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
@@ -955,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)
@@ -970,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
@@ -1263,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")
@@ -1307,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
@@ -1332,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."""
@@ -1411,6 +1590,12 @@ def common():
if not os.path.isfile(args.ota_update):
meshtastic.util.our_exit(f"Error: OTA firmware file not found: {args.ota_update}", 1)
# OTA (WiFi/BLE) only needs the local node to send the admin request and then
# streams the firmware directly; it never reads the node DB. Skip fetching it so a
# large node DB dump can't stall/close the connection before the OTA request lands.
if getattr(args, "ota_update", None) or getattr(args, "reboot_ota", False):
args.no_nodes = True
if have_powermon:
create_power_meter()
@@ -1702,7 +1887,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(
@@ -1710,7 +1897,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
@@ -1782,43 +1975,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",
)
+12 -2
View File
@@ -287,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
+37 -29
View File
File diff suppressed because one or more lines are too long.
+236 -6
View File
@@ -236,6 +236,10 @@ class AdminMessage(google.protobuf.message.Message):
"""
TAK module config
"""
MESHBEACON_CONFIG: AdminMessage._ModuleConfigType.ValueType # 16
"""
Mesh Beacon module config
"""
class ModuleConfigType(_ModuleConfigType, metaclass=_ModuleConfigTypeEnumTypeWrapper):
"""
@@ -306,6 +310,10 @@ class AdminMessage(google.protobuf.message.Message):
"""
TAK module config
"""
MESHBEACON_CONFIG: AdminMessage.ModuleConfigType.ValueType # 16
"""
Mesh Beacon module config
"""
class _BackupLocation:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -851,6 +859,8 @@ class LockdownAuth(google.protobuf.message.Message):
BOOTS_REMAINING_FIELD_NUMBER: builtins.int
VALID_UNTIL_EPOCH_FIELD_NUMBER: builtins.int
LOCK_NOW_FIELD_NUMBER: builtins.int
MAX_SESSION_SECONDS_FIELD_NUMBER: builtins.int
DISABLE_FIELD_NUMBER: builtins.int
passphrase: builtins.bytes
"""
Passphrase bytes (1-32). Empty when lock_now is true.
@@ -876,6 +886,67 @@ class LockdownAuth(google.protobuf.message.Message):
connection-level admin authorization, and reboot the device into
the locked state. Always honoured regardless of current lock state.
"""
max_session_seconds: builtins.int
"""
Optional per-boot uptime cap on the unlocked session, in seconds.
0 = unlimited (token-only enforcement, suitable for unattended
tower / infrastructure nodes).
When non-zero, the firmware arms an uptime timer at unlock. On
each expiry, while there is still boot-count budget, the firmware
decrements the on-flash boot count in place, revokes per-
connection admin auth (clients must re-authenticate to see
content), re-engages the screen lock, and re-arms the timer
without rebooting. Mesh routing keeps running across session
boundaries; only when the boot-count budget reaches zero does
the device hard-lock and reboot.
Total exposure ceiling = ((resolved boot count) + 1) * max_session_seconds.
The +1 accounts for the initial passphrase-unlocked session
itself, since boots_remaining is the number of subsequent
session rolls (each consuming one boot from the rollback ledger).
The resolved boot count is the value the firmware writes into the
token at unlock time: the client-supplied boots_remaining when
non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS).
Note that boots_remaining == 0 in this message means "use firmware
default", NOT "zero boots" — a client computing the ceiling for
display should mirror that resolution rather than multiplying the
raw request value.
The cap is persisted in the token, so it survives token-based
auto-unlock across reboots. Explicit operator Lock Now still
deletes the token and forces passphrase re-entry.
Uses millis() (CPU uptime), not wall-clock time, so the cap is
immune to GPS spoofing, RTC backup-battery removal, and Faraday
cage isolation — none of those move the uptime counter. The only
way to reset the session clock is a reboot, which costs a boot
from the on-flash, HMAC-bound counter.
"""
disable: builtins.bool
"""
Disable lockdown mode. Requires a valid passphrase in the same
message (the device must prove the operator owns it before
reverting at-rest encryption). On success the firmware decrypts
every stored config / channel / nodedb file back to plaintext,
removes the wrapped DEK, unlock token, monotonic-counter, and
backoff files, and reboots out of lockdown.
This is the inverse of the provision/unlock path: it is how the
client app's "lockdown mode" toggle returns a device to normal
operation.
NOT reversed by this operation: APPROTECT. Once the debug port
lockout has been burned (on silicon where it is effective) it is
permanent — disabling lockdown decrypts your data and removes the
access gates, but the SWD/JTAG port stays locked for the life of
the device (recoverable only via a full chip erase over a debug
probe, which destroys all data). Clients should make this
irreversibility clear at the moment lockdown is first enabled.
When true the passphrase field is still required; boots_remaining,
valid_until_epoch, max_session_seconds, and lock_now are ignored.
"""
def __init__(
self,
*,
@@ -883,8 +954,10 @@ class LockdownAuth(google.protobuf.message.Message):
boots_remaining: builtins.int = ...,
valid_until_epoch: builtins.int = ...,
lock_now: builtins.bool = ...,
max_session_seconds: builtins.int = ...,
disable: 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: ...
def ClearField(self, field_name: typing.Literal["boots_remaining", b"boots_remaining", "disable", b"disable", "lock_now", b"lock_now", "max_session_seconds", b"max_session_seconds", "passphrase", b"passphrase", "valid_until_epoch", b"valid_until_epoch"]) -> None: ...
global___LockdownAuth = LockdownAuth
@@ -900,6 +973,7 @@ class HamParameters(google.protobuf.message.Message):
TX_POWER_FIELD_NUMBER: builtins.int
FREQUENCY_FIELD_NUMBER: builtins.int
SHORT_NAME_FIELD_NUMBER: builtins.int
LONG_NAME_FIELD_NUMBER: builtins.int
call_sign: builtins.str
"""
Amateur radio call sign, eg. KD2ABC
@@ -918,6 +992,11 @@ class HamParameters(google.protobuf.message.Message):
"""
Optional short name of user
"""
long_name: builtins.str
"""
Optional long name of user
Appended to callsign
"""
def __init__(
self,
*,
@@ -925,8 +1004,9 @@ class HamParameters(google.protobuf.message.Message):
tx_power: builtins.int = ...,
frequency: builtins.float = ...,
short_name: builtins.str = ...,
long_name: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["call_sign", b"call_sign", "frequency", b"frequency", "short_name", b"short_name", "tx_power", b"tx_power"]) -> None: ...
def ClearField(self, field_name: typing.Literal["call_sign", b"call_sign", "frequency", b"frequency", "long_name", b"long_name", "short_name", b"short_name", "tx_power", b"tx_power"]) -> None: ...
global___HamParameters = HamParameters
@@ -1087,6 +1167,9 @@ class SensorConfig(google.protobuf.message.Message):
SEN5X_CONFIG_FIELD_NUMBER: builtins.int
SCD30_CONFIG_FIELD_NUMBER: builtins.int
SHTXX_CONFIG_FIELD_NUMBER: builtins.int
DS248X_CONFIG_FIELD_NUMBER: builtins.int
SEN6X_CONFIG_FIELD_NUMBER: builtins.int
AS3935_CONFIG_FIELD_NUMBER: builtins.int
@property
def scd4x_config(self) -> global___SCD4X_config:
"""
@@ -1111,6 +1194,24 @@ class SensorConfig(google.protobuf.message.Message):
SHTXX temperature and relative humidity sensor configuration
"""
@property
def ds248x_config(self) -> global___DS248X_config:
"""
DS248X-800 temperature sensor configuration
"""
@property
def sen6x_config(self) -> global___SEN6X_config:
"""
SEN6X PM/RHT/VOC/NOx/CO2/HCHO Sensor configuration
"""
@property
def as3935_config(self) -> global___AS3935_config:
"""
AS3935 lightning sensor configuration
"""
def __init__(
self,
*,
@@ -1118,9 +1219,12 @@ class SensorConfig(google.protobuf.message.Message):
sen5x_config: global___SEN5X_config | None = ...,
scd30_config: global___SCD30_config | None = ...,
shtxx_config: global___SHTXX_config | None = ...,
ds248x_config: global___DS248X_config | None = ...,
sen6x_config: global___SEN6X_config | None = ...,
as3935_config: global___AS3935_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: ...
def HasField(self, field_name: typing.Literal["as3935_config", b"as3935_config", "ds248x_config", b"ds248x_config", "scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "sen6x_config", b"sen6x_config", "shtxx_config", b"shtxx_config"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["as3935_config", b"as3935_config", "ds248x_config", b"ds248x_config", "scd30_config", b"scd30_config", "scd4x_config", b"scd4x_config", "sen5x_config", b"sen5x_config", "sen6x_config", b"sen6x_config", "shtxx_config", b"shtxx_config"]) -> None: ...
global___SensorConfig = SensorConfig
@@ -1199,6 +1303,7 @@ class SEN5X_config(google.protobuf.message.Message):
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
SET_ONE_SHOT_MODE_FIELD_NUMBER: builtins.int
START_FAN_CLEANING_FIELD_NUMBER: builtins.int
set_temperature: builtins.float
"""
Reference temperature in degC
@@ -1207,21 +1312,105 @@ class SEN5X_config(google.protobuf.message.Message):
"""
One-shot mode (true for low power - one-shot mode, false for normal - continuous mode)
"""
start_fan_cleaning: builtins.bool
"""
Trigger a fan cleaning cycle
"""
def __init__(
self,
*,
set_temperature: builtins.float | None = ...,
set_one_shot_mode: builtins.bool | None = ...,
start_fan_cleaning: 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: ...
def HasField(self, field_name: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode", "_set_temperature", b"_set_temperature", "_start_fan_cleaning", b"_start_fan_cleaning", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_set_one_shot_mode", b"_set_one_shot_mode", "_set_temperature", b"_set_temperature", "_start_fan_cleaning", b"_start_fan_cleaning", "set_one_shot_mode", b"set_one_shot_mode", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> 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: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_start_fan_cleaning", b"_start_fan_cleaning"]) -> typing.Literal["start_fan_cleaning"] | None: ...
global___SEN5X_config = SEN5X_config
@typing.final
class SEN6X_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_TEMPERATURE_FIELD_NUMBER: builtins.int
SET_ONE_SHOT_MODE_FIELD_NUMBER: builtins.int
START_FAN_CLEANING_FIELD_NUMBER: builtins.int
SET_ASC_FIELD_NUMBER: builtins.int
SET_TARGET_CO2_CONC_FIELD_NUMBER: builtins.int
SET_ALTITUDE_FIELD_NUMBER: builtins.int
SET_AMBIENT_PRESSURE_FIELD_NUMBER: builtins.int
FACTORY_RESET_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)
"""
start_fan_cleaning: builtins.bool
"""
Trigger a fan cleaning cycle
"""
set_asc: builtins.bool
"""
Set Automatic self-calibration enabled (CO2-capable variants only: SEN63C, SEN66, SEN69C)
"""
set_target_co2_conc: builtins.int
"""
Recalibration target CO2 concentration in ppm (FRC only), CO2-capable variants only
"""
set_altitude: builtins.int
"""
Altitude of sensor in meters above sea level. 0 - 3000m (overrides ambient pressure), CO2-capable variants only
"""
set_ambient_pressure: builtins.int
"""
Sensor ambient pressure in Pa. 70000 - 120000 Pa (overrides altitude), CO2-capable variants only
"""
factory_reset: builtins.bool
"""
Perform a factory reset of the CO2 sensor's calibration, CO2-capable variants only
"""
def __init__(
self,
*,
set_temperature: builtins.float | None = ...,
set_one_shot_mode: builtins.bool | None = ...,
start_fan_cleaning: builtins.bool | None = ...,
set_asc: builtins.bool | None = ...,
set_target_co2_conc: builtins.int | None = ...,
set_altitude: builtins.int | None = ...,
set_ambient_pressure: builtins.int | None = ...,
factory_reset: 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_one_shot_mode", b"_set_one_shot_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_start_fan_cleaning", b"_start_fan_cleaning", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_one_shot_mode", b"set_one_shot_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> 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_one_shot_mode", b"_set_one_shot_mode", "_set_target_co2_conc", b"_set_target_co2_conc", "_set_temperature", b"_set_temperature", "_start_fan_cleaning", b"_start_fan_cleaning", "factory_reset", b"factory_reset", "set_altitude", b"set_altitude", "set_ambient_pressure", b"set_ambient_pressure", "set_asc", b"set_asc", "set_one_shot_mode", b"set_one_shot_mode", "set_target_co2_conc", b"set_target_co2_conc", "set_temperature", b"set_temperature", "start_fan_cleaning", b"start_fan_cleaning"]) -> 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_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_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["_start_fan_cleaning", b"_start_fan_cleaning"]) -> typing.Literal["start_fan_cleaning"] | None: ...
global___SEN6X_config = SEN6X_config
@typing.final
class SCD30_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -1302,3 +1491,44 @@ class SHTXX_config(google.protobuf.message.Message):
def WhichOneof(self, oneof_group: typing.Literal["_set_accuracy", b"_set_accuracy"]) -> typing.Literal["set_accuracy"] | None: ...
global___SHTXX_config = SHTXX_config
@typing.final
class DS248X_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
MAIN_TEMPERATURE_CHANNEL_FIELD_NUMBER: builtins.int
main_temperature_channel: builtins.int
"""
Main channel for temperature reporting (0-7)
"""
def __init__(
self,
*,
main_temperature_channel: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_main_temperature_channel", b"_main_temperature_channel", "main_temperature_channel", b"main_temperature_channel"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_main_temperature_channel", b"_main_temperature_channel", "main_temperature_channel", b"main_temperature_channel"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_main_temperature_channel", b"_main_temperature_channel"]) -> typing.Literal["main_temperature_channel"] | None: ...
global___DS248X_config = DS248X_config
@typing.final
class AS3935_config(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
SET_TUNING_CAP_PF_FIELD_NUMBER: builtins.int
set_tuning_cap_pf: builtins.int
"""
Antenna tuning capacitance in pF, 0 to 120 in steps of 8. The antenna tank must
resonate within 3.5% of 500kHz; the correct trim is specific to the sensor board.
"""
def __init__(
self,
*,
set_tuning_cap_pf: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_set_tuning_cap_pf", b"_set_tuning_cap_pf", "set_tuning_cap_pf", b"set_tuning_cap_pf"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_set_tuning_cap_pf", b"_set_tuning_cap_pf", "set_tuning_cap_pf", b"set_tuning_cap_pf"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_set_tuning_cap_pf", b"_set_tuning_cap_pf"]) -> typing.Literal["set_tuning_cap_pf"] | None: ...
global___AS3935_config = AS3935_config
+3 -3
View File
@@ -16,7 +16,7 @@ from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb
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\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')
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\"\xbe\x04\n\rDeviceProfile\x12\x1d\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08\x19H\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\x12\x1c\n\x0fis_unmessagable\x18\t \x01(\x08H\x08\x88\x01\x01\x12\x18\n\x0bis_licensed\x18\n \x01(\x08H\t\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_messagesB\x12\n\x10_is_unmessagableB\x0e\n\x0c_is_licensedBf\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)
@@ -25,7 +25,7 @@ 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'
_DEVICEPROFILE.fields_by_name['long_name']._options = None
_DEVICEPROFILE.fields_by_name['long_name']._serialized_options = b'\222?\002\010('
_DEVICEPROFILE.fields_by_name['long_name']._serialized_options = b'\222?\002\010\031'
_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
@@ -33,5 +33,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_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
_globals['_DEVICEPROFILE']._serialized_end=739
# @@protoc_insertion_point(module_scope)
+18 -2
View File
@@ -29,6 +29,8 @@ class DeviceProfile(google.protobuf.message.Message):
FIXED_POSITION_FIELD_NUMBER: builtins.int
RINGTONE_FIELD_NUMBER: builtins.int
CANNED_MESSAGES_FIELD_NUMBER: builtins.int
IS_UNMESSAGABLE_FIELD_NUMBER: builtins.int
IS_LICENSED_FIELD_NUMBER: builtins.int
long_name: builtins.str
"""
Long name for the node
@@ -49,6 +51,14 @@ class DeviceProfile(google.protobuf.message.Message):
"""
Predefined messages for CannedMessage
"""
is_unmessagable: builtins.bool
"""
Is the node unmessagable
"""
is_licensed: builtins.bool
"""
Is this node in licensed user mode
"""
@property
def config(self) -> meshtastic.protobuf.localonly_pb2.LocalConfig:
"""
@@ -78,9 +88,11 @@ class DeviceProfile(google.protobuf.message.Message):
fixed_position: meshtastic.protobuf.mesh_pb2.Position | None = ...,
ringtone: builtins.str | None = ...,
canned_messages: builtins.str | None = ...,
is_unmessagable: builtins.bool | None = ...,
is_licensed: builtins.bool | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> None: ...
def HasField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_is_licensed", b"_is_licensed", "_is_unmessagable", b"_is_unmessagable", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_canned_messages", b"_canned_messages", "_channel_url", b"_channel_url", "_config", b"_config", "_fixed_position", b"_fixed_position", "_is_licensed", b"_is_licensed", "_is_unmessagable", b"_is_unmessagable", "_long_name", b"_long_name", "_module_config", b"_module_config", "_ringtone", b"_ringtone", "_short_name", b"_short_name", "canned_messages", b"canned_messages", "channel_url", b"channel_url", "config", b"config", "fixed_position", b"fixed_position", "is_licensed", b"is_licensed", "is_unmessagable", b"is_unmessagable", "long_name", b"long_name", "module_config", b"module_config", "ringtone", b"ringtone", "short_name", b"short_name"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_canned_messages", b"_canned_messages"]) -> typing.Literal["canned_messages"] | None: ...
@typing.overload
@@ -90,6 +102,10 @@ class DeviceProfile(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_fixed_position", b"_fixed_position"]) -> typing.Literal["fixed_position"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_is_licensed", b"_is_licensed"]) -> typing.Literal["is_licensed"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_is_unmessagable", b"_is_unmessagable"]) -> typing.Literal["is_unmessagable"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_long_name", b"_long_name"]) -> typing.Literal["long_name"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_module_config", b"_module_config"]) -> typing.Literal["module_config"] | None: ...
+20 -16
View File
File diff suppressed because one or more lines are too long.
+140 -1
View File
@@ -1402,6 +1402,27 @@ class Config(google.protobuf.message.Message):
"""
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
"""
ITU1_70CM: Config.LoRaConfig._RegionCode.ValueType # 34
"""
ITU Region 1 Amateur Radio 70cm band (430-440 MHz)
"""
ITU2_70CM: Config.LoRaConfig._RegionCode.ValueType # 35
"""
ITU Region 2 Amateur Radio 70cm band (420-450 MHz)
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz.
Check local law!
"""
ITU3_70CM: Config.LoRaConfig._RegionCode.ValueType # 36
"""
ITU Region 3 Amateur Radio 70cm band (430-450 MHz)
Note: Some countries do not allocate 440-450 MHz. Check local law!
"""
ITU2_125CM: Config.LoRaConfig._RegionCode.ValueType # 37
"""
ITU Region 2 Amateur Radio 1.25m '125cm' band (220-225 MHz)
Note: Some countries do not allocate 220-222 MHz (Ex: USA/Canada).
Check local law!
"""
class RegionCode(_RegionCode, metaclass=_RegionCodeEnumTypeWrapper): ...
UNSET: Config.LoRaConfig.RegionCode.ValueType # 0
@@ -1537,6 +1558,27 @@ class Config(google.protobuf.message.Message):
"""
ITU Region 3 Amateur Radio 2m band (144-148 MHz)
"""
ITU1_70CM: Config.LoRaConfig.RegionCode.ValueType # 34
"""
ITU Region 1 Amateur Radio 70cm band (430-440 MHz)
"""
ITU2_70CM: Config.LoRaConfig.RegionCode.ValueType # 35
"""
ITU Region 2 Amateur Radio 70cm band (420-450 MHz)
Note: Some countries do not allocate 420-430 MHz or 440-450 MHz.
Check local law!
"""
ITU3_70CM: Config.LoRaConfig.RegionCode.ValueType # 36
"""
ITU Region 3 Amateur Radio 70cm band (430-450 MHz)
Note: Some countries do not allocate 440-450 MHz. Check local law!
"""
ITU2_125CM: Config.LoRaConfig.RegionCode.ValueType # 37
"""
ITU Region 2 Amateur Radio 1.25m '125cm' band (220-225 MHz)
Note: Some countries do not allocate 220-222 MHz (Ex: USA/Canada).
Check local law!
"""
class _ModemPreset:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -1614,6 +1656,30 @@ class Config(google.protobuf.message.Message):
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_FAST: Config.LoRaConfig._ModemPreset.ValueType # 14
"""
Tiny Fast
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_SLOW: Config.LoRaConfig._ModemPreset.ValueType # 15
"""
Tiny Slow
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_MODERATE.
"""
MEDIUM_TURBO: Config.LoRaConfig._ModemPreset.ValueType # 16
"""
Medium Range - Turbo
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
class ModemPreset(_ModemPreset, metaclass=_ModemPresetEnumTypeWrapper):
"""
@@ -1691,6 +1757,30 @@ class Config(google.protobuf.message.Message):
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_FAST: Config.LoRaConfig.ModemPreset.ValueType # 14
"""
Tiny Fast
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_FAST.
"""
TINY_SLOW: Config.LoRaConfig.ModemPreset.ValueType # 15
"""
Tiny Slow
Preset optimized for compliance with Amateur Radio restrictions with 20kHz bandwidth.
Many regions limit data transmission bandwidth in lower amateur bands (2 Meter).
Note: TCXO with tight tolerances (±5 ppm or better) is *absolutely required* at these narrow bandwidths.
Only compatible with SX127x and SX126x chipsets.
Comparable link budget and data rate to LONG_MODERATE.
"""
MEDIUM_TURBO: Config.LoRaConfig.ModemPreset.ValueType # 16
"""
Medium Range - Turbo
This preset performs similarly to MEDIUM_FAST, but with 500kHz bandwidth.
It is not legal to use in all regions due to this wider bandwidth.
"""
class _FEM_LNA_Mode:
ValueType = typing.NewType("ValueType", builtins.int)
@@ -1949,6 +2039,49 @@ class Config(google.protobuf.message.Message):
class SecurityConfig(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _PacketSignaturePolicy:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _PacketSignaturePolicyEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Config.SecurityConfig._PacketSignaturePolicy.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
PACKET_SIGNATURE_POLICY_COMPATIBLE: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 0
"""
Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
This is the default to avoid legacy nodes dropping signed packets during rebroadcast.
"""
PACKET_SIGNATURE_POLICY_BALANCED: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 1
"""
Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
Rejects unsigned, signable broadcasts from nodes that have previously signed.
"""
PACKET_SIGNATURE_POLICY_STRICT: Config.SecurityConfig._PacketSignaturePolicy.ValueType # 2
"""
Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
Unsigned, malformed, invalid, or unverifiable packets are ignored.
"""
class PacketSignaturePolicy(_PacketSignaturePolicy, metaclass=_PacketSignaturePolicyEnumTypeWrapper):
"""
Controls how the device authenticates remotely received mesh packets.
"""
PACKET_SIGNATURE_POLICY_COMPATIBLE: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 0
"""
Accept unsigned packets for maximum compatibility while still rejecting malformed or invalid signatures.
This is the default to avoid legacy nodes dropping signed packets during rebroadcast.
"""
PACKET_SIGNATURE_POLICY_BALANCED: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 1
"""
Prefer authenticated packets while retaining compatibility with unsigned packets from nodes not known to sign.
Rejects unsigned, signable broadcasts from nodes that have previously signed.
"""
PACKET_SIGNATURE_POLICY_STRICT: Config.SecurityConfig.PacketSignaturePolicy.ValueType # 2
"""
Accept only packets authenticated by a verified XEdDSA signature or successful PKI decryption.
Unsigned, malformed, invalid, or unverifiable packets are ignored.
"""
PUBLIC_KEY_FIELD_NUMBER: builtins.int
PRIVATE_KEY_FIELD_NUMBER: builtins.int
ADMIN_KEY_FIELD_NUMBER: builtins.int
@@ -1956,6 +2089,7 @@ class Config(google.protobuf.message.Message):
SERIAL_ENABLED_FIELD_NUMBER: builtins.int
DEBUG_LOG_API_ENABLED_FIELD_NUMBER: builtins.int
ADMIN_CHANNEL_ENABLED_FIELD_NUMBER: builtins.int
PACKET_SIGNATURE_POLICY_FIELD_NUMBER: builtins.int
public_key: builtins.bytes
"""
The public key of the user's device.
@@ -1984,6 +2118,10 @@ class Config(google.protobuf.message.Message):
"""
Allow incoming device control over the insecure legacy admin channel.
"""
packet_signature_policy: global___Config.SecurityConfig.PacketSignaturePolicy.ValueType
"""
Determines the packet signature policy applied to remotely received mesh packets.
"""
@property
def admin_key(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]:
"""
@@ -2000,8 +2138,9 @@ class Config(google.protobuf.message.Message):
serial_enabled: builtins.bool = ...,
debug_log_api_enabled: builtins.bool = ...,
admin_channel_enabled: builtins.bool = ...,
packet_signature_policy: global___Config.SecurityConfig.PacketSignaturePolicy.ValueType = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["admin_channel_enabled", b"admin_channel_enabled", "admin_key", b"admin_key", "debug_log_api_enabled", b"debug_log_api_enabled", "is_managed", b"is_managed", "private_key", b"private_key", "public_key", b"public_key", "serial_enabled", b"serial_enabled"]) -> None: ...
def ClearField(self, field_name: typing.Literal["admin_channel_enabled", b"admin_channel_enabled", "admin_key", b"admin_key", "debug_log_api_enabled", b"debug_log_api_enabled", "is_managed", b"is_managed", "packet_signature_policy", b"packet_signature_policy", "private_key", b"private_key", "public_key", b"public_key", "serial_enabled", b"serial_enabled"]) -> None: ...
@typing.final
class SessionkeyConfig(google.protobuf.message.Message):
+39
View File
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/deviceonly_legacy.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 deviceonly_pb2 as meshtastic_dot_protobuf_dot_deviceonly__pb2
from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_telemetry__pb2
from meshtastic.protobuf import nanopb_pb2 as meshtastic_dot_protobuf_dot_nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n+meshtastic/protobuf/deviceonly_legacy.proto\x12\x13meshtastic.protobuf\x1a$meshtastic/protobuf/deviceonly.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a meshtastic/protobuf/nanopb.proto\"\x8c\x03\n\x13NodeInfoLite_Legacy\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\"\x92\x01\n\x13NodeDatabase_Legacy\x12\x0f\n\x07version\x18\x01 \x01(\r\x12j\n\x05nodes\x18\x02 \x03(\x0b\x32(.meshtastic.protobuf.NodeInfoLite_LegacyB1\x92?.\x92\x01+std::vector<meshtastic_NodeInfoLite_Legacy>Bt\n\x14org.meshtastic.protoB\x10\x44\x65viceOnlyLegacyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.protobuf.deviceonly_legacy_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020DeviceOnlyLegacyZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000\222?\013\302\001\010<vector>'
_NODEINFOLITE_LEGACY.fields_by_name['channel']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['channel']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE_LEGACY.fields_by_name['hops_away']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['hops_away']._serialized_options = b'\222?\0028\010'
_NODEINFOLITE_LEGACY.fields_by_name['next_hop']._options = None
_NODEINFOLITE_LEGACY.fields_by_name['next_hop']._serialized_options = b'\222?\0028\010'
_NODEDATABASE_LEGACY.fields_by_name['nodes']._options = None
_NODEDATABASE_LEGACY.fields_by_name['nodes']._serialized_options = b'\222?.\222\001+std::vector<meshtastic_NodeInfoLite_Legacy>'
_globals['_NODEINFOLITE_LEGACY']._serialized_start=178
_globals['_NODEINFOLITE_LEGACY']._serialized_end=574
_globals['_NODEDATABASE_LEGACY']._serialized_start=577
_globals['_NODEDATABASE_LEGACY']._serialized_end=723
# @@protoc_insertion_point(module_scope)
+106
View File
@@ -0,0 +1,106 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import collections.abc
import google.protobuf.descriptor
import google.protobuf.internal.containers
import google.protobuf.message
import meshtastic.protobuf.deviceonly_pb2
import meshtastic.protobuf.telemetry_pb2
import typing
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class NodeInfoLite_Legacy(google.protobuf.message.Message):
"""
Legacy NodeInfoLite descriptor used only to decode pre-split
/prefs/nodes.proto saves during the v24 -> v25 migration boot.
This preserves the original NodeInfoLite-compatible field numbers needed
to parse old wire bytes cleanly, including user (2), position (3),
device_metrics (6), and the legacy-only compatibility fields via_mqtt (8),
is_favorite (10), and is_ignored (11). Steady-state code does not use
this struct; it is dropped after migration completes. This file should be
removed once DEVICESTATE_MIN_VER advances past 24.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
USER_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
SNR_FIELD_NUMBER: builtins.int
LAST_HEARD_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
CHANNEL_FIELD_NUMBER: builtins.int
VIA_MQTT_FIELD_NUMBER: builtins.int
HOPS_AWAY_FIELD_NUMBER: builtins.int
IS_FAVORITE_FIELD_NUMBER: builtins.int
IS_IGNORED_FIELD_NUMBER: builtins.int
NEXT_HOP_FIELD_NUMBER: builtins.int
BITFIELD_FIELD_NUMBER: builtins.int
num: builtins.int
snr: builtins.float
last_heard: builtins.int
channel: builtins.int
via_mqtt: builtins.bool
hops_away: builtins.int
is_favorite: builtins.bool
is_ignored: builtins.bool
next_hop: builtins.int
bitfield: builtins.int
@property
def user(self) -> meshtastic.protobuf.deviceonly_pb2.UserLite: ...
@property
def position(self) -> meshtastic.protobuf.deviceonly_pb2.PositionLite: ...
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
user: meshtastic.protobuf.deviceonly_pb2.UserLite | None = ...,
position: meshtastic.protobuf.deviceonly_pb2.PositionLite | None = ...,
snr: builtins.float = ...,
last_heard: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
channel: builtins.int = ...,
via_mqtt: builtins.bool = ...,
hops_away: builtins.int | None = ...,
is_favorite: builtins.bool = ...,
is_ignored: builtins.bool = ...,
next_hop: builtins.int = ...,
bitfield: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "position", b"position", "user", b"user"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "last_heard", b"last_heard", "next_hop", b"next_hop", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_hops_away", b"_hops_away"]) -> typing.Literal["hops_away"] | None: ...
global___NodeInfoLite_Legacy = NodeInfoLite_Legacy
@typing.final
class NodeDatabase_Legacy(google.protobuf.message.Message):
"""
Legacy NodeDatabase shape: one repeated array of fat NodeInfoLite_Legacy
with no satellite position/telemetry arrays.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
NODES_FIELD_NUMBER: builtins.int
version: builtins.int
@property
def nodes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeInfoLite_Legacy]: ...
def __init__(
self,
*,
version: builtins.int = ...,
nodes: collections.abc.Iterable[global___NodeInfoLite_Legacy] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodes", b"nodes", "version", b"version"]) -> None: ...
global___NodeDatabase_Legacy = NodeDatabase_Legacy
+40 -14
View File
File diff suppressed because one or more lines are too long.
+159 -52
View File
@@ -30,6 +30,7 @@ class PositionLite(google.protobuf.message.Message):
ALTITUDE_FIELD_NUMBER: builtins.int
TIME_FIELD_NUMBER: builtins.int
LOCATION_SOURCE_FIELD_NUMBER: builtins.int
PRECISION_BITS_FIELD_NUMBER: builtins.int
latitude_i: builtins.int
"""
The new preferred location encoding, multiply by 1e-7 to get degrees
@@ -55,6 +56,10 @@ class PositionLite(google.protobuf.message.Message):
"""
TODO: REPLACE
"""
precision_bits: builtins.int
"""
Indicates the bits of precision set by the sending node
"""
def __init__(
self,
*,
@@ -63,8 +68,9 @@ class PositionLite(google.protobuf.message.Message):
altitude: builtins.int = ...,
time: builtins.int = ...,
location_source: meshtastic.protobuf.mesh_pb2.Position.LocSource.ValueType = ...,
precision_bits: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "latitude_i", b"latitude_i", "location_source", b"location_source", "longitude_i", b"longitude_i", "time", b"time"]) -> None: ...
def ClearField(self, field_name: typing.Literal["altitude", b"altitude", "latitude_i", b"latitude_i", "location_source", b"location_source", "longitude_i", b"longitude_i", "precision_bits", b"precision_bits", "time", b"time"]) -> None: ...
global___PositionLite = PositionLite
@@ -142,26 +148,26 @@ class NodeInfoLite(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
USER_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
SNR_FIELD_NUMBER: builtins.int
LAST_HEARD_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
CHANNEL_FIELD_NUMBER: builtins.int
VIA_MQTT_FIELD_NUMBER: builtins.int
HOPS_AWAY_FIELD_NUMBER: builtins.int
IS_FAVORITE_FIELD_NUMBER: builtins.int
IS_IGNORED_FIELD_NUMBER: builtins.int
NEXT_HOP_FIELD_NUMBER: builtins.int
BITFIELD_FIELD_NUMBER: builtins.int
LONG_NAME_FIELD_NUMBER: builtins.int
SHORT_NAME_FIELD_NUMBER: builtins.int
HW_MODEL_FIELD_NUMBER: builtins.int
ROLE_FIELD_NUMBER: builtins.int
PUBLIC_KEY_FIELD_NUMBER: builtins.int
SNR_Q4_FIELD_NUMBER: builtins.int
num: builtins.int
"""
The node number
"""
snr: builtins.float
"""
Returns the Signal-to-noise ratio (SNR) of the last received message,
as measured by the receiver. Return SNR of the last received message in dB
In-memory SNR of the last received message in dB. Not serialised directly:
always zeroed before encode; persisted as snr_q4 = 19 below.
"""
last_heard: builtins.int
"""
@@ -171,72 +177,69 @@ class NodeInfoLite(google.protobuf.message.Message):
"""
local channel index we heard that node on. Only populated if its not the default channel.
"""
via_mqtt: builtins.bool
"""
True if we witnessed the node over MQTT instead of LoRA transport
"""
hops_away: builtins.int
"""
Number of hops away from us this node is (0 if direct neighbor)
"""
is_favorite: builtins.bool
"""
True if node is in our favorites list
Persists between NodeDB internal clean ups
"""
is_ignored: builtins.bool
"""
True if node is in our ignored list
Persists between NodeDB internal clean ups
"""
next_hop: builtins.int
"""
Last byte of the node number of the node that should be used as the next hop to reach this node.
"""
bitfield: builtins.int
"""
Bitfield for storing booleans.
LSB 0 is_key_manually_verified
LSB 1 is_muted
Bitfield for storing booleans. See NODEINFO_BITFIELD_* in src/mesh/NodeDB.h.
"""
@property
def user(self) -> global___UserLite:
"""
The user info for this node
"""
long_name: builtins.str
"""Flattened user fields (formerly UserLite). macaddr dropped (deprecated 1.2.11).
@property
def position(self) -> global___PositionLite:
"""
This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
Position.time now indicates the last time we received a POSITION from that node.
"""
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics:
"""
The latest device metrics for the node.
"""
A full name for this user, i.e. "Kevin Hester".
"""
short_name: builtins.str
"""
A VERY short name, ideally two characters or an emoji.
Suitable for a tiny OLED screen.
"""
hw_model: meshtastic.protobuf.mesh_pb2.HardwareModel.ValueType
"""
Hardware model the user's device is running.
"""
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType
"""
The user's role in the mesh.
"""
public_key: builtins.bytes
"""
The public key of the user's device, for PKI-based encrypted DMs.
"""
snr_q4: builtins.int
"""
Q4-encoded SNR: dB × 4, sint32 zigzag. Matches RouteDiscovery convention.
Encode: snr_q4 = (int32_t)lroundf(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
float snr is always zeroed on disk; this field carries all persisted SNR.
A stored 0 does not by itself mean "unknown" here - see NODEINFO_BITFIELD_HAS_SNR in
src/mesh/NodeDB.h for the presence bit that disambiguates a genuine 0 dB reading from
"never measured".
"""
def __init__(
self,
*,
num: builtins.int = ...,
user: global___UserLite | None = ...,
position: global___PositionLite | None = ...,
snr: builtins.float = ...,
last_heard: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
channel: builtins.int = ...,
via_mqtt: builtins.bool = ...,
hops_away: builtins.int | None = ...,
is_favorite: builtins.bool = ...,
is_ignored: builtins.bool = ...,
next_hop: builtins.int = ...,
bitfield: builtins.int = ...,
long_name: builtins.str = ...,
short_name: builtins.str = ...,
hw_model: meshtastic.protobuf.mesh_pb2.HardwareModel.ValueType = ...,
role: meshtastic.protobuf.config_pb2.Config.DeviceConfig.Role.ValueType = ...,
public_key: builtins.bytes = ...,
snr_q4: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "position", b"position", "user", b"user"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "device_metrics", b"device_metrics", "hops_away", b"hops_away", "is_favorite", b"is_favorite", "is_ignored", b"is_ignored", "last_heard", b"last_heard", "next_hop", b"next_hop", "num", b"num", "position", b"position", "snr", b"snr", "user", b"user", "via_mqtt", b"via_mqtt"]) -> None: ...
def HasField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "hops_away", b"hops_away"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "bitfield", b"bitfield", "channel", b"channel", "hops_away", b"hops_away", "hw_model", b"hw_model", "last_heard", b"last_heard", "long_name", b"long_name", "next_hop", b"next_hop", "num", b"num", "public_key", b"public_key", "role", b"role", "short_name", b"short_name", "snr", b"snr", "snr_q4", b"snr_q4"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_hops_away", b"_hops_away"]) -> typing.Literal["hops_away"] | None: ...
global___NodeInfoLite = NodeInfoLite
@@ -337,12 +340,100 @@ class DeviceState(google.protobuf.message.Message):
global___DeviceState = DeviceState
@typing.final
class NodePositionEntry(google.protobuf.message.Message):
"""Satellite per-node entries; stored alongside the slim NodeInfoLite so nodes
that never report don't pay the embedded cost.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
POSITION_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def position(self) -> global___PositionLite: ...
def __init__(
self,
*,
num: builtins.int = ...,
position: global___PositionLite | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["position", b"position"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["num", b"num", "position", b"position"]) -> None: ...
global___NodePositionEntry = NodePositionEntry
@typing.final
class NodeTelemetryEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
DEVICE_METRICS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def device_metrics(self) -> meshtastic.protobuf.telemetry_pb2.DeviceMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
device_metrics: meshtastic.protobuf.telemetry_pb2.DeviceMetrics | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["device_metrics", b"device_metrics"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["device_metrics", b"device_metrics", "num", b"num"]) -> None: ...
global___NodeTelemetryEntry = NodeTelemetryEntry
@typing.final
class NodeEnvironmentEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
ENVIRONMENT_METRICS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def environment_metrics(self) -> meshtastic.protobuf.telemetry_pb2.EnvironmentMetrics: ...
def __init__(
self,
*,
num: builtins.int = ...,
environment_metrics: meshtastic.protobuf.telemetry_pb2.EnvironmentMetrics | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["environment_metrics", b"environment_metrics"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["environment_metrics", b"environment_metrics", "num", b"num"]) -> None: ...
global___NodeEnvironmentEntry = NodeEnvironmentEntry
@typing.final
class NodeStatusEntry(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
NUM_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
num: builtins.int
@property
def status(self) -> meshtastic.protobuf.mesh_pb2.StatusMessage: ...
def __init__(
self,
*,
num: builtins.int = ...,
status: meshtastic.protobuf.mesh_pb2.StatusMessage | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["status", b"status"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["num", b"num", "status", b"status"]) -> None: ...
global___NodeStatusEntry = NodeStatusEntry
@typing.final
class NodeDatabase(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
VERSION_FIELD_NUMBER: builtins.int
NODES_FIELD_NUMBER: builtins.int
POSITIONS_FIELD_NUMBER: builtins.int
TELEMETRY_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
ENVIRONMENT_FIELD_NUMBER: builtins.int
version: builtins.int
"""
A version integer used to invalidate old save files when we make
@@ -355,13 +446,29 @@ class NodeDatabase(google.protobuf.message.Message):
New lite version of NodeDB to decrease memory footprint
"""
@property
def positions(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodePositionEntry]:
"""Per-NodeNum satellite arrays. Constrained platforms (e.g. STM32WL) omit
these via MESHTASTIC_EXCLUDE_*DB build flags.
"""
@property
def telemetry(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeTelemetryEntry]: ...
@property
def status(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeStatusEntry]: ...
@property
def environment(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___NodeEnvironmentEntry]: ...
def __init__(
self,
*,
version: builtins.int = ...,
nodes: collections.abc.Iterable[global___NodeInfoLite] | None = ...,
positions: collections.abc.Iterable[global___NodePositionEntry] | None = ...,
telemetry: collections.abc.Iterable[global___NodeTelemetryEntry] | None = ...,
status: collections.abc.Iterable[global___NodeStatusEntry] | None = ...,
environment: collections.abc.Iterable[global___NodeEnvironmentEntry] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["nodes", b"nodes", "version", b"version"]) -> None: ...
def ClearField(self, field_name: typing.Literal["environment", b"environment", "nodes", b"nodes", "positions", b"positions", "status", b"status", "telemetry", b"telemetry", "version", b"version"]) -> None: ...
global___NodeDatabase = NodeDatabase
+47 -7
View File
@@ -14,7 +14,7 @@ _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\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')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%meshtastic/protobuf/interdevice.proto\x12\x13meshtastic.protobuf\x1a meshtastic/protobuf/nanopb.proto\"\xf6\x01\n\x0c\x46ileTransfer\x12\x35\n\toperation\x18\x01 \x01(\x0e\x32\".meshtastic.protobuf.FileOperation\x12\x18\n\x08\x66ilepath\x18\x02 \x01(\tB\x06\x92?\x03\x08\x80\x02\x12\x18\n\x08\x66iledata\x18\x03 \x01(\x0c\x42\x06\x92?\x03\x08\x80 \x12/\n\x06status\x18\x04 \x01(\x0e\x32\x1f.meshtastic.protobuf.FileStatus\x12\x17\n\x07message\x18\x05 \x01(\tB\x06\x92?\x03\x08\xff\x01\x12\x0e\n\x06offset\x18\x06 \x01(\x04\x12\x0e\n\x06length\x18\x07 \x01(\r\x12\x11\n\tfile_size\x18\x08 \x01(\x04\"\xb9\x01\n\x10\x44irectoryListing\x12\x19\n\tdirectory\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x02\x12\x1b\n\tfilenames\x18\x02 \x03(\tB\x08\x92?\x05\x10\x10p\xff\x01\x12/\n\x06status\x18\x03 \x01(\x0e\x32\x1f.meshtastic.protobuf.FileStatus\x12\x17\n\x07message\x18\x04 \x01(\tB\x06\x92?\x03\x08\xff\x01\x12\x0e\n\x06offset\x18\x05 \x01(\r\x12\x13\n\x0btotal_count\x18\x06 \x01(\r\"O\n\x0eI2CTransaction\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\r\x12\x1a\n\nwrite_data\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02\x12\x10\n\x08read_len\x18\x03 \x01(\r\"\x92\x03\n\nSdCardInfo\x12\x0f\n\x07present\x18\x01 \x01(\x08\x12;\n\tcard_type\x18\x02 \x01(\x0e\x32(.meshtastic.protobuf.SdCardInfo.CardType\x12\x39\n\x08\x66\x61t_type\x18\x03 \x01(\x0e\x32\'.meshtastic.protobuf.SdCardInfo.FatType\x12\x11\n\tcard_size\x18\x04 \x01(\x04\x12\x12\n\nused_bytes\x18\x05 \x01(\x04\x12\x12\n\nfree_bytes\x18\x06 \x01(\x04\x12\x13\n\x0bstats_valid\x18\x07 \x01(\x08\x12\x0c\n\x04\x62usy\x18\x08 \x01(\x08\x12\x13\n\x0bunformatted\x18\t \x01(\x08\"K\n\x08\x43\x61rdType\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03MMC\x10\x01\x12\x06\n\x02SD\x10\x02\x12\x08\n\x04SDHC\x10\x03\x12\x08\n\x04SDXC\x10\x04\x12\x10\n\x0cUNKNOWN_CARD\x10\x05\";\n\x07\x46\x61tType\x12\x0f\n\x0bUNKNOWN_FAT\x10\x00\x12\t\n\x05\x46\x41T16\x10\x01\x12\t\n\x05\x46\x41T32\x10\x02\x12\t\n\x05\x45XFAT\x10\x03\"\xac\x01\n\tI2CResult\x12\x35\n\x06status\x18\x01 \x01(\x0e\x32%.meshtastic.protobuf.I2CResult.Status\x12\x19\n\tread_data\x18\x02 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x02\"M\n\x06Status\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x06\n\x02OK\x10\x01\x12\x10\n\x0cNACK_ADDRESS\x10\x02\x12\r\n\tNACK_DATA\x10\x03\x12\t\n\x05\x45RROR\x10\x04\"\x87\x05\n\x12InterdeviceMessage\x12\n\n\x02id\x18\x0f \x01(\r\x12\x16\n\x04nmea\x18\x01 \x01(\tB\x06\x92?\x03\x08\x80\x08H\x00\x12\x15\n\x04\x62\x65\x65p\x18\x02 \x01(\rB\x05\x92?\x02\x38\x10H\x00\x12>\n\x0fi2c_transaction\x18\x03 \x01(\x0b\x32#.meshtastic.protobuf.I2CTransactionH\x00\x12\x34\n\ni2c_result\x18\x04 \x01(\x0b\x32\x1e.meshtastic.protobuf.I2CResultH\x00\x12\x12\n\x08i2c_scan\x18\x05 \x01(\x08H\x00\x12!\n\x0fi2c_scan_result\x18\x06 \x01(\x0c\x42\x06\x92?\x03\x08\x80\x01H\x00\x12:\n\rfile_transfer\x18\x07 \x01(\x0b\x32!.meshtastic.protobuf.FileTransferH\x00\x12\x42\n\x11\x64irectory_listing\x18\x08 \x01(\x0b\x32%.meshtastic.protobuf.DirectoryListingH\x00\x12\x15\n\x0bget_sd_info\x18\t \x01(\x08H\x00\x12\x32\n\x07sd_info\x18\n \x01(\x0b\x32\x1f.meshtastic.protobuf.SdCardInfoH\x00\x12\x37\n\x04ping\x18\x0b \x01(\x0e\x32\'.meshtastic.protobuf.InterdeviceVersionH\x00\x12\x37\n\x04pong\x18\x0c \x01(\x0e\x32\'.meshtastic.protobuf.InterdeviceVersionH\x00\x12\x0e\n\x04nack\x18\r \x01(\x08H\x00\x12\x34\n\nsd_command\x18\x0e \x01(\x0e\x32\x1e.meshtastic.protobuf.SdCommandH\x00\x42\x06\n\x04\x64\x61ta*Z\n\x12InterdeviceVersion\x12#\n\x1fINTERDEVICE_VERSION_UNSPECIFIED\x10\x00\x12\x1f\n\x1bINTERDEVICE_VERSION_CURRENT\x10\x02*7\n\rFileOperation\x12\x07\n\x03GET\x10\x00\x12\x08\n\x04POST\x10\x01\x12\x07\n\x03PUT\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03*\xa6\x01\n\nFileStatus\x12\x14\n\x10\x46ILE_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x46ILE_OK\x10\x01\x12\r\n\tFILE_BUSY\x10\x02\x12\x10\n\x0c\x46ILE_NO_CARD\x10\x03\x12\x12\n\x0e\x46ILE_NOT_FOUND\x10\x04\x12\x18\n\x14\x46ILE_OFFSET_CONFLICT\x10\x05\x12\x11\n\rFILE_IO_ERROR\x10\x06\x12\x13\n\x0f\x46ILE_NOT_A_FILE\x10\x07*R\n\tSdCommand\x12\x1a\n\x16SD_COMMAND_UNSPECIFIED\x10\x00\x12\x0c\n\x08SD_MOUNT\x10\x01\x12\x0c\n\x08SD_EJECT\x10\x02\x12\r\n\tSD_FORMAT\x10\x03\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)
@@ -22,12 +22,52 @@ _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'
_FILETRANSFER.fields_by_name['filepath']._options = None
_FILETRANSFER.fields_by_name['filepath']._serialized_options = b'\222?\003\010\200\002'
_FILETRANSFER.fields_by_name['filedata']._options = None
_FILETRANSFER.fields_by_name['filedata']._serialized_options = b'\222?\003\010\200 '
_FILETRANSFER.fields_by_name['message']._options = None
_FILETRANSFER.fields_by_name['message']._serialized_options = b'\222?\003\010\377\001'
_DIRECTORYLISTING.fields_by_name['directory']._options = None
_DIRECTORYLISTING.fields_by_name['directory']._serialized_options = b'\222?\003\010\200\002'
_DIRECTORYLISTING.fields_by_name['filenames']._options = None
_DIRECTORYLISTING.fields_by_name['filenames']._serialized_options = b'\222?\005\020\020p\377\001'
_DIRECTORYLISTING.fields_by_name['message']._options = None
_DIRECTORYLISTING.fields_by_name['message']._serialized_options = b'\222?\003\010\377\001'
_I2CTRANSACTION.fields_by_name['write_data']._options = None
_I2CTRANSACTION.fields_by_name['write_data']._serialized_options = b'\222?\003\010\200\002'
_I2CRESULT.fields_by_name['read_data']._options = None
_I2CRESULT.fields_by_name['read_data']._serialized_options = b'\222?\003\010\200\002'
_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
_INTERDEVICEMESSAGE.fields_by_name['beep']._options = None
_INTERDEVICEMESSAGE.fields_by_name['beep']._serialized_options = b'\222?\0028\020'
_INTERDEVICEMESSAGE.fields_by_name['i2c_scan_result']._options = None
_INTERDEVICEMESSAGE.fields_by_name['i2c_scan_result']._serialized_options = b'\222?\003\010\200\001'
_globals['_INTERDEVICEVERSION']._serialized_start=1844
_globals['_INTERDEVICEVERSION']._serialized_end=1934
_globals['_FILEOPERATION']._serialized_start=1936
_globals['_FILEOPERATION']._serialized_end=1991
_globals['_FILESTATUS']._serialized_start=1994
_globals['_FILESTATUS']._serialized_end=2160
_globals['_SDCOMMAND']._serialized_start=2162
_globals['_SDCOMMAND']._serialized_end=2244
_globals['_FILETRANSFER']._serialized_start=97
_globals['_FILETRANSFER']._serialized_end=343
_globals['_DIRECTORYLISTING']._serialized_start=346
_globals['_DIRECTORYLISTING']._serialized_end=531
_globals['_I2CTRANSACTION']._serialized_start=533
_globals['_I2CTRANSACTION']._serialized_end=612
_globals['_SDCARDINFO']._serialized_start=615
_globals['_SDCARDINFO']._serialized_end=1017
_globals['_SDCARDINFO_CARDTYPE']._serialized_start=881
_globals['_SDCARDINFO_CARDTYPE']._serialized_end=956
_globals['_SDCARDINFO_FATTYPE']._serialized_start=958
_globals['_SDCARDINFO_FATTYPE']._serialized_end=1017
_globals['_I2CRESULT']._serialized_start=1020
_globals['_I2CRESULT']._serialized_end=1192
_globals['_I2CRESULT_STATUS']._serialized_start=1115
_globals['_I2CRESULT_STATUS']._serialized_end=1192
_globals['_INTERDEVICEMESSAGE']._serialized_start=1195
_globals['_INTERDEVICEMESSAGE']._serialized_end=1842
# @@protoc_insertion_point(module_scope)
+445 -56
View File
@@ -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
@@ -17,89 +19,476 @@ else:
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
class _MessageType:
class _InterdeviceVersion:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _MessageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_MessageType.ValueType], builtins.type):
class _InterdeviceVersionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_InterdeviceVersion.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
ACK: _MessageType.ValueType # 0
COLLECT_INTERVAL: _MessageType.ValueType # 160
"""in ms"""
BEEP_ON: _MessageType.ValueType # 161
"""duration ms"""
BEEP_OFF: _MessageType.ValueType # 162
"""cancel prematurely"""
SHUTDOWN: _MessageType.ValueType # 163
POWER_ON: _MessageType.ValueType # 164
SCD41_TEMP: _MessageType.ValueType # 176
SCD41_HUMIDITY: _MessageType.ValueType # 177
SCD41_CO2: _MessageType.ValueType # 178
AHT20_TEMP: _MessageType.ValueType # 179
AHT20_HUMIDITY: _MessageType.ValueType # 180
TVOC_INDEX: _MessageType.ValueType # 181
INTERDEVICE_VERSION_UNSPECIFIED: _InterdeviceVersion.ValueType # 0
INTERDEVICE_VERSION_CURRENT: _InterdeviceVersion.ValueType # 2
"""Never use 1: ping/pong were bools before the handshake existed, and a
bool true is the same varint on the wire as the number 1, so firmware
predating the handshake would pass it.
"""
class MessageType(_MessageType, metaclass=_MessageTypeEnumTypeWrapper):
"""encapsulate up to 1k of NMEA string data"""
class InterdeviceVersion(_InterdeviceVersion, metaclass=_InterdeviceVersionEnumTypeWrapper):
"""Version of the interdevice protocol spoken on the link. Both sides send
theirs in the ping/pong handshake; a peer reporting a different one runs
firmware that does not match and is not talked to.
ACK: MessageType.ValueType # 0
COLLECT_INTERVAL: MessageType.ValueType # 160
"""in ms"""
BEEP_ON: MessageType.ValueType # 161
"""duration ms"""
BEEP_OFF: MessageType.ValueType # 162
"""cancel prematurely"""
SHUTDOWN: MessageType.ValueType # 163
POWER_ON: MessageType.ValueType # 164
SCD41_TEMP: MessageType.ValueType # 176
SCD41_HUMIDITY: MessageType.ValueType # 177
SCD41_CO2: MessageType.ValueType # 178
AHT20_TEMP: MessageType.ValueType # 179
AHT20_HUMIDITY: MessageType.ValueType # 180
TVOC_INDEX: MessageType.ValueType # 181
global___MessageType = MessageType
On a change that breaks the other side (renumbered fields, changed
semantics, removed messages), raise the value of CURRENT. Do not add
another entry: this enum carries a single constant, not a history.
"""
INTERDEVICE_VERSION_UNSPECIFIED: InterdeviceVersion.ValueType # 0
INTERDEVICE_VERSION_CURRENT: InterdeviceVersion.ValueType # 2
"""Never use 1: ping/pong were bools before the handshake existed, and a
bool true is the same varint on the wire as the number 1, so firmware
predating the handshake would pass it.
"""
global___InterdeviceVersion = InterdeviceVersion
class _FileOperation:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FileOperationEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FileOperation.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
GET: _FileOperation.ValueType # 0
POST: _FileOperation.ValueType # 1
PUT: _FileOperation.ValueType # 2
DELETE: _FileOperation.ValueType # 3
class FileOperation(_FileOperation, metaclass=_FileOperationEnumTypeWrapper):
"""Defines the supported file operations"""
GET: FileOperation.ValueType # 0
POST: FileOperation.ValueType # 1
PUT: FileOperation.ValueType # 2
DELETE: FileOperation.ValueType # 3
global___FileOperation = FileOperation
class _FileStatus:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FileStatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_FileStatus.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FILE_UNSPECIFIED: _FileStatus.ValueType # 0
FILE_OK: _FileStatus.ValueType # 1
FILE_BUSY: _FileStatus.ValueType # 2
"""Retry later: the co-processor is doing card maintenance (mount,
free space scan) and cannot serve the request right now
"""
FILE_NO_CARD: _FileStatus.ValueType # 3
FILE_NOT_FOUND: _FileStatus.ValueType # 4
FILE_OFFSET_CONFLICT: _FileStatus.ValueType # 5
"""PUT only: offset did not match the current end of the file. file_size
carries the size the file actually has, so the writer can resync (or
recognize its own chunk as already written after a lost response).
"""
FILE_IO_ERROR: _FileStatus.ValueType # 6
FILE_NOT_A_FILE: _FileStatus.ValueType # 7
"""path is a directory (GET) or not one (listing)"""
class FileStatus(_FileStatus, metaclass=_FileStatusEnumTypeWrapper):
"""Outcome of a file or directory operation. The requester must be able to
tell a transient condition from a definitive one: BUSY is worth another
try, NOT_FOUND is not.
"""
FILE_UNSPECIFIED: FileStatus.ValueType # 0
FILE_OK: FileStatus.ValueType # 1
FILE_BUSY: FileStatus.ValueType # 2
"""Retry later: the co-processor is doing card maintenance (mount,
free space scan) and cannot serve the request right now
"""
FILE_NO_CARD: FileStatus.ValueType # 3
FILE_NOT_FOUND: FileStatus.ValueType # 4
FILE_OFFSET_CONFLICT: FileStatus.ValueType # 5
"""PUT only: offset did not match the current end of the file. file_size
carries the size the file actually has, so the writer can resync (or
recognize its own chunk as already written after a lost response).
"""
FILE_IO_ERROR: FileStatus.ValueType # 6
FILE_NOT_A_FILE: FileStatus.ValueType # 7
"""path is a directory (GET) or not one (listing)"""
global___FileStatus = FileStatus
class _SdCommand:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _SdCommandEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_SdCommand.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
SD_COMMAND_UNSPECIFIED: _SdCommand.ValueType # 0
SD_MOUNT: _SdCommand.ValueType # 1
"""mount a card that is in the slot, also after an eject"""
SD_EJECT: _SdCommand.ValueType # 2
"""flush and release the card so it can be pulled safely"""
SD_FORMAT: _SdCommand.ValueType # 3
"""wipe the card and put a fresh FAT on it, then mount it"""
class SdCommand(_SdCommand, metaclass=_SdCommandEnumTypeWrapper):
"""What to do with the SD card of the co-processor"""
SD_COMMAND_UNSPECIFIED: SdCommand.ValueType # 0
SD_MOUNT: SdCommand.ValueType # 1
"""mount a card that is in the slot, also after an eject"""
SD_EJECT: SdCommand.ValueType # 2
"""flush and release the card so it can be pulled safely"""
SD_FORMAT: SdCommand.ValueType # 3
"""wipe the card and put a fresh FAT on it, then mount it"""
global___SdCommand = SdCommand
@typing.final
class SensorData(google.protobuf.message.Message):
class FileTransfer(google.protobuf.message.Message):
"""Message for file operations"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
TYPE_FIELD_NUMBER: builtins.int
FLOAT_VALUE_FIELD_NUMBER: builtins.int
UINT32_VALUE_FIELD_NUMBER: builtins.int
type: global___MessageType.ValueType
"""The message type"""
float_value: builtins.float
uint32_value: builtins.int
OPERATION_FIELD_NUMBER: builtins.int
FILEPATH_FIELD_NUMBER: builtins.int
FILEDATA_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
MESSAGE_FIELD_NUMBER: builtins.int
OFFSET_FIELD_NUMBER: builtins.int
LENGTH_FIELD_NUMBER: builtins.int
FILE_SIZE_FIELD_NUMBER: builtins.int
operation: global___FileOperation.ValueType
"""File operation (GET, POST, PUT, DELETE)"""
filepath: builtins.str
filedata: builtins.bytes
status: global___FileStatus.ValueType
"""Response: outcome of the operation"""
message: builtins.str
offset: builtins.int
"""Byte offset of this chunk within the file (ranged GET/PUT)"""
length: builtins.int
"""GET request: number of bytes to read, 0 = max chunk size. A response
carries at most the filedata max_size (see interdevice.options) per
chunk; larger requests are truncated, visible in the filedata length.
"""
file_size: builtins.int
"""GET response: total size of the file"""
def __init__(
self,
*,
type: global___MessageType.ValueType = ...,
float_value: builtins.float = ...,
uint32_value: builtins.int = ...,
operation: global___FileOperation.ValueType = ...,
filepath: builtins.str = ...,
filedata: builtins.bytes = ...,
status: global___FileStatus.ValueType = ...,
message: builtins.str = ...,
offset: builtins.int = ...,
length: builtins.int = ...,
file_size: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["data", b"data", "float_value", b"float_value", "uint32_value", b"uint32_value"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "float_value", b"float_value", "type", b"type", "uint32_value", b"uint32_value"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["data", b"data"]) -> typing.Literal["float_value", "uint32_value"] | None: ...
def ClearField(self, field_name: typing.Literal["file_size", b"file_size", "filedata", b"filedata", "filepath", b"filepath", "length", b"length", "message", b"message", "offset", b"offset", "operation", b"operation", "status", b"status"]) -> None: ...
global___SensorData = SensorData
global___FileTransfer = FileTransfer
@typing.final
class DirectoryListing(google.protobuf.message.Message):
"""Message for structured directory listing"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
DIRECTORY_FIELD_NUMBER: builtins.int
FILENAMES_FIELD_NUMBER: builtins.int
STATUS_FIELD_NUMBER: builtins.int
MESSAGE_FIELD_NUMBER: builtins.int
OFFSET_FIELD_NUMBER: builtins.int
TOTAL_COUNT_FIELD_NUMBER: builtins.int
directory: builtins.str
status: global___FileStatus.ValueType
"""Response: outcome of the operation"""
message: builtins.str
offset: builtins.int
"""Request: skip this many entries (paging)"""
total_count: builtins.int
"""Response: total number of entries in the directory"""
@property
def filenames(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
"""One page of entry names, full FAT LFN length. Subdirectories carry a
trailing slash. Note that a name whose directory prefix pushes the
combined path past the FileTransfer.filepath limit cannot round-trip.
Page size is the max_count in interdevice.options; page through with
offset and total_count.
"""
def __init__(
self,
*,
directory: builtins.str = ...,
filenames: collections.abc.Iterable[builtins.str] | None = ...,
status: global___FileStatus.ValueType = ...,
message: builtins.str = ...,
offset: builtins.int = ...,
total_count: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["directory", b"directory", "filenames", b"filenames", "message", b"message", "offset", b"offset", "status", b"status", "total_count", b"total_count"]) -> None: ...
global___DirectoryListing = DirectoryListing
@typing.final
class I2CTransaction(google.protobuf.message.Message):
"""A single I2C transaction: an optional write followed by an optional
read with repeated start, matching the TwoWire usage of sensor drivers
(beginTransmission/write.../endTransmission(false)/requestFrom)
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ADDRESS_FIELD_NUMBER: builtins.int
WRITE_DATA_FIELD_NUMBER: builtins.int
READ_LEN_FIELD_NUMBER: builtins.int
address: builtins.int
"""7-bit device address"""
write_data: builtins.bytes
read_len: builtins.int
"""Number of bytes to read after the write, 0 = write-only. Bounded by
the read_data max_size of I2CResult (see interdevice.options); larger
requests are truncated, visible in the returned byte count.
"""
def __init__(
self,
*,
address: builtins.int = ...,
write_data: builtins.bytes = ...,
read_len: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["address", b"address", "read_len", b"read_len", "write_data", b"write_data"]) -> None: ...
global___I2CTransaction = I2CTransaction
@typing.final
class SdCardInfo(google.protobuf.message.Message):
"""SD card statistics"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _CardType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _CardTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SdCardInfo._CardType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
NONE: SdCardInfo._CardType.ValueType # 0
MMC: SdCardInfo._CardType.ValueType # 1
SD: SdCardInfo._CardType.ValueType # 2
SDHC: SdCardInfo._CardType.ValueType # 3
SDXC: SdCardInfo._CardType.ValueType # 4
UNKNOWN_CARD: SdCardInfo._CardType.ValueType # 5
class CardType(_CardType, metaclass=_CardTypeEnumTypeWrapper): ...
NONE: SdCardInfo.CardType.ValueType # 0
MMC: SdCardInfo.CardType.ValueType # 1
SD: SdCardInfo.CardType.ValueType # 2
SDHC: SdCardInfo.CardType.ValueType # 3
SDXC: SdCardInfo.CardType.ValueType # 4
UNKNOWN_CARD: SdCardInfo.CardType.ValueType # 5
class _FatType:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FatTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SdCardInfo._FatType.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNKNOWN_FAT: SdCardInfo._FatType.ValueType # 0
FAT16: SdCardInfo._FatType.ValueType # 1
FAT32: SdCardInfo._FatType.ValueType # 2
EXFAT: SdCardInfo._FatType.ValueType # 3
class FatType(_FatType, metaclass=_FatTypeEnumTypeWrapper): ...
UNKNOWN_FAT: SdCardInfo.FatType.ValueType # 0
FAT16: SdCardInfo.FatType.ValueType # 1
FAT32: SdCardInfo.FatType.ValueType # 2
EXFAT: SdCardInfo.FatType.ValueType # 3
PRESENT_FIELD_NUMBER: builtins.int
CARD_TYPE_FIELD_NUMBER: builtins.int
FAT_TYPE_FIELD_NUMBER: builtins.int
CARD_SIZE_FIELD_NUMBER: builtins.int
USED_BYTES_FIELD_NUMBER: builtins.int
FREE_BYTES_FIELD_NUMBER: builtins.int
STATS_VALID_FIELD_NUMBER: builtins.int
BUSY_FIELD_NUMBER: builtins.int
UNFORMATTED_FIELD_NUMBER: builtins.int
present: builtins.bool
"""Card initialized and usable. False while `busy` is set does not mean
there is no card: the co-processor does not know yet.
"""
card_type: global___SdCardInfo.CardType.ValueType
fat_type: global___SdCardInfo.FatType.ValueType
card_size: builtins.int
"""Filesystem size in bytes"""
used_bytes: builtins.int
"""Used bytes (may be expensive to compute on FAT32)"""
free_bytes: builtins.int
"""Free bytes"""
stats_valid: builtins.bool
"""used_bytes/free_bytes are only meaningful when true: the scan behind
them runs in the background after mount and can take a while, and a
full card is otherwise indistinguishable from a scan in progress
"""
busy: builtins.bool
"""The co-processor is mounting a card right now, so whether one is
present is not decided yet. Ask again rather than concluding the slot
is empty.
"""
unformatted: builtins.bool
"""A card answers in the slot but carries no filesystem that could be
mounted (present is false then). Formatting it makes it usable.
"""
def __init__(
self,
*,
present: builtins.bool = ...,
card_type: global___SdCardInfo.CardType.ValueType = ...,
fat_type: global___SdCardInfo.FatType.ValueType = ...,
card_size: builtins.int = ...,
used_bytes: builtins.int = ...,
free_bytes: builtins.int = ...,
stats_valid: builtins.bool = ...,
busy: builtins.bool = ...,
unformatted: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["busy", b"busy", "card_size", b"card_size", "card_type", b"card_type", "fat_type", b"fat_type", "free_bytes", b"free_bytes", "present", b"present", "stats_valid", b"stats_valid", "unformatted", b"unformatted", "used_bytes", b"used_bytes"]) -> None: ...
global___SdCardInfo = SdCardInfo
@typing.final
class I2CResult(google.protobuf.message.Message):
"""Result of an I2CTransaction"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Status:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _StatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[I2CResult._Status.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNSPECIFIED: I2CResult._Status.ValueType # 0
"""Never sent: an all-defaults (e.g. accidentally empty) message must
not decode as a successful transaction
"""
OK: I2CResult._Status.ValueType # 1
NACK_ADDRESS: I2CResult._Status.ValueType # 2
NACK_DATA: I2CResult._Status.ValueType # 3
ERROR: I2CResult._Status.ValueType # 4
class Status(_Status, metaclass=_StatusEnumTypeWrapper): ...
UNSPECIFIED: I2CResult.Status.ValueType # 0
"""Never sent: an all-defaults (e.g. accidentally empty) message must
not decode as a successful transaction
"""
OK: I2CResult.Status.ValueType # 1
NACK_ADDRESS: I2CResult.Status.ValueType # 2
NACK_DATA: I2CResult.Status.ValueType # 3
ERROR: I2CResult.Status.ValueType # 4
STATUS_FIELD_NUMBER: builtins.int
READ_DATA_FIELD_NUMBER: builtins.int
status: global___I2CResult.Status.ValueType
read_data: builtins.bytes
def __init__(
self,
*,
status: global___I2CResult.Status.ValueType = ...,
read_data: builtins.bytes = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["read_data", b"read_data", "status", b"status"]) -> None: ...
global___I2CResult = I2CResult
@typing.final
class InterdeviceMessage(google.protobuf.message.Message):
"""Main message for interdevice communication"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ID_FIELD_NUMBER: builtins.int
NMEA_FIELD_NUMBER: builtins.int
SENSOR_FIELD_NUMBER: builtins.int
BEEP_FIELD_NUMBER: builtins.int
I2C_TRANSACTION_FIELD_NUMBER: builtins.int
I2C_RESULT_FIELD_NUMBER: builtins.int
I2C_SCAN_FIELD_NUMBER: builtins.int
I2C_SCAN_RESULT_FIELD_NUMBER: builtins.int
FILE_TRANSFER_FIELD_NUMBER: builtins.int
DIRECTORY_LISTING_FIELD_NUMBER: builtins.int
GET_SD_INFO_FIELD_NUMBER: builtins.int
SD_INFO_FIELD_NUMBER: builtins.int
PING_FIELD_NUMBER: builtins.int
PONG_FIELD_NUMBER: builtins.int
NACK_FIELD_NUMBER: builtins.int
SD_COMMAND_FIELD_NUMBER: builtins.int
id: builtins.int
"""Correlates a response with its request: responses echo the id of the
request they answer. 0 for unsolicited messages (e.g. the nmea stream).
"""
nmea: builtins.str
beep: builtins.int
i2c_scan: builtins.bool
"""Request: scan the secondary I2C bus"""
i2c_scan_result: builtins.bytes
get_sd_info: builtins.bool
"""Request: SD card statistics"""
ping: global___InterdeviceVersion.ValueType
"""Link liveness probe and version handshake. The receiver answers ping
with pong, echoing the id. Touches no peripherals, so it works with
nothing attached. Both carry the version the sender speaks; a peer
that answers with a different one speaks another protocol and must
not be used.
"""
pong: global___InterdeviceVersion.ValueType
nack: builtins.bool
"""Response: the request could not be decoded or is of an unhandled
type, so the requester fails fast instead of burning its timeout.
Echoes the id when known, 0 when the frame was undecodable. Never
sent in reaction to a nack.
"""
sd_command: global___SdCommand.ValueType
"""Request: mount the card, or release it so it can be pulled safely. The
co-processor answers with sd_info. Without an eject the card is mounted
on its own and kept mounted; after one it stays released until a mount
is asked for.
"""
@property
def sensor(self) -> global___SensorData: ...
def i2c_transaction(self) -> global___I2CTransaction: ...
@property
def i2c_result(self) -> global___I2CResult: ...
@property
def file_transfer(self) -> global___FileTransfer: ...
@property
def directory_listing(self) -> global___DirectoryListing: ...
@property
def sd_info(self) -> global___SdCardInfo:
"""Response"""
def __init__(
self,
*,
id: builtins.int = ...,
nmea: builtins.str = ...,
sensor: global___SensorData | None = ...,
beep: builtins.int = ...,
i2c_transaction: global___I2CTransaction | None = ...,
i2c_result: global___I2CResult | None = ...,
i2c_scan: builtins.bool = ...,
i2c_scan_result: builtins.bytes = ...,
file_transfer: global___FileTransfer | None = ...,
directory_listing: global___DirectoryListing | None = ...,
get_sd_info: builtins.bool = ...,
sd_info: global___SdCardInfo | None = ...,
ping: global___InterdeviceVersion.ValueType = ...,
pong: global___InterdeviceVersion.ValueType = ...,
nack: builtins.bool = ...,
sd_command: global___SdCommand.ValueType = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["data", b"data", "nmea", b"nmea", "sensor", b"sensor"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["data", b"data", "nmea", b"nmea", "sensor", b"sensor"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["data", b"data"]) -> typing.Literal["nmea", "sensor"] | None: ...
def HasField(self, field_name: typing.Literal["beep", b"beep", "data", b"data", "directory_listing", b"directory_listing", "file_transfer", b"file_transfer", "get_sd_info", b"get_sd_info", "i2c_result", b"i2c_result", "i2c_scan", b"i2c_scan", "i2c_scan_result", b"i2c_scan_result", "i2c_transaction", b"i2c_transaction", "nack", b"nack", "nmea", b"nmea", "ping", b"ping", "pong", b"pong", "sd_command", b"sd_command", "sd_info", b"sd_info"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["beep", b"beep", "data", b"data", "directory_listing", b"directory_listing", "file_transfer", b"file_transfer", "get_sd_info", b"get_sd_info", "i2c_result", b"i2c_result", "i2c_scan", b"i2c_scan", "i2c_scan_result", b"i2c_scan_result", "i2c_transaction", b"i2c_transaction", "id", b"id", "nack", b"nack", "nmea", b"nmea", "ping", b"ping", "pong", b"pong", "sd_command", b"sd_command", "sd_info", b"sd_info"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["data", b"data"]) -> typing.Literal["nmea", "beep", "i2c_transaction", "i2c_result", "i2c_scan", "i2c_scan_result", "file_transfer", "directory_listing", "get_sd_info", "sd_info", "ping", "pong", "nack", "sd_command"] | None: ...
global___InterdeviceMessage = InterdeviceMessage
+2 -2
View File
@@ -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\"\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')
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\"\x98\n\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\x12G\n\x0bmesh_beacon\x18\x12 \x01(\x0b\x32\x32.meshtastic.protobuf.ModuleConfig.MeshBeaconConfig\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=1876
_globals['_LOCALMODULECONFIG']._serialized_end=1949
# @@protoc_insertion_point(module_scope)
+10 -2
View File
@@ -122,6 +122,7 @@ class LocalModuleConfig(google.protobuf.message.Message):
STATUSMESSAGE_FIELD_NUMBER: builtins.int
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
TAK_FIELD_NUMBER: builtins.int
MESH_BEACON_FIELD_NUMBER: builtins.int
VERSION_FIELD_NUMBER: builtins.int
version: builtins.int
"""
@@ -225,6 +226,12 @@ class LocalModuleConfig(google.protobuf.message.Message):
TAK Config
"""
@property
def mesh_beacon(self) -> meshtastic.protobuf.module_config_pb2.ModuleConfig.MeshBeaconConfig:
"""
MeshBeacon Config
"""
def __init__(
self,
*,
@@ -244,9 +251,10 @@ class LocalModuleConfig(google.protobuf.message.Message):
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 = ...,
mesh_beacon: meshtastic.protobuf.module_config_pb2.ModuleConfig.MeshBeaconConfig | 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", "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: ...
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", "mesh_beacon", b"mesh_beacon", "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", "mesh_beacon", b"mesh_beacon", "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
+31
View File
@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: meshtastic/protobuf/mesh_beacon.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 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/mesh_beacon.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a meshtastic/protobuf/config.proto\x1a meshtastic/protobuf/nanopb.proto\"\x8a\x02\n\nMeshBeacon\x12\x16\n\x07message\x18\x01 \x01(\tB\x05\x92?\x02\x08\x65\x12;\n\roffer_channel\x18\x02 \x01(\x0b\x32$.meshtastic.protobuf.ChannelSettings\x12G\n\x0coffer_region\x18\x03 \x01(\x0e\x32\x31.meshtastic.protobuf.Config.LoRaConfig.RegionCode\x12M\n\x0coffer_preset\x18\x04 \x01(\x0e\x32\x32.meshtastic.protobuf.Config.LoRaConfig.ModemPresetH\x00\x88\x01\x01\x42\x0f\n\r_offer_presetBf\n\x14org.meshtastic.protoB\x10MeshBeaconProtosZ\"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.mesh_beacon_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\024org.meshtastic.protoB\020MeshBeaconProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_MESHBEACON.fields_by_name['message']._options = None
_MESHBEACON.fields_by_name['message']._serialized_options = b'\222?\002\010e'
_globals['_MESHBEACON']._serialized_start=166
_globals['_MESHBEACON']._serialized_end=432
# @@protoc_insertion_point(module_scope)
+62
View File
@@ -0,0 +1,62 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import google.protobuf.descriptor
import google.protobuf.message
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import typing
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
@typing.final
class MeshBeacon(google.protobuf.message.Message):
"""
Payload for MESH_BEACON_APP packets.
Periodically broadcast by nodes in beacon mode.
Listeners deliver the text message to the local inbox and cache any offered
channel/preset for the client app to act on — the firmware never auto-applies them.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
MESSAGE_FIELD_NUMBER: builtins.int
OFFER_CHANNEL_FIELD_NUMBER: builtins.int
OFFER_REGION_FIELD_NUMBER: builtins.int
OFFER_PRESET_FIELD_NUMBER: builtins.int
message: builtins.str
"""
Human-readable beacon message. Max 100 bytes enforced by firmware on send.
"""
offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Optional region being advertised alongside offer_preset.
"""
offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Optional modem preset being advertised.
Combined with offer_region, tells a client "there is a mesh on this preset/region".
"""
@property
def offer_channel(self) -> meshtastic.protobuf.channel_pb2.ChannelSettings:
"""
Optional channel (name + PSK) being advertised to listening clients.
A client app may offer to switch the user to this channel; firmware never applies it automatically.
"""
def __init__(
self,
*,
message: builtins.str = ...,
offer_channel: meshtastic.protobuf.channel_pb2.ChannelSettings | None = ...,
offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_offer_preset", b"_offer_preset", "offer_channel", b"offer_channel", "offer_preset", b"offer_preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_offer_preset", b"_offer_preset", "message", b"message", "offer_channel", b"offer_channel", "offer_preset", b"offer_preset", "offer_region", b"offer_region"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_offer_preset", b"_offer_preset"]) -> typing.Literal["offer_preset"] | None: ...
global___MeshBeacon = MeshBeacon
+104 -86
View File
File diff suppressed because one or more lines are too long.
+384 -25
View File
@@ -142,13 +142,13 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
"""
RAK11310 (RP2040 + SX1262)
"""
SENSELORA_RP2040: _HardwareModel.ValueType # 27
MAKERFABS_TRACKER: _HardwareModel.ValueType # 27
"""
Makerfabs SenseLoRA Receiver (RP2040 + RFM96)
Makerfabs Tracker Reserved
"""
SENSELORA_S3: _HardwareModel.ValueType # 28
MAKERFABS_RESERVED: _HardwareModel.ValueType # 28
"""
Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96)
Makerfabs Reserved
"""
CANARYONE: _HardwareModel.ValueType # 29
"""
@@ -563,9 +563,9 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
"""
Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen.
"""
TRACKER_T1000_E_PRO: _HardwareModel.ValueType # 128
MESH_TRACKER_X1: _HardwareModel.ValueType # 128
"""
Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio,
Seeed studio Mesh Tracker X1card. NRF52840 w/ LR2021 radio,
GPS, button, buzzer, and sensors.
"""
THINKNODE_M7: _HardwareModel.ValueType # 129
@@ -610,6 +610,30 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
"""
Meshnology W10
"""
HELTEC_RC32: _HardwareModel.ValueType # 141
"""
Heltec ESP32S3 + SX1262
"""
HELTEC_RC52: _HardwareModel.ValueType # 142
"""
Heltec NRF52840 + SX1262
"""
HELTEC_RCC6: _HardwareModel.ValueType # 143
"""
Heltec ESP32C6 + SX1262
"""
SEEED_WIO_TRACKER_L1_PRO_1W: _HardwareModel.ValueType # 144
"""
Seeed Wio Tracker L1 Pro 1W, nRF52840 + SX1262 with 1 W external PA
"""
MESHNOLOGY_W12: _HardwareModel.ValueType # 145
"""
Meshnology W12
"""
MESHPAGER_X2: _HardwareModel.ValueType # 146
"""
Seeed Studio MeshPager X2
"""
PRIVATE_HW: _HardwareModel.ValueType # 255
"""
------------------------------------------------------------------------------------------------------------------------------------------
@@ -735,13 +759,13 @@ RAK11310: HardwareModel.ValueType # 26
"""
RAK11310 (RP2040 + SX1262)
"""
SENSELORA_RP2040: HardwareModel.ValueType # 27
MAKERFABS_TRACKER: HardwareModel.ValueType # 27
"""
Makerfabs SenseLoRA Receiver (RP2040 + RFM96)
Makerfabs Tracker Reserved
"""
SENSELORA_S3: HardwareModel.ValueType # 28
MAKERFABS_RESERVED: HardwareModel.ValueType # 28
"""
Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96)
Makerfabs Reserved
"""
CANARYONE: HardwareModel.ValueType # 29
"""
@@ -1156,9 +1180,9 @@ 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
MESH_TRACKER_X1: HardwareModel.ValueType # 128
"""
Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio,
Seeed studio Mesh Tracker X1card. NRF52840 w/ LR2021 radio,
GPS, button, buzzer, and sensors.
"""
THINKNODE_M7: HardwareModel.ValueType # 129
@@ -1203,6 +1227,30 @@ MESHNOLOGY_W10: HardwareModel.ValueType # 140
"""
Meshnology W10
"""
HELTEC_RC32: HardwareModel.ValueType # 141
"""
Heltec ESP32S3 + SX1262
"""
HELTEC_RC52: HardwareModel.ValueType # 142
"""
Heltec NRF52840 + SX1262
"""
HELTEC_RCC6: HardwareModel.ValueType # 143
"""
Heltec ESP32C6 + SX1262
"""
SEEED_WIO_TRACKER_L1_PRO_1W: HardwareModel.ValueType # 144
"""
Seeed Wio Tracker L1 Pro 1W, nRF52840 + SX1262 with 1 W external PA
"""
MESHNOLOGY_W12: HardwareModel.ValueType # 145
"""
Meshnology W12
"""
MESHPAGER_X2: HardwareModel.ValueType # 146
"""
Seeed Studio MeshPager X2
"""
PRIVATE_HW: HardwareModel.ValueType # 255
"""
------------------------------------------------------------------------------------------------------------------------------------------
@@ -1414,6 +1462,18 @@ class _FirmwareEditionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper
"""
Hamvention, the Dayton amateur radio convention
"""
FAB: _FirmwareEdition.ValueType # 20
"""
FAB, the international Fab Lab digital fabrication conference
"""
DRAGON_CON: _FirmwareEdition.ValueType # 21
"""
Dragon Con, the yearly pop culture convention in Atlanta, GA
"""
CCC: _FirmwareEdition.ValueType # 22
"""
Chaos Communication Congress, the hacker conference held yearly in Germany
"""
DIY_EDITION: _FirmwareEdition.ValueType # 127
"""
Placeholder for DIY and unofficial events
@@ -1449,6 +1509,18 @@ HAMVENTION: FirmwareEdition.ValueType # 19
"""
Hamvention, the Dayton amateur radio convention
"""
FAB: FirmwareEdition.ValueType # 20
"""
FAB, the international Fab Lab digital fabrication conference
"""
DRAGON_CON: FirmwareEdition.ValueType # 21
"""
Dragon Con, the yearly pop culture convention in Atlanta, GA
"""
CCC: FirmwareEdition.ValueType # 22
"""
Chaos Communication Congress, the hacker conference held yearly in Germany
"""
DIY_EDITION: FirmwareEdition.ValueType # 127
"""
Placeholder for DIY and unofficial events
@@ -1797,7 +1869,7 @@ class Position(google.protobuf.message.Message):
"""
ground_speed: builtins.int
"""
Ground speed in m/s and True North TRACK in 1/100 degrees
Ground speed in km/h and True North TRACK in 1/100 degrees
Clarification of terms:
- "track" is the direction of motion (measured in horizontal plane)
- "heading" is where the fuselage points (measured in horizontal plane)
@@ -1932,6 +2004,9 @@ class User(google.protobuf.message.Message):
long_name: builtins.str
"""
A full name for this user, i.e. "Kevin Hester"
Limited to 24 bytes of UTF-8: longer names are accepted from senders
built against the older 39-byte limit, but devices truncate them before
storing or rebroadcasting. Clients should enforce 24 bytes in their UI.
"""
short_name: builtins.str
"""
@@ -2263,6 +2338,7 @@ class Data(google.protobuf.message.Message):
REPLY_ID_FIELD_NUMBER: builtins.int
EMOJI_FIELD_NUMBER: builtins.int
BITFIELD_FIELD_NUMBER: builtins.int
XEDDSA_SIGNATURE_FIELD_NUMBER: builtins.int
portnum: meshtastic.protobuf.portnums_pb2.PortNum.ValueType
"""
Formerly named typ and of type Type
@@ -2309,6 +2385,10 @@ class Data(google.protobuf.message.Message):
"""
Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT.
"""
xeddsa_signature: builtins.bytes
"""
XEdDSA signature for the payload
"""
def __init__(
self,
*,
@@ -2321,9 +2401,10 @@ class Data(google.protobuf.message.Message):
reply_id: builtins.int = ...,
emoji: builtins.int = ...,
bitfield: builtins.int | None = ...,
xeddsa_signature: builtins.bytes = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_bitfield", b"_bitfield", "bitfield", b"bitfield"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_bitfield", b"_bitfield", "bitfield", b"bitfield", "dest", b"dest", "emoji", b"emoji", "payload", b"payload", "portnum", b"portnum", "reply_id", b"reply_id", "request_id", b"request_id", "source", b"source", "want_response", b"want_response"]) -> None: ...
def ClearField(self, field_name: typing.Literal["_bitfield", b"_bitfield", "bitfield", b"bitfield", "dest", b"dest", "emoji", b"emoji", "payload", b"payload", "portnum", b"portnum", "reply_id", b"reply_id", "request_id", b"request_id", "source", b"source", "want_response", b"want_response", "xeddsa_signature", b"xeddsa_signature"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_bitfield", b"_bitfield"]) -> typing.Literal["bitfield"] | None: ...
global___Data = Data
@@ -2620,6 +2701,51 @@ class RemoteShell(google.protobuf.message.Message):
global___RemoteShell = RemoteShell
@typing.final
class BoundingBox(google.protobuf.message.Message):
"""
A rectangular, axis-aligned geographic bounding box.
Used to define a rectangular geofence region for a Waypoint.
Fields are ordered west, south, east, north to match the standard bounding box
convention used by GeoJSON and PMTiles (min longitude, min latitude, max longitude, max latitude),
so the box can drive an offline map extract directly.
All coordinates are in degrees scaled by 1e-7 (same convention as Position and Waypoint).
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
LONGITUDE_WEST_I_FIELD_NUMBER: builtins.int
LATITUDE_SOUTH_I_FIELD_NUMBER: builtins.int
LONGITUDE_EAST_I_FIELD_NUMBER: builtins.int
LATITUDE_NORTH_I_FIELD_NUMBER: builtins.int
longitude_west_i: builtins.int
"""
Western edge of the box - minimum longitude (south-west corner)
"""
latitude_south_i: builtins.int
"""
Southern edge of the box - minimum latitude (south-west corner)
"""
longitude_east_i: builtins.int
"""
Eastern edge of the box - maximum longitude (north-east corner)
"""
latitude_north_i: builtins.int
"""
Northern edge of the box - maximum latitude (north-east corner)
"""
def __init__(
self,
*,
longitude_west_i: builtins.int = ...,
latitude_south_i: builtins.int = ...,
longitude_east_i: builtins.int = ...,
latitude_north_i: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["latitude_north_i", b"latitude_north_i", "latitude_south_i", b"latitude_south_i", "longitude_east_i", b"longitude_east_i", "longitude_west_i", b"longitude_west_i"]) -> None: ...
global___BoundingBox = BoundingBox
@typing.final
class Waypoint(google.protobuf.message.Message):
"""
@@ -2636,6 +2762,11 @@ class Waypoint(google.protobuf.message.Message):
NAME_FIELD_NUMBER: builtins.int
DESCRIPTION_FIELD_NUMBER: builtins.int
ICON_FIELD_NUMBER: builtins.int
GEOFENCE_RADIUS_FIELD_NUMBER: builtins.int
BOUNDING_BOX_FIELD_NUMBER: builtins.int
NOTIFY_ON_ENTER_FIELD_NUMBER: builtins.int
NOTIFY_ON_EXIT_FIELD_NUMBER: builtins.int
NOTIFY_FAVORITES_ONLY_FIELD_NUMBER: builtins.int
id: builtins.int
"""
Id of the waypoint
@@ -2669,6 +2800,36 @@ class Waypoint(google.protobuf.message.Message):
"""
Designator icon for the waypoint in the form of a unicode emoji
"""
geofence_radius: builtins.int
"""
If greater than zero, defines a circular geofence centred on this waypoint's
location (latitude_i / longitude_i) with this radius in meters.
Zero means the waypoint has no circular geofence.
"""
notify_on_enter: builtins.bool
"""
If true, a notification should be raised when a tracked node enters this
waypoint's geofence (the circular radius and/or the bounding box).
"""
notify_on_exit: builtins.bool
"""
If true, a notification should be raised when a tracked node exits this
waypoint's geofence (the circular radius and/or the bounding box).
"""
notify_favorites_only: builtins.bool
"""
If true, only raise geofence enter/exit notifications for nodes that are
marked as favorites on the receiving device. Applies to both notify_on_enter
and notify_on_exit. Favorite status is resolved locally per receiver, so the
same waypoint alerts each node only for its own favorites.
"""
@property
def bounding_box(self) -> global___BoundingBox:
"""
Optional rectangular geofence region for this waypoint.
May be used instead of, or in addition to, geofence_radius.
"""
def __init__(
self,
*,
@@ -2680,9 +2841,16 @@ class Waypoint(google.protobuf.message.Message):
name: builtins.str = ...,
description: builtins.str = ...,
icon: builtins.int = ...,
geofence_radius: builtins.int = ...,
bounding_box: global___BoundingBox | None = ...,
notify_on_enter: builtins.bool = ...,
notify_on_exit: builtins.bool = ...,
notify_favorites_only: builtins.bool = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_latitude_i", b"_latitude_i", "_longitude_i", b"_longitude_i", "latitude_i", b"latitude_i", "longitude_i", b"longitude_i"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_latitude_i", b"_latitude_i", "_longitude_i", b"_longitude_i", "description", b"description", "expire", b"expire", "icon", b"icon", "id", b"id", "latitude_i", b"latitude_i", "locked_to", b"locked_to", "longitude_i", b"longitude_i", "name", b"name"]) -> None: ...
def HasField(self, field_name: typing.Literal["_bounding_box", b"_bounding_box", "_latitude_i", b"_latitude_i", "_longitude_i", b"_longitude_i", "bounding_box", b"bounding_box", "latitude_i", b"latitude_i", "longitude_i", b"longitude_i"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_bounding_box", b"_bounding_box", "_latitude_i", b"_latitude_i", "_longitude_i", b"_longitude_i", "bounding_box", b"bounding_box", "description", b"description", "expire", b"expire", "geofence_radius", b"geofence_radius", "icon", b"icon", "id", b"id", "latitude_i", b"latitude_i", "locked_to", b"locked_to", "longitude_i", b"longitude_i", "name", b"name", "notify_favorites_only", b"notify_favorites_only", "notify_on_enter", b"notify_on_enter", "notify_on_exit", b"notify_on_exit"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_bounding_box", b"_bounding_box"]) -> typing.Literal["bounding_box"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_latitude_i", b"_latitude_i"]) -> typing.Literal["latitude_i"] | None: ...
@typing.overload
@@ -2954,6 +3122,10 @@ class MeshPacket(google.protobuf.message.Message):
"""
Arrived via API connection
"""
TRANSPORT_UNICAST_UDP: MeshPacket._TransportMechanism.ValueType # 8
"""
Arrived via Unicast UDP
"""
class TransportMechanism(_TransportMechanism, metaclass=_TransportMechanismEnumTypeWrapper):
"""
@@ -2992,6 +3164,10 @@ class MeshPacket(google.protobuf.message.Message):
"""
Arrived via API connection
"""
TRANSPORT_UNICAST_UDP: MeshPacket.TransportMechanism.ValueType # 8
"""
Arrived via Unicast UDP
"""
FROM_FIELD_NUMBER: builtins.int
TO_FIELD_NUMBER: builtins.int
@@ -3014,6 +3190,7 @@ class MeshPacket(google.protobuf.message.Message):
RELAY_NODE_FIELD_NUMBER: builtins.int
TX_AFTER_FIELD_NUMBER: builtins.int
TRANSPORT_MECHANISM_FIELD_NUMBER: builtins.int
XEDDSA_SIGNED_FIELD_NUMBER: builtins.int
to: builtins.int
"""
The (immediate) destination for this packet
@@ -3053,6 +3230,12 @@ class MeshPacket(google.protobuf.message.Message):
Note: this field is _never_ sent on the radio link itself (to save space) Times
are typically not sent over the mesh, but they will be added to any Packet
(chain of SubPacket) sent to the phone (so the phone can know exact time of reception)
Explicit presence: firmware cannot always attach a trustworthy wall-clock timestamp at the
moment of reception - a node with no GPS and no phone connected yet has no time source at
all. has_rx_time disambiguates that state from a genuine (if coincidental) 1970-01-01
reading. A packet delivered with this field absent may still be re-timestamped once a valid
clock becomes available, before the phone ever sees it - "absent" is not guaranteed
permanent, only "not yet known at last observation".
"""
rx_snr: builtins.float
"""
@@ -3087,6 +3270,9 @@ class MeshPacket(google.protobuf.message.Message):
rx_rssi: builtins.int
"""
rssi of received packet. Only sent to phone for dispay purposes.
Explicit presence: rssi 0 is a legitimate reading on some radios (SX126x can report exactly
0 dBm; SX127x's formula can even go positive). has_rx_rssi disambiguates; a replayed packet
built from history should leave this field absent rather than emitting 0.
"""
delayed: global___MeshPacket.Delayed.ValueType
"""
@@ -3100,6 +3286,10 @@ class MeshPacket(google.protobuf.message.Message):
"""
Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header.
When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled.
hop_start == 0 does not necessarily mean a direct (0-hop) neighbor: firmware prior to 2.3.0
never populated this field, so a receiver can only trust hop_start == 0 as genuine once it has
decoded the packet and confirmed the sender's bitfield is present (added in 2.5.0). Until then,
or for a sender that never sets that bitfield, treat hop_start == 0 as unknown, not direct.
"""
public_key: builtins.bytes
"""
@@ -3129,6 +3319,10 @@ class MeshPacket(google.protobuf.message.Message):
"""
Indicates which transport mechanism this packet arrived over
"""
xeddsa_signed: builtins.bool
"""
Indicates whether the packet has a valid signature
"""
@property
def decoded(self) -> global___Data:
"""
@@ -3143,12 +3337,12 @@ class MeshPacket(google.protobuf.message.Message):
decoded: global___Data | None = ...,
encrypted: builtins.bytes = ...,
id: builtins.int = ...,
rx_time: builtins.int = ...,
rx_time: builtins.int | None = ...,
rx_snr: builtins.float = ...,
hop_limit: builtins.int = ...,
want_ack: builtins.bool = ...,
priority: global___MeshPacket.Priority.ValueType = ...,
rx_rssi: builtins.int = ...,
rx_rssi: builtins.int | None = ...,
delayed: global___MeshPacket.Delayed.ValueType = ...,
via_mqtt: builtins.bool = ...,
hop_start: builtins.int = ...,
@@ -3158,9 +3352,15 @@ class MeshPacket(google.protobuf.message.Message):
relay_node: builtins.int = ...,
tx_after: builtins.int = ...,
transport_mechanism: global___MeshPacket.TransportMechanism.ValueType = ...,
xeddsa_signed: builtins.bool = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["decoded", b"decoded", "encrypted", b"encrypted", "payload_variant", b"payload_variant"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["channel", b"channel", "decoded", b"decoded", "delayed", b"delayed", "encrypted", b"encrypted", "from", b"from", "hop_limit", b"hop_limit", "hop_start", b"hop_start", "id", b"id", "next_hop", b"next_hop", "payload_variant", b"payload_variant", "pki_encrypted", b"pki_encrypted", "priority", b"priority", "public_key", b"public_key", "relay_node", b"relay_node", "rx_rssi", b"rx_rssi", "rx_snr", b"rx_snr", "rx_time", b"rx_time", "to", b"to", "transport_mechanism", b"transport_mechanism", "tx_after", b"tx_after", "via_mqtt", b"via_mqtt", "want_ack", b"want_ack"]) -> None: ...
def HasField(self, field_name: typing.Literal["_rx_rssi", b"_rx_rssi", "_rx_time", b"_rx_time", "decoded", b"decoded", "encrypted", b"encrypted", "payload_variant", b"payload_variant", "rx_rssi", b"rx_rssi", "rx_time", b"rx_time"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_rx_rssi", b"_rx_rssi", "_rx_time", b"_rx_time", "channel", b"channel", "decoded", b"decoded", "delayed", b"delayed", "encrypted", b"encrypted", "from", b"from", "hop_limit", b"hop_limit", "hop_start", b"hop_start", "id", b"id", "next_hop", b"next_hop", "payload_variant", b"payload_variant", "pki_encrypted", b"pki_encrypted", "priority", b"priority", "public_key", b"public_key", "relay_node", b"relay_node", "rx_rssi", b"rx_rssi", "rx_snr", b"rx_snr", "rx_time", b"rx_time", "to", b"to", "transport_mechanism", b"transport_mechanism", "tx_after", b"tx_after", "via_mqtt", b"via_mqtt", "want_ack", b"want_ack", "xeddsa_signed", b"xeddsa_signed"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_rx_rssi", b"_rx_rssi"]) -> typing.Literal["rx_rssi"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_rx_time", b"_rx_time"]) -> typing.Literal["rx_time"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["decoded", "encrypted"] | None: ...
global___MeshPacket = MeshPacket
@@ -3201,6 +3401,7 @@ class NodeInfo(google.protobuf.message.Message):
IS_IGNORED_FIELD_NUMBER: builtins.int
IS_KEY_MANUALLY_VERIFIED_FIELD_NUMBER: builtins.int
IS_MUTED_FIELD_NUMBER: builtins.int
HAS_XEDDSA_SIGNED_FIELD_NUMBER: builtins.int
num: builtins.int
"""
The node number
@@ -3253,6 +3454,12 @@ class NodeInfo(google.protobuf.message.Message):
True if node has been muted
Persistes between NodeDB internal clean ups
"""
has_xeddsa_signed: builtins.bool
"""
True if node is signing its packets via XEdDSA
Persists between NodeDB internal clean ups
LSB 1 of the bitfield
"""
@property
def user(self) -> global___User:
"""
@@ -3288,9 +3495,10 @@ class NodeInfo(google.protobuf.message.Message):
is_ignored: builtins.bool = ...,
is_key_manually_verified: builtins.bool = ...,
is_muted: builtins.bool = ...,
has_xeddsa_signed: 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", "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 ClearField(self, field_name: typing.Literal["_hops_away", b"_hops_away", "channel", b"channel", "device_metrics", b"device_metrics", "has_xeddsa_signed", b"has_xeddsa_signed", "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
@@ -3529,6 +3737,7 @@ class FromRadio(google.protobuf.message.Message):
CLIENTNOTIFICATION_FIELD_NUMBER: builtins.int
DEVICEUICONFIG_FIELD_NUMBER: builtins.int
LOCKDOWN_STATUS_FIELD_NUMBER: builtins.int
REGION_PRESETS_FIELD_NUMBER: builtins.int
id: builtins.int
"""
The packet id, used to allow the phone to request missing read packets from the FIFO,
@@ -3644,6 +3853,16 @@ class FromRadio(google.protobuf.message.Message):
encoding state as magic-string prefixes inside ClientNotification.
"""
@property
def region_presets(self) -> global___LoRaRegionPresetMap:
"""
Map of which modem presets are legal in each LoRa region. Sent once
during the want_config handshake (right after `metadata`, before the
first `channel`) so client UIs can prevent the user from selecting an
illegal region+preset combination. A region that does not appear in
any group carries no constraint info and should not be restricted.
"""
def __init__(
self,
*,
@@ -3665,10 +3884,11 @@ class FromRadio(google.protobuf.message.Message):
clientNotification: global___ClientNotification | None = ...,
deviceuiConfig: meshtastic.protobuf.device_ui_pb2.DeviceUIConfig | None = ...,
lockdown_status: global___LockdownStatus | None = ...,
region_presets: global___LoRaRegionPresetMap | 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", "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: ...
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", "region_presets", b"region_presets", "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", "region_presets", b"region_presets", "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", "region_presets"] | None: ...
global___FromRadio = FromRadio
@@ -3713,6 +3933,15 @@ class LockdownStatus(google.protobuf.message.Message):
"""
Passphrase rejected. backoff_seconds is non-zero when rate-limited.
"""
DISABLED: LockdownStatus._State.ValueType # 5
"""
Lockdown is supported by this firmware but not currently active
(no passphrase has been provisioned, or it was disabled via
AdminMessage.lockdown_auth.disable). The device is operating in
normal, non-encrypted mode. Clients render the lockdown-mode
toggle as OFF on receiving this. Distinct from NEEDS_PROVISION,
which is only used during an in-progress enable flow.
"""
class State(_State, metaclass=_StateEnumTypeWrapper): ...
STATE_UNSPECIFIED: LockdownStatus.State.ValueType # 0
@@ -3739,6 +3968,15 @@ class LockdownStatus(google.protobuf.message.Message):
"""
Passphrase rejected. backoff_seconds is non-zero when rate-limited.
"""
DISABLED: LockdownStatus.State.ValueType # 5
"""
Lockdown is supported by this firmware but not currently active
(no passphrase has been provisioned, or it was disabled via
AdminMessage.lockdown_auth.disable). The device is operating in
normal, non-encrypted mode. Clients render the lockdown-mode
toggle as OFF on receiving this. Distinct from NEEDS_PROVISION,
which is only used during an in-progress enable flow.
"""
STATE_FIELD_NUMBER: builtins.int
LOCK_REASON_FIELD_NUMBER: builtins.int
@@ -4174,6 +4412,7 @@ class DeviceMetadata(google.protobuf.message.Message):
HASREMOTEHARDWARE_FIELD_NUMBER: builtins.int
HASPKC_FIELD_NUMBER: builtins.int
EXCLUDED_MODULES_FIELD_NUMBER: builtins.int
HAS_XEDDSA_FIELD_NUMBER: builtins.int
firmware_version: builtins.str
"""
Device firmware version string
@@ -4223,6 +4462,11 @@ class DeviceMetadata(google.protobuf.message.Message):
Bit field of boolean for excluded modules
(bitwise OR of ExcludedModules)
"""
has_xeddsa: builtins.bool
"""
Indicates whether this firmware build includes XEdDSA packet signature verification.
This is a read-only capability and must be false when XEdDSA is not compiled in.
"""
def __init__(
self,
*,
@@ -4238,11 +4482,126 @@ class DeviceMetadata(google.protobuf.message.Message):
hasRemoteHardware: builtins.bool = ...,
hasPKC: builtins.bool = ...,
excluded_modules: builtins.int = ...,
has_xeddsa: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["canShutdown", b"canShutdown", "device_state_version", b"device_state_version", "excluded_modules", b"excluded_modules", "firmware_version", b"firmware_version", "hasBluetooth", b"hasBluetooth", "hasEthernet", b"hasEthernet", "hasPKC", b"hasPKC", "hasRemoteHardware", b"hasRemoteHardware", "hasWifi", b"hasWifi", "hw_model", b"hw_model", "position_flags", b"position_flags", "role", b"role"]) -> None: ...
def ClearField(self, field_name: typing.Literal["canShutdown", b"canShutdown", "device_state_version", b"device_state_version", "excluded_modules", b"excluded_modules", "firmware_version", b"firmware_version", "hasBluetooth", b"hasBluetooth", "hasEthernet", b"hasEthernet", "hasPKC", b"hasPKC", "hasRemoteHardware", b"hasRemoteHardware", "hasWifi", b"hasWifi", "has_xeddsa", b"has_xeddsa", "hw_model", b"hw_model", "position_flags", b"position_flags", "role", b"role"]) -> None: ...
global___DeviceMetadata = DeviceMetadata
@typing.final
class LoRaPresetGroup(google.protobuf.message.Message):
"""
A distinct set of legal modem presets shared by one or more LoRa regions.
Regions that have an identical preset list / default / licensing reference
the same group (by index) via LoRaRegionPresetMap.region_groups. This keeps
the whole map small enough to fit in a single FromRadio packet, since most
regions share the one standard preset list.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PRESETS_FIELD_NUMBER: builtins.int
DEFAULT_PRESET_FIELD_NUMBER: builtins.int
LICENSED_ONLY_FIELD_NUMBER: builtins.int
default_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
The firmware's default modem preset for regions in this group.
Always one of `presets`. Clients should select this when switching to one
of these regions, or when the current preset is not legal in the new region.
"""
licensed_only: builtins.bool
"""
True if regions referencing this group are for licensed operators only
(e.g. amateur / ham radio bands). Clients should warn or gate accordingly.
"""
@property
def presets(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType]:
"""
The modem presets that are legal for every region referencing this group.
"""
def __init__(
self,
*,
presets: collections.abc.Iterable[meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType] | None = ...,
default_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType = ...,
licensed_only: builtins.bool = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["default_preset", b"default_preset", "licensed_only", b"licensed_only", "presets", b"presets"]) -> None: ...
global___LoRaPresetGroup = LoRaPresetGroup
@typing.final
class LoRaRegionPresets(google.protobuf.message.Message):
"""
Associates a single LoRa region with its preset group.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
REGION_FIELD_NUMBER: builtins.int
GROUP_INDEX_FIELD_NUMBER: builtins.int
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
The LoRa region this entry describes.
"""
group_index: builtins.int
"""
Index into LoRaRegionPresetMap.groups for the preset list that is legal
in `region`.
"""
def __init__(
self,
*,
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
group_index: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["group_index", b"group_index", "region", b"region"]) -> None: ...
global___LoRaRegionPresets = LoRaRegionPresets
@typing.final
class LoRaRegionPresetMap(google.protobuf.message.Message):
"""
Map describing which modem presets are valid for each LoRa region. Sent by
the firmware during the want_config handshake (as FromRadio.region_presets)
so that client UIs can prevent illegal region+preset selections.
Delivery is grouped to save space: `groups` holds each distinct preset list,
and `region_groups` maps every known region to one of those groups by index.
A region that does NOT appear in `region_groups` carries no constraint
information and should not be restricted by the client (e.g. firmware that
predates this message, or a region with no firmware table entry). Clients
must also tolerate this whole message being absent.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
GROUPS_FIELD_NUMBER: builtins.int
REGION_GROUPS_FIELD_NUMBER: builtins.int
@property
def groups(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LoRaPresetGroup]:
"""
One entry per distinct (preset-list, default, licensing) combination.
Referenced by index from `region_groups`.
"""
@property
def region_groups(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___LoRaRegionPresets]:
"""
One entry per known LoRa region, pointing at its preset group.
"""
def __init__(
self,
*,
groups: collections.abc.Iterable[global___LoRaPresetGroup] | None = ...,
region_groups: collections.abc.Iterable[global___LoRaRegionPresets] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["groups", b"groups", "region_groups", b"region_groups"]) -> None: ...
global___LoRaRegionPresetMap = LoRaRegionPresetMap
@typing.final
class Heartbeat(google.protobuf.message.Message):
"""
File diff suppressed because one or more lines are too long.
+193 -63
View File
@@ -10,6 +10,8 @@ import google.protobuf.internal.containers
import google.protobuf.internal.enum_type_wrapper
import google.protobuf.message
import meshtastic.protobuf.atak_pb2
import meshtastic.protobuf.channel_pb2
import meshtastic.protobuf.config_pb2
import sys
import typing
@@ -502,95 +504,48 @@ class ModuleConfig(google.protobuf.message.Message):
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
Minimum interval in seconds between position updates from the same node.
A non-zero value implicitly enables the suppression window; 0 disables it.
"""
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
Maximum hop distance from the requestor at which direct NodeInfo responses
are served from the local cache. A non-zero value implicitly enables direct
response; 0 disables it.
"""
rate_limit_window_secs: builtins.int
"""
Time window in seconds for rate limiting calculations
Time window in seconds for per-node rate limiting.
A non-zero value implicitly enables rate limiting; 0 disables it.
"""
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
Maximum packets allowed per node within the rate limit window.
A non-zero value implicitly enables rate limiting; 0 disables it.
"""
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
Maximum unknown/undecryptable packets per rate window before the source
is dropped. A non-zero value implicitly enables unknown-packet filtering;
0 disables it.
"""
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: ...
def ClearField(self, field_name: typing.Literal["nodeinfo_direct_response_max_hops", b"nodeinfo_direct_response_max_hops", "position_min_interval_secs", b"position_min_interval_secs", "rate_limit_max_packets", b"rate_limit_max_packets", "rate_limit_window_secs", b"rate_limit_window_secs", "unknown_packet_threshold", b"unknown_packet_threshold"]) -> None: ...
@typing.final
class SerialConfig(google.protobuf.message.Message):
@@ -1302,6 +1257,173 @@ class ModuleConfig(google.protobuf.message.Message):
) -> None: ...
def ClearField(self, field_name: typing.Literal["node_status", b"node_status"]) -> None: ...
@typing.final
class MeshBeaconConfig(google.protobuf.message.Message):
"""
MeshBeacon module config
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Flags:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _FlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ModuleConfig.MeshBeaconConfig._Flags.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
FLAG_NONE: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 0
"""
No options enabled.
"""
FLAG_LISTEN_ENABLED: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 1
"""
Enable receiving MESH_BEACON_APP packets from other nodes.
The text portion is delivered to the local message inbox.
Offered channel/preset are stored for the client app to act on.
"""
FLAG_BROADCAST_ENABLED: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 2
"""
Enable periodically broadcasting MESH_BEACON_APP packets from this node.
"""
FLAG_LEGACY_SPLIT: ModuleConfig.MeshBeaconConfig._Flags.ValueType # 4
"""
When both text and offer content are present, split the beacon into a separate
MESH_BEACON_APP (offer only) and TEXT_MESSAGE_APP (text only) packet, so firmware
that only decodes TEXT_MESSAGE_APP still receives the human-readable text.
"""
class Flags(_Flags, metaclass=_FlagsEnumTypeWrapper):
"""
Boolean options for the beacon module, packed into the `flags` bitfield below.
OR the FLAG_* values together; a flag is on when its bit is set.
"""
FLAG_NONE: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 0
"""
No options enabled.
"""
FLAG_LISTEN_ENABLED: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 1
"""
Enable receiving MESH_BEACON_APP packets from other nodes.
The text portion is delivered to the local message inbox.
Offered channel/preset are stored for the client app to act on.
"""
FLAG_BROADCAST_ENABLED: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 2
"""
Enable periodically broadcasting MESH_BEACON_APP packets from this node.
"""
FLAG_LEGACY_SPLIT: ModuleConfig.MeshBeaconConfig.Flags.ValueType # 4
"""
When both text and offer content are present, split the beacon into a separate
MESH_BEACON_APP (offer only) and TEXT_MESSAGE_APP (text only) packet, so firmware
that only decodes TEXT_MESSAGE_APP still receives the human-readable text.
"""
@typing.final
class BroadcastTarget(google.protobuf.message.Message):
"""
One entry in the broadcast destination list.
Each entry names one set of radio settings to send a beacon copy on.
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
PRESET_FIELD_NUMBER: builtins.int
REGION_FIELD_NUMBER: builtins.int
CHANNEL_INDEX_FIELD_NUMBER: builtins.int
preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Modem preset to use for this target.
Falls back to the running config preset if unset.
"""
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Region to use for this target. UNSET means use the running config region.
"""
channel_index: builtins.int
"""Tag 3 was an embedded ChannelSettings; replaced by channel_index (tag 4) to keep
ModuleConfig within the BLE FromRadio size budget. Branch unreleased, so tag 3 is a gap.
Index into the device's channel table (0..MAX_NUM_CHANNELS-1) of the channel to
transmit this target's beacon on. The referenced channel must already be configured
on the node (its key is needed to encrypt). If unset, the default channel for the
preset is used.
"""
def __init__(
self,
*,
preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
channel_index: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_channel_index", b"_channel_index", "_preset", b"_preset", "channel_index", b"channel_index", "preset", b"preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_channel_index", b"_channel_index", "_preset", b"_preset", "channel_index", b"channel_index", "preset", b"preset", "region", b"region"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_channel_index", b"_channel_index"]) -> typing.Literal["channel_index"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_preset", b"_preset"]) -> typing.Literal["preset"] | None: ...
FLAGS_FIELD_NUMBER: builtins.int
BROADCAST_MESSAGE_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_CHANNEL_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_REGION_FIELD_NUMBER: builtins.int
BROADCAST_OFFER_PRESET_FIELD_NUMBER: builtins.int
BROADCAST_INTERVAL_SECS_FIELD_NUMBER: builtins.int
BROADCAST_TARGETS_FIELD_NUMBER: builtins.int
flags: builtins.int
"""
Bitwise-OR of Flags values (listen / broadcast / legacy-split toggles).
"""
broadcast_message: builtins.str
"""
Message to include in each beacon broadcast. Max 100 bytes enforced by firmware.
"""
broadcast_offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType
"""
Optional region to advertise in the MeshBeacon offer_region field.
"""
broadcast_offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType
"""
Optional modem preset to advertise in the MeshBeacon offer_preset field.
"""
broadcast_interval_secs: builtins.int
"""
How often to broadcast, in seconds. Min 3600 (1 h), default 3600.
"""
@property
def broadcast_offer_channel(self) -> meshtastic.protobuf.channel_pb2.ChannelSettings:
"""
Optional channel (name + PSK) to advertise in the MeshBeacon offer_channel field.
"""
@property
def broadcast_targets(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ModuleConfig.MeshBeaconConfig.BroadcastTarget]:
"""
Broadcast destination list.
The broadcaster sends one beacon copy per distinct destination, in sequence, temporarily
switching the radio to that entry's preset/region/channel for each.
When empty, a single beacon is sent on the node's running preset and region over the
primary channel.
Entries that resolve to the same effective preset, region and channel are deduplicated, so
a duplicate entry does not produce a second transmission.
"""
def __init__(
self,
*,
flags: builtins.int = ...,
broadcast_message: builtins.str = ...,
broadcast_offer_channel: meshtastic.protobuf.channel_pb2.ChannelSettings | None = ...,
broadcast_offer_region: meshtastic.protobuf.config_pb2.Config.LoRaConfig.RegionCode.ValueType = ...,
broadcast_offer_preset: meshtastic.protobuf.config_pb2.Config.LoRaConfig.ModemPreset.ValueType | None = ...,
broadcast_interval_secs: builtins.int = ...,
broadcast_targets: collections.abc.Iterable[global___ModuleConfig.MeshBeaconConfig.BroadcastTarget] | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset", "broadcast_offer_channel", b"broadcast_offer_channel", "broadcast_offer_preset", b"broadcast_offer_preset"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset", "broadcast_interval_secs", b"broadcast_interval_secs", "broadcast_message", b"broadcast_message", "broadcast_offer_channel", b"broadcast_offer_channel", "broadcast_offer_preset", b"broadcast_offer_preset", "broadcast_offer_region", b"broadcast_offer_region", "broadcast_targets", b"broadcast_targets", "flags", b"flags"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["_broadcast_offer_preset", b"_broadcast_offer_preset"]) -> typing.Literal["broadcast_offer_preset"] | None: ...
@typing.final
class TAKConfig(google.protobuf.message.Message):
"""
@@ -1346,6 +1468,7 @@ class ModuleConfig(google.protobuf.message.Message):
STATUSMESSAGE_FIELD_NUMBER: builtins.int
TRAFFIC_MANAGEMENT_FIELD_NUMBER: builtins.int
TAK_FIELD_NUMBER: builtins.int
MESH_BEACON_FIELD_NUMBER: builtins.int
@property
def mqtt(self) -> global___ModuleConfig.MQTTConfig:
"""
@@ -1442,6 +1565,12 @@ class ModuleConfig(google.protobuf.message.Message):
TAK team/role configuration for TAK_TRACKER
"""
@property
def mesh_beacon(self) -> global___ModuleConfig.MeshBeaconConfig:
"""
MeshBeacon module config
"""
def __init__(
self,
*,
@@ -1461,10 +1590,11 @@ class ModuleConfig(google.protobuf.message.Message):
statusmessage: global___ModuleConfig.StatusMessageConfig | None = ...,
traffic_management: global___ModuleConfig.TrafficManagementConfig | None = ...,
tak: global___ModuleConfig.TAKConfig | None = ...,
mesh_beacon: global___ModuleConfig.MeshBeaconConfig | 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", "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: ...
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", "mesh_beacon", b"mesh_beacon", "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", "mesh_beacon", b"mesh_beacon", "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", "mesh_beacon"] | None: ...
global___ModuleConfig = ModuleConfig
+11 -5
View File
@@ -16,7 +16,7 @@ from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb
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\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')
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\"\x7f\n\x0fServiceEnvelope\x12\x36\n\x06packet\x18\x01 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacketB\x05\x92?\x02\x18\x04\x12\x19\n\nchannel_id\x18\x02 \x01(\tB\x05\x92?\x02\x18\x04\x12\x19\n\ngateway_id\x18\x03 \x01(\tB\x05\x92?\x02\x18\x04\"\x9f\x04\n\tMapReport\x12\x18\n\tlong_name\x18\x01 \x01(\tB\x05\x92?\x02\x08\x19\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)
@@ -24,8 +24,14 @@ _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'
_SERVICEENVELOPE.fields_by_name['packet']._options = None
_SERVICEENVELOPE.fields_by_name['packet']._serialized_options = b'\222?\002\030\004'
_SERVICEENVELOPE.fields_by_name['channel_id']._options = None
_SERVICEENVELOPE.fields_by_name['channel_id']._serialized_options = b'\222?\002\030\004'
_SERVICEENVELOPE.fields_by_name['gateway_id']._options = None
_SERVICEENVELOPE.fields_by_name['gateway_id']._serialized_options = b'\222?\002\030\004'
_MAPREPORT.fields_by_name['long_name']._options = None
_MAPREPORT.fields_by_name['long_name']._serialized_options = b'\222?\002\010('
_MAPREPORT.fields_by_name['long_name']._serialized_options = b'\222?\002\010\031'
_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
@@ -33,7 +39,7 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_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
_globals['_SERVICEENVELOPE']._serialized_end=282
_globals['_MAPREPORT']._serialized_start=285
_globals['_MAPREPORT']._serialized_end=828
# @@protoc_insertion_point(module_scope)
+2 -2
View File
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
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')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xa4\x06\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\x13\n\x0fMESH_BEACON_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\x10\n\x0cLORA_OTA_APP\x10O\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=825
_globals['_PORTNUM']._serialized_end=864
# @@protoc_insertion_point(module_scope)
+24
View File
@@ -135,6 +135,13 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
This module allows setting an extra string of status for a node.
Broadcasts on change and on a timer, possibly once a day.
"""
MESH_BEACON_APP: _PortNum.ValueType # 37
"""
Beacon module broadcast packets.
ENCODING: protobuf
Periodically broadcast by nodes in beacon mode; received by nodes with MeshBeaconConfig.FLAG_LISTEN_ENABLED.
Carries a text message plus optional channel/preset offers for client apps.
"""
SERIAL_APP: _PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -223,6 +230,11 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
Portnum for payloads from the official Meshtastic ATAK plugin using
TAKPacketV2 with zstd dictionary compression.
"""
LORA_OTA_APP: _PortNum.ValueType # 79
"""signed firmware updates over lora.
ENCODING: binary (ota-common transport frames)
"""
GROUPALARM_APP: _PortNum.ValueType # 112
"""
GroupAlarm integration
@@ -374,6 +386,13 @@ 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.
"""
MESH_BEACON_APP: PortNum.ValueType # 37
"""
Beacon module broadcast packets.
ENCODING: protobuf
Periodically broadcast by nodes in beacon mode; received by nodes with MeshBeaconConfig.FLAG_LISTEN_ENABLED.
Carries a text message plus optional channel/preset offers for client apps.
"""
SERIAL_APP: PortNum.ValueType # 64
"""
Provides a hardware serial interface to send and receive from the Meshtastic network.
@@ -462,6 +481,11 @@ ATAK Plugin V2
Portnum for payloads from the official Meshtastic ATAK plugin using
TAKPacketV2 with zstd dictionary compression.
"""
LORA_OTA_APP: PortNum.ValueType # 79
"""signed firmware updates over lora.
ENCODING: binary (ota-common transport frames)
"""
GROUPALARM_APP: PortNum.ValueType # 112
"""
GroupAlarm integration
+55 -23
View File
File diff suppressed because one or more lines are too long.
+285 -16
View File
@@ -241,6 +241,22 @@ class _TelemetrySensorTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wra
"""
ICM-42607-P 6Axis IMU
"""
SPA06: _TelemetrySensorType.ValueType # 54
"""
SPA06 pressure and temperature
"""
HM330X: _TelemetrySensorType.ValueType # 55
"""
HM330X PM SENSOR
"""
SEN6X: _TelemetrySensorType.ValueType # 56
"""
Sensirion SEN6X PM/RHT/VOC/NOx/CO2/HCHO sensor family (SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C)
"""
AS3935: _TelemetrySensorType.ValueType # 57
"""
AS3935 Franklin lightning sensor
"""
class TelemetrySensorType(_TelemetrySensorType, metaclass=_TelemetrySensorTypeEnumTypeWrapper):
"""
@@ -463,6 +479,22 @@ ICM42607P: TelemetrySensorType.ValueType # 53
"""
ICM-42607-P 6Axis IMU
"""
SPA06: TelemetrySensorType.ValueType # 54
"""
SPA06 pressure and temperature
"""
HM330X: TelemetrySensorType.ValueType # 55
"""
HM330X PM SENSOR
"""
SEN6X: TelemetrySensorType.ValueType # 56
"""
Sensirion SEN6X PM/RHT/VOC/NOx/CO2/HCHO sensor family (SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C)
"""
AS3935: TelemetrySensorType.ValueType # 57
"""
AS3935 Franklin lightning sensor
"""
global___TelemetrySensorType = TelemetrySensorType
@typing.final
@@ -553,6 +585,24 @@ class EnvironmentMetrics(google.protobuf.message.Message):
SOIL_MOISTURE_FIELD_NUMBER: builtins.int
SOIL_TEMPERATURE_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH0_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH1_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH2_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH3_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH4_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH5_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH6_FIELD_NUMBER: builtins.int
ADC_VOLTAGE_CH7_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH0_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH1_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH2_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH3_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH4_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH5_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH6_FIELD_NUMBER: builtins.int
ONE_WIRE_TEMPERATURE_CH7_FIELD_NUMBER: builtins.int
LIGHTNING_STRIKE_COUNT_1H_FIELD_NUMBER: builtins.int
LIGHTNING_DISTANCE_KM_FIELD_NUMBER: builtins.int
temperature: builtins.float
"""
Temperature measured
@@ -643,10 +693,82 @@ class EnvironmentMetrics(google.protobuf.message.Message):
"""
Soil temperature measured (*C)
"""
adc_voltage_ch0: builtins.float
"""
Multi-channel ADC Voltage Channel 0 (V)
"""
adc_voltage_ch1: builtins.float
"""
Multi-channel ADC Voltage Channel 1 (V)
"""
adc_voltage_ch2: builtins.float
"""
Multi-channel ADC Voltage Channel 2 (V)
"""
adc_voltage_ch3: builtins.float
"""
Multi-channel ADC Voltage Channel 3 (V)
"""
adc_voltage_ch4: builtins.float
"""
Multi-channel ADC Voltage Channel 4 (V)
"""
adc_voltage_ch5: builtins.float
"""
Multi-channel ADC Voltage Channel 5 (V)
"""
adc_voltage_ch6: builtins.float
"""
Multi-channel ADC Voltage Channel 6 (V)
"""
adc_voltage_ch7: builtins.float
"""
Multi-channel ADC Voltage Channel 7 (V)
"""
one_wire_temperature_ch0: builtins.float
"""
Multi-channel One-Wire Temperature Channel 0 (*C)
"""
one_wire_temperature_ch1: builtins.float
"""
Multi-channel One-Wire Temperature Channel 1 (*C)
"""
one_wire_temperature_ch2: builtins.float
"""
Multi-channel One-Wire Temperature Channel 2 (*C)
"""
one_wire_temperature_ch3: builtins.float
"""
Multi-channel One-Wire Temperature Channel 3 (*C)
"""
one_wire_temperature_ch4: builtins.float
"""
Multi-channel One-Wire Temperature Channel 4 (*C)
"""
one_wire_temperature_ch5: builtins.float
"""
Multi-channel One-Wire Temperature Channel 5 (*C)
"""
one_wire_temperature_ch6: builtins.float
"""
Multi-channel One-Wire Temperature Channel 6 (*C)
"""
one_wire_temperature_ch7: builtins.float
"""
Multi-channel One-Wire Temperature Channel 7 (*C)
"""
lightning_strike_count_1h: builtins.int
"""
Lightning strikes detected in the last hour
"""
lightning_distance_km: builtins.float
"""
Estimated distance to the leading edge of the storm, in km
"""
@property
def one_wire_temperature(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.float]:
"""
One-wire temperature (*C)
Never implemented, but Voltage may be mis-interpreted by old clients as temperature
"""
def __init__(
@@ -675,9 +797,43 @@ class EnvironmentMetrics(google.protobuf.message.Message):
soil_moisture: builtins.int | None = ...,
soil_temperature: builtins.float | None = ...,
one_wire_temperature: collections.abc.Iterable[builtins.float] | None = ...,
adc_voltage_ch0: builtins.float | None = ...,
adc_voltage_ch1: builtins.float | None = ...,
adc_voltage_ch2: builtins.float | None = ...,
adc_voltage_ch3: builtins.float | None = ...,
adc_voltage_ch4: builtins.float | None = ...,
adc_voltage_ch5: builtins.float | None = ...,
adc_voltage_ch6: builtins.float | None = ...,
adc_voltage_ch7: builtins.float | None = ...,
one_wire_temperature_ch0: builtins.float | None = ...,
one_wire_temperature_ch1: builtins.float | None = ...,
one_wire_temperature_ch2: builtins.float | None = ...,
one_wire_temperature_ch3: builtins.float | None = ...,
one_wire_temperature_ch4: builtins.float | None = ...,
one_wire_temperature_ch5: builtins.float | None = ...,
one_wire_temperature_ch6: builtins.float | None = ...,
one_wire_temperature_ch7: builtins.float | None = ...,
lightning_strike_count_1h: builtins.int | None = ...,
lightning_distance_km: 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", "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: ...
def HasField(self, field_name: typing.Literal["_adc_voltage_ch0", b"_adc_voltage_ch0", "_adc_voltage_ch1", b"_adc_voltage_ch1", "_adc_voltage_ch2", b"_adc_voltage_ch2", "_adc_voltage_ch3", b"_adc_voltage_ch3", "_adc_voltage_ch4", b"_adc_voltage_ch4", "_adc_voltage_ch5", b"_adc_voltage_ch5", "_adc_voltage_ch6", b"_adc_voltage_ch6", "_adc_voltage_ch7", b"_adc_voltage_ch7", "_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lightning_distance_km", b"_lightning_distance_km", "_lightning_strike_count_1h", b"_lightning_strike_count_1h", "_lux", b"_lux", "_one_wire_temperature_ch0", b"_one_wire_temperature_ch0", "_one_wire_temperature_ch1", b"_one_wire_temperature_ch1", "_one_wire_temperature_ch2", b"_one_wire_temperature_ch2", "_one_wire_temperature_ch3", b"_one_wire_temperature_ch3", "_one_wire_temperature_ch4", b"_one_wire_temperature_ch4", "_one_wire_temperature_ch5", b"_one_wire_temperature_ch5", "_one_wire_temperature_ch6", b"_one_wire_temperature_ch6", "_one_wire_temperature_ch7", b"_one_wire_temperature_ch7", "_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", "adc_voltage_ch0", b"adc_voltage_ch0", "adc_voltage_ch1", b"adc_voltage_ch1", "adc_voltage_ch2", b"adc_voltage_ch2", "adc_voltage_ch3", b"adc_voltage_ch3", "adc_voltage_ch4", b"adc_voltage_ch4", "adc_voltage_ch5", b"adc_voltage_ch5", "adc_voltage_ch6", b"adc_voltage_ch6", "adc_voltage_ch7", b"adc_voltage_ch7", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lightning_distance_km", b"lightning_distance_km", "lightning_strike_count_1h", b"lightning_strike_count_1h", "lux", b"lux", "one_wire_temperature_ch0", b"one_wire_temperature_ch0", "one_wire_temperature_ch1", b"one_wire_temperature_ch1", "one_wire_temperature_ch2", b"one_wire_temperature_ch2", "one_wire_temperature_ch3", b"one_wire_temperature_ch3", "one_wire_temperature_ch4", b"one_wire_temperature_ch4", "one_wire_temperature_ch5", b"one_wire_temperature_ch5", "one_wire_temperature_ch6", b"one_wire_temperature_ch6", "one_wire_temperature_ch7", b"one_wire_temperature_ch7", "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["_adc_voltage_ch0", b"_adc_voltage_ch0", "_adc_voltage_ch1", b"_adc_voltage_ch1", "_adc_voltage_ch2", b"_adc_voltage_ch2", "_adc_voltage_ch3", b"_adc_voltage_ch3", "_adc_voltage_ch4", b"_adc_voltage_ch4", "_adc_voltage_ch5", b"_adc_voltage_ch5", "_adc_voltage_ch6", b"_adc_voltage_ch6", "_adc_voltage_ch7", b"_adc_voltage_ch7", "_barometric_pressure", b"_barometric_pressure", "_current", b"_current", "_distance", b"_distance", "_gas_resistance", b"_gas_resistance", "_iaq", b"_iaq", "_ir_lux", b"_ir_lux", "_lightning_distance_km", b"_lightning_distance_km", "_lightning_strike_count_1h", b"_lightning_strike_count_1h", "_lux", b"_lux", "_one_wire_temperature_ch0", b"_one_wire_temperature_ch0", "_one_wire_temperature_ch1", b"_one_wire_temperature_ch1", "_one_wire_temperature_ch2", b"_one_wire_temperature_ch2", "_one_wire_temperature_ch3", b"_one_wire_temperature_ch3", "_one_wire_temperature_ch4", b"_one_wire_temperature_ch4", "_one_wire_temperature_ch5", b"_one_wire_temperature_ch5", "_one_wire_temperature_ch6", b"_one_wire_temperature_ch6", "_one_wire_temperature_ch7", b"_one_wire_temperature_ch7", "_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", "adc_voltage_ch0", b"adc_voltage_ch0", "adc_voltage_ch1", b"adc_voltage_ch1", "adc_voltage_ch2", b"adc_voltage_ch2", "adc_voltage_ch3", b"adc_voltage_ch3", "adc_voltage_ch4", b"adc_voltage_ch4", "adc_voltage_ch5", b"adc_voltage_ch5", "adc_voltage_ch6", b"adc_voltage_ch6", "adc_voltage_ch7", b"adc_voltage_ch7", "barometric_pressure", b"barometric_pressure", "current", b"current", "distance", b"distance", "gas_resistance", b"gas_resistance", "iaq", b"iaq", "ir_lux", b"ir_lux", "lightning_distance_km", b"lightning_distance_km", "lightning_strike_count_1h", b"lightning_strike_count_1h", "lux", b"lux", "one_wire_temperature", b"one_wire_temperature", "one_wire_temperature_ch0", b"one_wire_temperature_ch0", "one_wire_temperature_ch1", b"one_wire_temperature_ch1", "one_wire_temperature_ch2", b"one_wire_temperature_ch2", "one_wire_temperature_ch3", b"one_wire_temperature_ch3", "one_wire_temperature_ch4", b"one_wire_temperature_ch4", "one_wire_temperature_ch5", b"one_wire_temperature_ch5", "one_wire_temperature_ch6", b"one_wire_temperature_ch6", "one_wire_temperature_ch7", b"one_wire_temperature_ch7", "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["_adc_voltage_ch0", b"_adc_voltage_ch0"]) -> typing.Literal["adc_voltage_ch0"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch1", b"_adc_voltage_ch1"]) -> typing.Literal["adc_voltage_ch1"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch2", b"_adc_voltage_ch2"]) -> typing.Literal["adc_voltage_ch2"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch3", b"_adc_voltage_ch3"]) -> typing.Literal["adc_voltage_ch3"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch4", b"_adc_voltage_ch4"]) -> typing.Literal["adc_voltage_ch4"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch5", b"_adc_voltage_ch5"]) -> typing.Literal["adc_voltage_ch5"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch6", b"_adc_voltage_ch6"]) -> typing.Literal["adc_voltage_ch6"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_adc_voltage_ch7", b"_adc_voltage_ch7"]) -> typing.Literal["adc_voltage_ch7"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_barometric_pressure", b"_barometric_pressure"]) -> typing.Literal["barometric_pressure"] | None: ...
@typing.overload
@@ -691,8 +847,28 @@ class EnvironmentMetrics(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_ir_lux", b"_ir_lux"]) -> typing.Literal["ir_lux"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_lightning_distance_km", b"_lightning_distance_km"]) -> typing.Literal["lightning_distance_km"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_lightning_strike_count_1h", b"_lightning_strike_count_1h"]) -> typing.Literal["lightning_strike_count_1h"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_lux", b"_lux"]) -> typing.Literal["lux"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch0", b"_one_wire_temperature_ch0"]) -> typing.Literal["one_wire_temperature_ch0"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch1", b"_one_wire_temperature_ch1"]) -> typing.Literal["one_wire_temperature_ch1"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch2", b"_one_wire_temperature_ch2"]) -> typing.Literal["one_wire_temperature_ch2"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch3", b"_one_wire_temperature_ch3"]) -> typing.Literal["one_wire_temperature_ch3"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch4", b"_one_wire_temperature_ch4"]) -> typing.Literal["one_wire_temperature_ch4"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch5", b"_one_wire_temperature_ch5"]) -> typing.Literal["one_wire_temperature_ch5"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch6", b"_one_wire_temperature_ch6"]) -> typing.Literal["one_wire_temperature_ch6"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_one_wire_temperature_ch7", b"_one_wire_temperature_ch7"]) -> typing.Literal["one_wire_temperature_ch7"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_radiation", b"_radiation"]) -> typing.Literal["radiation"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_rainfall_1h", b"_rainfall_1h"]) -> typing.Literal["rainfall_1h"] | None: ...
@@ -775,43 +951,43 @@ class PowerMetrics(google.protobuf.message.Message):
"""
ch4_voltage: builtins.float
"""
Voltage (Ch4)
Voltage (Ch4) - TODO Remove
"""
ch4_current: builtins.float
"""
Current (Ch4)
Current (Ch4) - TODO Remove
"""
ch5_voltage: builtins.float
"""
Voltage (Ch5)
Voltage (Ch5) - TODO Remove
"""
ch5_current: builtins.float
"""
Current (Ch5)
Current (Ch5) - TODO Remove
"""
ch6_voltage: builtins.float
"""
Voltage (Ch6)
Voltage (Ch6) - TODO Remove
"""
ch6_current: builtins.float
"""
Current (Ch6)
Current (Ch6) - TODO Remove
"""
ch7_voltage: builtins.float
"""
Voltage (Ch7)
Voltage (Ch7) - TODO Remove
"""
ch7_current: builtins.float
"""
Current (Ch7)
Current (Ch7) - TODO Remove
"""
ch8_voltage: builtins.float
"""
Voltage (Ch8)
Voltage (Ch8) - TODO Remove
"""
ch8_current: builtins.float
"""
Current (Ch8)
Current (Ch8) - TODO Remove
"""
def __init__(
self,
@@ -903,6 +1079,7 @@ class AirQualityMetrics(google.protobuf.message.Message):
PM_VOC_IDX_FIELD_NUMBER: builtins.int
PM_NOX_IDX_FIELD_NUMBER: builtins.int
PARTICLES_TPS_FIELD_NUMBER: builtins.int
PM_STATUS_FLAGS_FIELD_NUMBER: builtins.int
pm10_standard: builtins.int
"""
Concentration Units Standard PM1.0 in ug/m3
@@ -1003,6 +1180,13 @@ class AirQualityMetrics(google.protobuf.message.Message):
"""
Typical Particle Size in um
"""
pm_status_flags: builtins.int
"""
Raw PM sensor device status/error register bitmask, as defined by the sensor's own datasheet
(currently populated by the SEN6X family: bit 4 fan error, bit 6 RH&T error, bit 7 gas/VOC-NOx
error, bit 9 CO2 error (SEN66), bit 10 HCHO error, bit 11 PM error, bit 12 CO2 error (SEN63C/SEN69C),
bit 21 fan speed warning)
"""
def __init__(
self,
*,
@@ -1031,9 +1215,10 @@ class AirQualityMetrics(google.protobuf.message.Message):
pm_voc_idx: builtins.float | None = ...,
pm_nox_idx: builtins.float | None = ...,
particles_tps: builtins.float | None = ...,
pm_status_flags: builtins.int | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> None: ...
def HasField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_status_flags", b"_pm_status_flags", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_status_flags", b"pm_status_flags", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["_co2", b"_co2", "_co2_humidity", b"_co2_humidity", "_co2_temperature", b"_co2_temperature", "_form_formaldehyde", b"_form_formaldehyde", "_form_humidity", b"_form_humidity", "_form_temperature", b"_form_temperature", "_particles_03um", b"_particles_03um", "_particles_05um", b"_particles_05um", "_particles_100um", b"_particles_100um", "_particles_10um", b"_particles_10um", "_particles_25um", b"_particles_25um", "_particles_40um", b"_particles_40um", "_particles_50um", b"_particles_50um", "_particles_tps", b"_particles_tps", "_pm100_environmental", b"_pm100_environmental", "_pm100_standard", b"_pm100_standard", "_pm10_environmental", b"_pm10_environmental", "_pm10_standard", b"_pm10_standard", "_pm25_environmental", b"_pm25_environmental", "_pm25_standard", b"_pm25_standard", "_pm40_standard", b"_pm40_standard", "_pm_humidity", b"_pm_humidity", "_pm_nox_idx", b"_pm_nox_idx", "_pm_status_flags", b"_pm_status_flags", "_pm_temperature", b"_pm_temperature", "_pm_voc_idx", b"_pm_voc_idx", "co2", b"co2", "co2_humidity", b"co2_humidity", "co2_temperature", b"co2_temperature", "form_formaldehyde", b"form_formaldehyde", "form_humidity", b"form_humidity", "form_temperature", b"form_temperature", "particles_03um", b"particles_03um", "particles_05um", b"particles_05um", "particles_100um", b"particles_100um", "particles_10um", b"particles_10um", "particles_25um", b"particles_25um", "particles_40um", b"particles_40um", "particles_50um", b"particles_50um", "particles_tps", b"particles_tps", "pm100_environmental", b"pm100_environmental", "pm100_standard", b"pm100_standard", "pm10_environmental", b"pm10_environmental", "pm10_standard", b"pm10_standard", "pm25_environmental", b"pm25_environmental", "pm25_standard", b"pm25_standard", "pm40_standard", b"pm40_standard", "pm_humidity", b"pm_humidity", "pm_nox_idx", b"pm_nox_idx", "pm_status_flags", b"pm_status_flags", "pm_temperature", b"pm_temperature", "pm_voc_idx", b"pm_voc_idx"]) -> None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_co2", b"_co2"]) -> typing.Literal["co2"] | None: ...
@typing.overload
@@ -1081,6 +1266,8 @@ class AirQualityMetrics(google.protobuf.message.Message):
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_nox_idx", b"_pm_nox_idx"]) -> typing.Literal["pm_nox_idx"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_status_flags", b"_pm_status_flags"]) -> typing.Literal["pm_status_flags"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_temperature", b"_pm_temperature"]) -> typing.Literal["pm_temperature"] | None: ...
@typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_pm_voc_idx", b"_pm_voc_idx"]) -> typing.Literal["pm_voc_idx"] | None: ...
@@ -1488,10 +1675,33 @@ class Nau7802Config(google.protobuf.message.Message):
global___Nau7802Config = Nau7802Config
@typing.final
class AS3935Config(google.protobuf.message.Message):
"""
AS3935 lightning sensor configuration, for saving to flash
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
TUNING_CAP_PF_FIELD_NUMBER: builtins.int
tuning_cap_pf: builtins.int
"""
Antenna tuning capacitance in pF, 0 to 120 in steps of 8. The chip does not retain
this across power loss, so it is stored here and re-applied on every boot.
"""
def __init__(
self,
*,
tuning_cap_pf: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["tuning_cap_pf", b"tuning_cap_pf"]) -> None: ...
global___AS3935Config = AS3935Config
@typing.final
class SEN5XState(google.protobuf.message.Message):
"""
SEN5X State, for saving to flash
SEN5X State, for saving to flash (to be merged with SEN6XState)
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -1546,3 +1756,62 @@ class SEN5XState(google.protobuf.message.Message):
def WhichOneof(self, oneof_group: typing.Literal["_voc_state_valid", b"_voc_state_valid"]) -> typing.Literal["voc_state_valid"] | None: ...
global___SEN5XState = SEN5XState
@typing.final
class SEN6XState(google.protobuf.message.Message):
"""
SEN6X 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 SEN6X
"""
last_cleaning_valid: builtins.bool
"""
Last cleaning time for SEN6X - 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 models with a VOC sensor (SEN65, SEN66, SEN68, SEN69C)
"""
voc_state_valid: builtins.bool
"""
Last VOC state validity flag, for models with a VOC sensor (SEN65, SEN66, SEN68, SEN69C)
"""
voc_state_array: builtins.int
"""
VOC state array (8x uint8t), for models with a VOC sensor (SEN65, SEN66, SEN68, SEN69C)
"""
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___SEN6XState = SEN6XState
+31
View File
@@ -11,6 +11,11 @@ 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__)
@@ -80,6 +85,18 @@ class TCPInterface(StreamInterface):
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."""
logger.debug("Closing TCP stream")
@@ -87,6 +104,20 @@ class TCPInterface(StreamInterface):
# Therefore force a shutdown first to unblock reader thread reads.
self._wantExit = True
if self.socket is not None:
# 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
+81
View File
@@ -8,6 +8,87 @@ import pytest
from meshtastic import mt_config
from ..mesh_interface import MeshInterface
from .firmware_harness import (
CHAIN_TOPOLOGY,
DEFAULT_BASE_PORT,
SimMesh,
find_meshtasticd,
is_compatible_host,
)
from .fw_helpers import set_region
# Use a different base port for the single-node fixture so it doesn't
# conflict with the multi-node mesh fixture.
SINGLE_NODE_BASE_PORT = DEFAULT_BASE_PORT + 100
def _skip_firmware_if_unavailable() -> None:
"""Skip the test when meshtasticd can't run on this host."""
if not is_compatible_host():
pytest.skip("meshtasticd firmware tests require Linux")
if find_meshtasticd() is None:
pytest.skip(
"meshtasticd not found — set MESHTASTICD_BIN or install it on PATH"
)
@pytest.fixture(scope="function")
def firmware_node():
"""A single meshtasticd sim node for smokevirt tests.
Function-scoped so every test gets a freshly-erased node with no
state leaking from previous tests. This makes destructive commands
(``--reboot``, ``--set factory_reset true``) safe to run and lets
tests be order-independent.
Yields the SimNode instance. The node is booted with a fresh erased
config and listens on localhost at its TCP port. Region is set to US
so modem-preset tests work against firmware >= 2.8, which clamps
presets to the legal set for the current region.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=1, base_port=SINGLE_NODE_BASE_PORT)
mesh.start()
node = mesh.get_node(0)
set_region(node.port, "US")
# The region commit restarts the TCP listener, so reconnect the harness
# interface in case a test wants to use it directly.
if node.iface is not None:
try:
node.iface.close()
except Exception: # pylint: disable=broad-except
pass
node.connect()
yield node
mesh.stop()
@pytest.fixture(scope="function")
def firmware_mesh():
"""A 3-node chain (A-B-C) meshtasticd sim mesh for smokemesh tests.
Yields the SimMesh instance. Nodes are connected and the SIMULATOR_APP
packet bridge is running. Region is set to US for firmware >= 2.8
compatibility, interfaces are reconnected after the region change, and
node DB convergence is awaited.
"""
_skip_firmware_if_unavailable()
mesh = SimMesh(n_nodes=3, topology=CHAIN_TOPOLOGY)
mesh.start()
for node in mesh.nodes:
set_region(node.port, "US")
# The region commit restarts each node's TCP listener, so reconnect the
# harness interfaces before waiting for convergence.
for node in mesh.nodes:
if node.iface is not None:
try:
node.iface.close()
except Exception: # pylint: disable=broad-except
pass
node.connect()
mesh.wait_for_convergence(timeout=30)
yield mesh
mesh.stop()
@pytest.fixture
+357
View File
@@ -0,0 +1,357 @@
"""Test harness for running real meshtasticd firmware instances.
Launches one or more meshtasticd processes in simulator mode (-s) and bridges
their "over-the-air" packets via the SIMULATOR_APP protocol so that multiple
instances can communicate as if via LoRa, with a configurable topology.
The harness expects meshtasticd to be available on PATH or via the
MESHTASTICD_BIN environment variable. It does not download or build the
binary itself.
"""
import logging
import os
import platform
import shutil
import signal
import socket
import subprocess
import tempfile
import time
from typing import Dict, List, Optional, Set
from pubsub import pub # type: ignore[import-untyped]
from meshtastic import BROADCAST_NUM, mesh_pb2, portnums_pb2
from meshtastic.tcp_interface import TCPInterface
logger = logging.getLogger(__name__)
HW_ID_OFFSET = 16
DEFAULT_BASE_PORT = 4404
DEFAULT_RSSI = -50
DEFAULT_SNR = 10.0
BOOT_TIMEOUT = 30
CONNECT_TIMEOUT = 30
CHAIN_TOPOLOGY: Dict[int, Set[int]] = {
0: {1},
1: {0, 2},
2: {1},
}
def find_meshtasticd() -> Optional[str]:
"""Return the path to the meshtasticd binary, or None if not found."""
env_path = os.environ.get("MESHTASTICD_BIN")
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
return env_path
return shutil.which("meshtasticd")
def is_compatible_host() -> bool:
"""True when the host can run meshtasticd natively (Linux only)."""
return platform.system() == "Linux"
def _wait_for_port(port: int, timeout: int = BOOT_TIMEOUT) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
s = socket.create_connection(("localhost", port), timeout=0.5)
s.close()
return True
except OSError:
time.sleep(0.5)
return False
class SimNode:
"""A single meshtasticd simulator instance."""
def __init__(self, node_id: int, base_port: int = DEFAULT_BASE_PORT):
self.node_id = node_id
self.hw_id = node_id + HW_ID_OFFSET
self.port = base_port + node_id
self.process: Optional[subprocess.Popen] = None
self.workdir: Optional[str] = None
self.iface: Optional[TCPInterface] = None
self._log_files: list = []
@property
def node_num(self) -> int:
"""The firmware-assigned node number (== hw_id)."""
if self.iface and self.iface.myInfo:
return self.iface.myInfo.my_node_num
return self.hw_id
def start(self, binary: str) -> None:
"""Launch the meshtasticd process in simulator mode."""
self.workdir = tempfile.mkdtemp(prefix=f"mtd_node{self.node_id}_")
vfs_dir = os.path.join(self.workdir, "vfs")
os.mkdir(vfs_dir)
# Files are closed in _kill(); keep them open for the process lifetime.
log_stdout = open( # pylint: disable=consider-using-with
os.path.join(self.workdir, "meshtasticd.log"), "wb", buffering=0
)
log_stderr = open( # pylint: disable=consider-using-with
os.path.join(self.workdir, "meshtasticd.err"), "wb", buffering=0
)
self._log_files = [log_stdout, log_stderr]
self.process = subprocess.Popen( # pylint: disable=consider-using-with
[
binary,
"-s",
"-h", str(self.hw_id),
"-p", str(self.port),
"-d", vfs_dir,
"-e",
],
stdout=log_stdout,
stderr=log_stderr,
start_new_session=True,
)
if not _wait_for_port(self.port):
self._kill()
raise RuntimeError(
f"meshtasticd node {self.node_id} did not start listening on port {self.port}"
)
def connect(self) -> None:
"""Open a TCPInterface connection to this node."""
self.iface = TCPInterface(
hostname="localhost",
portNumber=self.port,
connectNow=False,
)
self.iface.myConnect()
self.iface.connect()
def close(self) -> None:
"""Close the interface and kill the process."""
if self.iface is not None:
try:
self.iface.localNode.exitSimulator()
except Exception:
pass
try:
self.iface.close()
except Exception:
pass
self.iface = None
self._kill()
if self.workdir:
shutil.rmtree(self.workdir, ignore_errors=True)
self.workdir = None
def _kill(self) -> None:
for f in self._log_files:
try:
f.close()
except Exception:
pass
self._log_files = []
if self.process is not None:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except (ProcessLookupError, OSError):
pass
try:
self.process.wait(timeout=5)
except Exception:
try:
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
except Exception:
pass
# Give OS time to release TCP port (avoid TIME_WAIT preventing
# next instance from binding the same port)
time.sleep(1.0)
self.process = None
class SimMesh:
"""Manages N meshtasticd sim instances and bridges their SIMULATOR_APP packets.
When *topology* is None every node hears every other node (full mesh).
Otherwise *topology* maps a transmitter's node index to the set of receiver
node indices that can hear it.
"""
def __init__(
self,
n_nodes: int = 1,
topology: Optional[Dict[int, Set[int]]] = None,
base_port: int = DEFAULT_BASE_PORT,
):
self.n_nodes = n_nodes
self.topology = topology
self.base_port = base_port
self.nodes: List[SimNode] = [
SimNode(i, base_port) for i in range(n_nodes)
]
self._port_to_idx: Dict[int, int] = {}
self._started = False
def start(self) -> None:
"""Launch all nodes, connect, and start the packet bridge."""
binary = find_meshtasticd()
if binary is None:
raise RuntimeError(
"meshtasticd not found. Set MESHTASTICD_BIN or install it on PATH."
)
for node in self.nodes:
node.start(binary)
for node in self.nodes:
node.connect()
self._port_to_idx[node.port] = node.node_id
pub.subscribe(self._on_sim_packet, "meshtastic.receive.simulator")
self._started = True
if self.n_nodes > 1:
self._trigger_convergence()
def _trigger_convergence(self) -> None:
"""Actively trigger NodeInfo exchange instead of waiting passively.
Sends a NODEINFO_APP packet with wantResponse from each node so the
firmware's NodeInfoModule responds with its own user info, populating
all node DBs deterministically.
"""
for node in self.nodes:
iface = node.iface
if iface is None:
continue
user = mesh_pb2.User()
user.id = f"!{node.node_num:08x}"
user.long_name = f"Node {node.node_id}"
user.short_name = f"{node.node_id:04d}"[:4]
user.hw_model = mesh_pb2.HardwareModel.PORTDUINO
try:
iface.sendData(
user,
destinationId=BROADCAST_NUM,
portNum=portnums_pb2.PortNum.NODEINFO_APP,
wantAck=False,
wantResponse=True,
)
except Exception as ex:
logger.debug("NodeInfo trigger for node %d failed: %s", node.node_id, ex)
time.sleep(5)
def stop(self) -> None:
"""Shut down all nodes and clean up."""
if not self._started:
return
try:
pub.unsubscribe(self._on_sim_packet, "meshtastic.receive.simulator")
except Exception:
pass
for node in self.nodes:
node.close()
self._started = False
def get_node(self, idx: int) -> SimNode:
"""Return the SimNode at the given index."""
return self.nodes[idx]
def get_iface(self, idx: int) -> TCPInterface:
"""Return the TCPInterface for the node at the given index."""
iface = self.nodes[idx].iface
assert iface is not None, f"node {idx} has no interface"
return iface
def wait_for_convergence(self, timeout: int = 30) -> bool:
"""Wait until every node sees all others in its node DB.
Returns True if converged, False if timed out (non-fatal the packet
bridge forwards regardless of node DB state).
"""
if self.n_nodes <= 1:
return True
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if all(
node.iface is not None
and node.iface.nodes is not None
and len(node.iface.nodes) >= self.n_nodes
for node in self.nodes
):
return True
time.sleep(2)
logger.warning("Mesh did not fully converge within %ds", timeout)
return False
def _get_receivers(self, tx_idx: int) -> List[int]:
"""Return node indices that can hear a transmission from *tx_idx*."""
if self.topology is not None:
return sorted(self.topology.get(tx_idx, set()))
return [i for i in range(self.n_nodes) if i != tx_idx]
def _on_sim_packet(self, interface, packet) -> None:
"""Bridge callback: forward a SIMULATOR_APP packet to receiving nodes."""
tx_port = getattr(interface, "portNumber", None)
tx_idx = self._port_to_idx.get(tx_port) if tx_port else None
if tx_idx is None:
return
rx_indices = self._get_receivers(tx_idx)
if not rx_indices:
return
data = packet["decoded"]["payload"]
if hasattr(data, "SerializeToString"):
data = data.SerializeToString()
if len(data) > mesh_pb2.Constants.DATA_PAYLOAD_LEN:
logger.warning("Simulator payload too big (%d bytes), dropping", len(data))
return
mesh_packet = _build_mesh_packet(packet, data)
for rx_idx in rx_indices:
rx_iface = self.nodes[rx_idx].iface
if rx_iface is None:
continue
mesh_packet.rx_rssi = DEFAULT_RSSI
mesh_packet.rx_snr = DEFAULT_SNR
to_radio = mesh_pb2.ToRadio()
to_radio.packet.CopyFrom(mesh_packet)
try:
rx_iface._sendToRadio(to_radio)
except Exception as ex:
logger.error("Error forwarding packet to node %d: %s", rx_idx, ex)
def __enter__(self):
self.start()
return self
def __exit__(self, *exc):
self.stop()
def _build_mesh_packet(packet: dict, data: bytes) -> mesh_pb2.MeshPacket:
"""Reconstruct a MeshPacket for SIMULATOR_APP injection."""
mp = mesh_pb2.MeshPacket()
mp.decoded.payload = data
mp.decoded.portnum = portnums_pb2.PortNum.SIMULATOR_APP
mp.to = packet.get("to", BROADCAST_NUM)
setattr(mp, "from", packet.get("from", 0))
mp.id = packet.get("id", 0)
mp.want_ack = packet.get("wantAck", False)
mp.hop_limit = packet.get("hopLimit", 0)
mp.hop_start = packet.get("hopStart", 0)
mp.via_mqtt = packet.get("viaMQTT", False)
mp.relay_node = packet.get("relayNode", 0)
mp.next_hop = packet.get("nextHop", 0)
mp.channel = int(packet.get("channel", 0))
decoded = packet.get("decoded", {})
if "requestId" in decoded:
mp.decoded.request_id = decoded["requestId"]
if "wantResponse" in decoded:
mp.decoded.want_response = decoded["wantResponse"]
if "bitfield" in decoded:
mp.decoded.bitfield = decoded["bitfield"]
return mp
+374
View 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",
]
+11 -2
View File
@@ -21,6 +21,7 @@ from meshtastic.protobuf import (
atak_pb2,
config_pb2,
mesh_pb2,
mqtt_pb2,
nanopb_pb2,
telemetry_pb2,
)
@@ -640,7 +641,15 @@ def test_descriptor_multilevel_nested_route_link_uid():
@pytest.mark.unit
def test_descriptor_telemetry_environment_one_wire_temperature():
"""EnvironmentMetrics.one_wire_temperature has max_count = 8 from telemetry.options."""
"""EnvironmentMetrics.one_wire_temperature has type = FT_IGNORE from telemetry.options."""
env = telemetry_pb2.DESCRIPTOR.message_types_by_name["EnvironmentMetrics"]
opts = _field_opts(env, "one_wire_temperature")
assert opts.max_count == 8
assert opts.type == nanopb_pb2.FT_IGNORE
@pytest.mark.unit
def test_descriptor_mqtt_service_envelope_pointer_fields():
"""ServiceEnvelope fields carry type = FT_POINTER from mqtt.options."""
envelope = mqtt_pb2.DESCRIPTOR.message_types_by_name["ServiceEnvelope"]
for name in ("packet", "channel_id", "gateway_id"):
assert _field_opts(envelope, name).type == nanopb_pb2.FT_POINTER
+597 -9
View File
@@ -10,11 +10,13 @@ import tempfile
from types import SimpleNamespace
from unittest.mock import mock_open, MagicMock, patch
import yaml
import pytest
import meshtastic.__main__ as mt_main
from meshtastic.__main__ import (
export_config,
export_profile,
initParser,
main,
onConnection,
@@ -23,11 +25,14 @@ from meshtastic.__main__ import (
setPref,
tunnelMain,
set_missing_flags_false,
_profile_from_yaml,
)
from meshtastic import mt_config
from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611
from ..protobuf.config_pb2 import Config # pylint: disable=E0611
from ..protobuf.clientonly_pb2 import DeviceProfile # pylint: disable=E0611
from ..protobuf.localonly_pb2 import LocalConfig, LocalModuleConfig # pylint: disable=E0611
# from ..ble_interface import BLEInterface
from ..mesh_interface import MeshInterface
@@ -871,7 +876,7 @@ def test_main_sendtext_with_invalid_channel_nine(caplog, capsys):
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_sendtext_with_dest(mock_findPorts, mock_serial, mocked_open, mock_hupcl, capsys, caplog, iface_with_nodes):
@@ -1069,7 +1074,7 @@ def test_main_seturl(capsys):
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
@@ -1093,7 +1098,7 @@ def test_main_set_valid(mocked_findports, mocked_serial, mocked_open, mocked_hup
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
@@ -1117,7 +1122,7 @@ def test_main_set_valid_wifi_psk(mocked_findports, mocked_serial, mocked_open, m
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
@@ -1144,7 +1149,7 @@ def test_main_set_invalid_wifi_psk(mocked_findports, mocked_serial, mocked_open,
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
@@ -1169,7 +1174,7 @@ def test_main_set_valid_camel_case(mocked_findports, mocked_serial, mocked_open,
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
@@ -1194,7 +1199,7 @@ def test_main_set_with_invalid(mocked_findports, mocked_serial, mocked_open, moc
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
@@ -1226,7 +1231,7 @@ def test_main_configure_with_snake_case(mocked_findports, mocked_serial, mocked_
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_main_configure_with_camel_case_keys(mocked_findports, mocked_serial, mocked_open, mocked_hupcl, capsys):
@@ -2012,6 +2017,471 @@ position_flags: 35"""
assert err == ""
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_export_profile_serializes_deviceprofile():
"""export_profile() returns a parseable DeviceProfile protobuf"""
iface = MagicMock(autospec=SerialInterface)
iface.getLongName.return_value = "foo"
iface.getShortName.return_value = "oof"
iface.localNode.getURL.return_value = "https://meshtastic.org/e/#test"
iface.getCannedMessage.return_value = "Hi|Bye"
iface.getRingtone.return_value = "24:d=32,o=5"
iface.getMyNodeInfo.return_value = {
"position": {"latitude": 35.88888, "longitude": -93.88888, "altitude": 304}
}
iface.localNode.localConfig = LocalConfig()
iface.localNode.localConfig.position.fixed_position = True
iface.localNode.moduleConfig = LocalModuleConfig()
raw = export_profile(iface)
profile = DeviceProfile()
profile.ParseFromString(raw) # must not raise
assert profile.long_name == "foo"
assert profile.short_name == "oof"
assert profile.channel_url == "https://meshtastic.org/e/#test"
assert profile.canned_messages == "Hi|Bye"
assert profile.ringtone == "24:d=32,o=5"
assert profile.HasField("fixed_position")
assert profile.fixed_position.latitude_i == 358888800
assert profile.fixed_position.longitude_i == -938888800
assert profile.fixed_position.altitude == 304
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_export_profile_omits_fixed_position_when_not_enabled():
"""export_profile() omits fixed_position when position.fixed_position is False"""
iface = MagicMock(autospec=SerialInterface)
iface.getLongName.return_value = "foo"
iface.getShortName.return_value = "oof"
iface.localNode.getURL.return_value = ""
iface.getCannedMessage.return_value = None
iface.getRingtone.return_value = None
iface.getMyNodeInfo.return_value = {
"position": {"latitude": 35.88888, "longitude": -93.88888, "altitude": 304}
}
iface.localNode.localConfig = LocalConfig()
iface.localNode.moduleConfig = LocalModuleConfig()
raw = export_profile(iface)
profile = DeviceProfile()
profile.ParseFromString(raw)
assert not profile.HasField("fixed_position"), \
"fixed_position should be omitted when position.fixed_position is not enabled"
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_export_profile_omits_empty_fields():
"""export_profile() should not populate protobuf oneofs for missing values"""
iface = MagicMock(autospec=SerialInterface)
iface.getLongName.return_value = None
iface.getShortName.return_value = None
iface.localNode.getURL.return_value = ""
iface.getCannedMessage.return_value = None
iface.getRingtone.return_value = None
iface.getMyNodeInfo.return_value = {}
iface.localNode.localConfig = LocalConfig()
iface.localNode.moduleConfig = LocalModuleConfig()
raw = export_profile(iface)
profile = DeviceProfile()
profile.ParseFromString(raw)
assert not profile.HasField("long_name")
assert not profile.HasField("short_name")
assert not profile.HasField("channel_url")
assert not profile.HasField("canned_messages")
assert not profile.HasField("ringtone")
assert not profile.HasField("fixed_position")
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_export_profile_converts_precisely():
"""export_profile() uses exact arithmetic for lat/lon conversion"""
iface = MagicMock(autospec=SerialInterface)
iface.getLongName.return_value = None
iface.getShortName.return_value = None
iface.localNode.getURL.return_value = ""
iface.getCannedMessage.return_value = None
iface.getRingtone.return_value = None
iface.getMyNodeInfo.return_value = {
"position": {"latitude": -49.82207, "longitude": 0, "altitude": 0},
}
iface.localNode.localConfig = LocalConfig()
iface.localNode.localConfig.position.fixed_position = True
iface.localNode.moduleConfig = LocalModuleConfig()
raw = export_profile(iface)
profile = DeviceProfile()
profile.ParseFromString(raw)
assert profile.fixed_position.latitude_i == -498220700
assert profile.fixed_position.longitude_i == 0
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_profile_from_yaml_maps_all_fields():
"""_profile_from_yaml() converts a YAML dict to an equivalent DeviceProfile"""
configuration = {
"owner": "YAML Owner",
"owner_short": "YO",
"channel_url": "https://meshtastic.org/e/#test",
"canned_messages": "Hi|Bye",
"ringtone": "24:d=32,o=5",
"location": {"lat": 35.88888, "lon": -93.88888, "alt": 304},
"config": {"bluetooth": {"enabled": True, "fixedPin": 123456}},
"module_config": {"telemetry": {"deviceUpdateInterval": 900}},
}
profile = _profile_from_yaml(configuration)
assert profile.long_name == "YAML Owner"
assert profile.short_name == "YO"
assert profile.channel_url == "https://meshtastic.org/e/#test"
assert profile.canned_messages == "Hi|Bye"
assert profile.ringtone == "24:d=32,o=5"
assert profile.HasField("fixed_position")
assert profile.fixed_position.latitude_i == 358888800
assert profile.fixed_position.longitude_i == -938888800
assert profile.fixed_position.altitude == 304
assert profile.HasField("config")
assert profile.config.bluetooth.enabled is True
assert profile.config.bluetooth.fixed_pin == 123456
assert profile.HasField("module_config")
assert profile.module_config.telemetry.device_update_interval == 900
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_profile_from_yaml_camelcase_keys():
"""_profile_from_yaml() handles camelCase YAML keys"""
configuration = {
"ownerShort": "CB",
"channelUrl": "https://meshtastic.org/e/#camel",
}
profile = _profile_from_yaml(configuration)
assert profile.short_name == "CB"
assert profile.channel_url == "https://meshtastic.org/e/#camel"
assert not profile.HasField("long_name")
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_profile_from_yaml_converts_precisely():
"""_profile_from_yaml() uses exact arithmetic for lat/lon conversion"""
# -49.82207 is a value where int(float * 1e7) truncates to -498220699
# instead of the correct -498220700
configuration = {
"location": {"lat": -49.82207, "lon": 0, "alt": 0},
}
profile = _profile_from_yaml(configuration)
assert profile.fixed_position.latitude_i == -498220700
assert profile.fixed_position.longitude_i == 0
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_combines_setowner_into_single_call(tmp_path, capsys):
"""Both long_name and short_name are set in a single setOwner call"""
profile = DeviceProfile()
profile.long_name = "Single Call"
profile.short_name = "SC"
cfg_path = tmp_path / "combined.cfg"
cfg_path.write_bytes(profile.SerializeToString())
sys.argv = ["", "--configure", str(cfg_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
# Single combined message, not two separate ones
assert re.search(r"Setting device owner to Single Call and short name to SC", out, re.MULTILINE)
assert not re.search(r"Setting device owner short to SC$", out, re.MULTILINE)
# setOwner should be called once, not twice
iface.getNode.return_value.setOwner.assert_called_once()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_with_binary_cfg(tmp_path, capsys):
"""--configure with a .cfg binary file parses and applies a DeviceProfile"""
profile = DeviceProfile()
profile.long_name = "Binary Bob"
profile.short_name = "BB"
cfg_path = tmp_path / "backup.cfg"
cfg_path.write_bytes(profile.SerializeToString())
sys.argv = ["", "--configure", str(cfg_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Setting device owner to Binary Bob and short name to BB", out, re.MULTILINE)
assert re.search(r"Writing modified configuration to device", out, re.MULTILINE)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_rejects_invalid_yaml_and_extension(tmp_path, capsys):
"""A non-dict YAML file with a non-.cfg extension should error cleanly."""
bad = tmp_path / "bad.yaml"
bad.write_text("just a scalar string")
sys.argv = ["", "--configure", str(bad)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
with pytest.raises(SystemExit):
main()
out, _ = capsys.readouterr()
assert re.search(r"is not a valid YAML config or DeviceProfile", out, re.MULTILINE)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_rejects_corrupt_cfg(tmp_path, capsys):
"""A .cfg file that is not a valid DeviceProfile should error cleanly."""
bad = tmp_path / "corrupt.cfg"
bad.write_bytes(b"\x00\x01\x02 not really protobuf \xff\xfe")
sys.argv = ["", "--configure", str(bad)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
with pytest.raises(SystemExit):
main()
out, _ = capsys.readouterr()
assert re.search(r"is not a valid YAML config or DeviceProfile", out, re.MULTILINE)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_binary_with_explicit_format(tmp_path, capsys):
"""--configure --export-format binary forces binary parsing regardless of extension"""
profile = DeviceProfile()
profile.long_name = "Forced Binary"
cfg_path = tmp_path / "notcfg.dat" # non-.cfg extension, but format forced
cfg_path.write_bytes(profile.SerializeToString())
sys.argv = ["", "--configure", str(cfg_path), "--export-format", "binary"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Setting device owner to Forced Binary", out, re.MULTILINE)
assert re.search(r"Writing modified configuration to device", out, re.MULTILINE)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_autodetects_binary_by_content(tmp_path, capsys):
"""Binary DeviceProfile is detected by content even without .cfg extension"""
profile = DeviceProfile()
profile.long_name = "Content Detected"
# Use .bin extension — autodetection must rely on content, not extension
cfg_path = tmp_path / "backup.bin"
cfg_path.write_bytes(profile.SerializeToString())
sys.argv = ["", "--configure", str(cfg_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Setting device owner to Content Detected", out, re.MULTILINE)
assert re.search(r"Writing modified configuration to device", out, re.MULTILINE)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_autodetects_yaml_by_content(tmp_path, capsys):
"""YAML config is detected by content even with a non-standard extension"""
yaml_path = tmp_path / "config.txt"
yaml_path.write_text("owner: YAML Detected\n")
sys.argv = ["", "--configure", str(yaml_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Setting device owner to YAML Detected", out, re.MULTILINE)
assert re.search(r"Writing modified configuration to device", out, re.MULTILINE)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_export_config_binary_round_trip(tmp_path, capsys):
"""Round-trip: export a .cfg via main(), then --configure it back via main()."""
cfg_path = tmp_path / "roundtrip.cfg"
# Export
sys.argv = ["", "--export-config", str(cfg_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
iface.getLongName.return_value = "Round Trip"
iface.getShortName.return_value = "RT"
iface.localNode.getURL.return_value = "https://meshtastic.org/e/#rt"
iface.getCannedMessage.return_value = "Yes|No"
iface.getRingtone.return_value = None
iface.getMyNodeInfo.return_value = {
"position": {"latitude": 1.0, "longitude": 2.0, "altitude": 3}
}
iface.localNode.localConfig = LocalConfig()
iface.localNode.localConfig.position.fixed_position = True
iface.localNode.moduleConfig = LocalModuleConfig()
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Exported profile to", out, re.MULTILINE)
assert cfg_path.exists()
# Verify the exported file is a valid DeviceProfile
profile = DeviceProfile()
profile.ParseFromString(cfg_path.read_bytes())
assert profile.long_name == "Round Trip"
assert profile.short_name == "RT"
assert profile.HasField("fixed_position")
assert profile.fixed_position.latitude_i == int(1.0 * 1e7)
assert profile.fixed_position.longitude_i == int(2.0 * 1e7)
assert profile.fixed_position.altitude == 3
# Re-import the file we just wrote
sys.argv = ["", "--configure", str(cfg_path)]
mt_config.args = sys.argv
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Setting device owner to Round Trip and short name to RT", out, re.MULTILINE)
assert re.search(r"Setting device position", out, re.MULTILINE)
assert re.search(r"Writing modified configuration to device", out, re.MULTILINE)
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_does_not_set_fixed_position_for_legacy_yaml(tmp_path, capsys):
"""Legacy YAML with location and no position config does NOT call setFixedPosition"""
yaml_path = tmp_path / "legacy.yaml"
yaml_path.write_text("location:\n lat: 35.0\n lon: -93.0\n alt: 100\n")
sys.argv = ["", "--configure", str(yaml_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert not re.search(r"Setting device position", out, re.MULTILINE)
iface.getNode.return_value.setFixedPosition.assert_not_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_skips_setfixedposition_when_config_disables_it(tmp_path, capsys):
"""YAML with location but position.fixed_position=false skips setFixedPosition"""
yaml_path = tmp_path / "no_fixed.yaml"
yaml_path.write_text(
"location:\n lat: 35.0\n lon: -93.0\n alt: 100\n"
"config:\n position:\n fixed_position: false\n"
)
sys.argv = ["", "--configure", str(yaml_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
iface.localNode.localConfig = LocalConfig()
iface.localNode.moduleConfig = LocalModuleConfig()
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert not re.search(r"Setting device position", out, re.MULTILINE)
iface.getNode.return_value.setFixedPosition.assert_not_called()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_calls_setfixedposition_when_config_opt_in(tmp_path, capsys):
"""YAML with location and position.fixed_position=true calls setFixedPosition"""
yaml_path = tmp_path / "fixed_on.yaml"
yaml_path.write_text(
"location:\n lat: 35.0\n lon: -93.0\n alt: 100\n"
"config:\n position:\n fixed_position: true\n"
)
sys.argv = ["", "--configure", str(yaml_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
iface.localNode.localConfig = LocalConfig()
iface.localNode.moduleConfig = LocalModuleConfig()
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Setting device position", out, re.MULTILINE)
iface.getNode.return_value.setFixedPosition.assert_called_once()
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_yaml_preserves_unmentioned_fields(tmp_path, capsys):
"""Partial YAML config does not reset unspecified fields in the same section."""
yaml_path = tmp_path / "partial.yaml"
yaml_path.write_text("config:\n bluetooth:\n enabled: false\n")
sys.argv = ["", "--configure", str(yaml_path)]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
local_config = LocalConfig()
local_config.bluetooth.enabled = True
local_config.bluetooth.fixed_pin = 654321
iface.localNode.localConfig = local_config
iface.localNode.moduleConfig = LocalModuleConfig()
# Make getNode return the same node object so apply writes back to our real config.
iface.getNode.return_value = iface.localNode
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Writing modified configuration to device", out, re.MULTILINE)
assert iface.localNode.localConfig.bluetooth.enabled is False
assert iface.localNode.localConfig.bluetooth.fixed_pin == 654321
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_main_configure_rejects_remote_node(tmp_path, capsys):
"""--configure with --dest pointing to a remote node is rejected."""
yaml_path = tmp_path / "remote.yaml"
yaml_path.write_text("owner: Remote Owner\n")
sys.argv = ["", "--configure", str(yaml_path), "--dest", "!12345678"]
mt_config.args = sys.argv
iface = MagicMock(autospec=SerialInterface)
with patch("meshtastic.serial_interface.SerialInterface", return_value=iface):
main()
out, _ = capsys.readouterr()
assert re.search(r"Configuring remote nodes is not supported", out, re.MULTILINE)
# TODO
# recursion depth exceeded error
#@pytest.mark.unit
@@ -2077,6 +2547,124 @@ position_flags: 35"""
# mo.assert_called()
_TRUE_DEFAULTS = [
"bluetooth.enabled",
"lora.sx126x_rx_boosted_gain",
"lora.tx_enabled",
"lora.use_preset",
"position.position_broadcast_smart_enabled",
"security.serial_enabled",
]
_MOD_TRUE_DEFAULTS = [
"mqtt.encryption_enabled",
]
def _set_config_bool(lc, path, value):
section, field = path.split(".")
setattr(getattr(lc, section), field, value)
def _assert_config_bool(profile, path, expected):
section, field = path.split(".")
assert getattr(getattr(profile.config, section), field) is expected
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
@pytest.mark.parametrize("fmt", ["binary", "yaml"])
@pytest.mark.parametrize("value", [True, False])
def test_round_trip_preserves_config_true_defaults(fmt, value):
"""Export-->import preserves config_true_defaults fields for both formats"""
iface = MagicMock(autospec=SerialInterface)
iface.getLongName.return_value = None
iface.getShortName.return_value = None
iface.localNode.getURL.return_value = ""
iface.getCannedMessage.return_value = None
iface.getRingtone.return_value = None
iface.getMyNodeInfo.return_value = {}
iface.localNode.localConfig = LocalConfig()
iface.localNode.moduleConfig = LocalModuleConfig()
for path in _TRUE_DEFAULTS:
_set_config_bool(iface.localNode.localConfig, path, value)
for path in _MOD_TRUE_DEFAULTS:
_set_config_bool(iface.localNode.moduleConfig, path, value)
if fmt == "binary":
raw = export_profile(iface)
profile = DeviceProfile()
profile.ParseFromString(raw)
else:
yaml_str = export_config(iface)
configuration = yaml.safe_load(yaml_str)
profile = _profile_from_yaml(configuration)
assert profile.HasField("config")
assert profile.HasField("module_config")
for path in _TRUE_DEFAULTS:
_assert_config_bool(profile, path, value)
for path in _MOD_TRUE_DEFAULTS:
section, field = path.split(".")
assert getattr(getattr(profile.module_config, section), field) is value
@pytest.mark.unit
@pytest.mark.usefixtures("reset_mt_config")
def test_binary_and_yaml_export_consistent():
"""Binary and YAML export paths produce equivalent DeviceProfile protos"""
iface = MagicMock(autospec=SerialInterface)
iface.getLongName.return_value = "Consistency"
iface.getShortName.return_value = "CON"
iface.localNode.getURL.return_value = ""
iface.getCannedMessage.return_value = None
iface.getRingtone.return_value = None
iface.getMyNodeInfo.return_value = {}
iface.localNode.localConfig = LocalConfig()
iface.localNode.moduleConfig = LocalModuleConfig()
lc = iface.localNode.localConfig
lc.bluetooth.enabled = True
lc.bluetooth.fixed_pin = 123456
lc.lora.sx126x_rx_boosted_gain = False
lc.lora.tx_enabled = True
lc.lora.use_preset = True
lc.position.position_broadcast_smart_enabled = False
lc.security.serial_enabled = True
lc_mod = iface.localNode.moduleConfig
lc_mod.mqtt.encryption_enabled = True
# Binary path
raw = export_profile(iface)
binary_profile = DeviceProfile()
binary_profile.ParseFromString(raw)
# YAML path
yaml_str = export_config(iface)
configuration = yaml.safe_load(yaml_str)
yaml_profile = _profile_from_yaml(configuration)
# Both should have config
assert binary_profile.HasField("config") == yaml_profile.HasField("config")
assert binary_profile.HasField("module_config") == yaml_profile.HasField("module_config")
# Compare individual field values
assert binary_profile.config.bluetooth.enabled == yaml_profile.config.bluetooth.enabled
assert binary_profile.config.bluetooth.fixed_pin == yaml_profile.config.bluetooth.fixed_pin
assert binary_profile.config.lora.sx126x_rx_boosted_gain == yaml_profile.config.lora.sx126x_rx_boosted_gain
assert binary_profile.config.lora.tx_enabled == yaml_profile.config.lora.tx_enabled
assert binary_profile.config.lora.use_preset == yaml_profile.config.lora.use_preset
assert binary_profile.config.position.position_broadcast_smart_enabled == yaml_profile.config.position.position_broadcast_smart_enabled
assert binary_profile.config.security.serial_enabled == yaml_profile.config.security.serial_enabled
assert binary_profile.module_config.mqtt.encryption_enabled == yaml_profile.module_config.mqtt.encryption_enabled
# Owner fields also match
assert binary_profile.long_name == yaml_profile.long_name
assert binary_profile.short_name == yaml_profile.short_name
@pytest.mark.unit
def test_set_missing_flags_false():
"""Test set_missing_flags_false() function"""
@@ -2906,7 +3494,7 @@ def test_tunnel_subnet_arg_with_no_devices(mock_platform_system, caplog, capsys)
@pytest.mark.usefixtures("reset_mt_config")
@patch("platform.system")
@patch("meshtastic.serial_interface.SerialInterface._set_hupcl_with_termios")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("builtins.open", new_callable=mock_open, read_data=b"{}")
@patch("serial.Serial")
@patch("meshtastic.util.findPorts", return_value=["/dev/ttyUSBfake"])
def test_tunnel_tunnel_arg(
@@ -12,11 +12,11 @@ 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
from_radio.moduleConfig.traffic_management.position_min_interval_secs = 30
from_radio.moduleConfig.traffic_management.rate_limit_window_secs = 60
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
assert iface.localNode.moduleConfig.traffic_management.position_min_interval_secs == 30
assert iface.localNode.moduleConfig.traffic_management.rate_limit_window_secs == 60
iface.close()
+76 -4
View File
@@ -669,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):
@@ -1081,8 +1153,8 @@ 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
anode.moduleConfig.traffic_management.position_min_interval_secs = 30
anode.moduleConfig.traffic_management.rate_limit_window_secs = 60
sent_admin = []
@@ -1095,8 +1167,8 @@ def test_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
assert sent_admin[0].set_module_config.traffic_management.position_min_interval_secs == 30
assert sent_admin[0].set_module_config.traffic_management.rate_limit_window_secs == 60
# TODO
+128
View 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
+51
View File
@@ -1,6 +1,7 @@
"""Meshtastic unit tests for tcp_interface.py"""
import re
import socket
from unittest.mock import MagicMock, patch
import pytest
@@ -78,6 +79,56 @@ def test_TCPInterface_close_shutdowns_socket_before_super_close():
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"""
Generated
+115 -47
View File
@@ -608,7 +608,7 @@ files = [
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
]
markers = {main = "extra == \"analysis\" and python_version >= \"3.11\"", powermon = "python_version >= \"3.11\""}
markers = {main = "python_version >= \"3.11\" and extra == \"analysis\"", powermon = "python_version >= \"3.11\""}
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
@@ -3284,7 +3284,7 @@ version = "2.0.2"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.9"
groups = ["main", "analysis", "powermon"]
groups = ["main", "analysis"]
files = [
{file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"},
{file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"},
@@ -3332,7 +3332,7 @@ files = [
{file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"},
{file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"},
]
markers = {main = "python_version < \"3.11\" and extra == \"analysis\"", analysis = "python_version < \"3.11\"", powermon = "python_version < \"3.11\""}
markers = {main = "python_version < \"3.11\" and extra == \"analysis\"", analysis = "python_version < \"3.11\""}
[[package]]
name = "numpy"
@@ -3340,7 +3340,7 @@ version = "2.3.4"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.11"
groups = ["main", "analysis", "powermon"]
groups = ["main", "analysis"]
files = [
{file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"},
{file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"},
@@ -3417,7 +3417,7 @@ files = [
{file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"},
{file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"},
]
markers = {main = "extra == \"analysis\" and python_version >= \"3.11\"", analysis = "python_version >= \"3.11\"", powermon = "python_version >= \"3.11\""}
markers = {main = "python_version >= \"3.11\" and extra == \"analysis\"", analysis = "python_version >= \"3.11\""}
[[package]]
name = "overrides"
@@ -3569,7 +3569,7 @@ description = "Type annotations for pandas"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"analysis\" and python_version >= \"3.11\""
markers = "python_version >= \"3.11\" and extra == \"analysis\""
files = [
{file = "pandas_stubs-2.3.2.250926-py3-none-any.whl", hash = "sha256:81121818453dcfe00f45c852f4dceee043640b813830f6e7bd084a4ef7ff7270"},
{file = "pandas_stubs-2.3.2.250926.tar.gz", hash = "sha256:c64b9932760ceefb96a3222b953e6a251321a9832a28548be6506df473a66406"},
@@ -4150,52 +4150,120 @@ tests = ["pytest"]
[[package]]
name = "pyarrow"
version = "16.1.0"
version = "18.1.0"
description = "Python library for Apache Arrow"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["powermon"]
markers = "python_version < \"3.14\""
files = [
{file = "pyarrow-16.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:17e23b9a65a70cc733d8b738baa6ad3722298fa0c81d88f63ff94bf25eaa77b9"},
{file = "pyarrow-16.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4740cc41e2ba5d641071d0ab5e9ef9b5e6e8c7611351a5cb7c1d175eaf43674a"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98100e0268d04e0eec47b73f20b39c45b4006f3c4233719c3848aa27a03c1aef"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68f409e7b283c085f2da014f9ef81e885d90dcd733bd648cfba3ef265961848"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a8914cd176f448e09746037b0c6b3a9d7688cef451ec5735094055116857580c"},
{file = "pyarrow-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:48be160782c0556156d91adbdd5a4a7e719f8d407cb46ae3bb4eaee09b3111bd"},
{file = "pyarrow-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cf389d444b0f41d9fe1444b70650fea31e9d52cfcb5f818b7888b91b586efff"},
{file = "pyarrow-16.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:d0ebea336b535b37eee9eee31761813086d33ed06de9ab6fc6aaa0bace7b250c"},
{file = "pyarrow-16.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e73cfc4a99e796727919c5541c65bb88b973377501e39b9842ea71401ca6c1c"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9251264247ecfe93e5f5a0cd43b8ae834f1e61d1abca22da55b20c788417f6"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf5aace92d520d3d2a20031d8b0ec27b4395cab9f74e07cc95edf42a5cc0147"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:25233642583bf658f629eb230b9bb79d9af4d9f9229890b3c878699c82f7d11e"},
{file = "pyarrow-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a33a64576fddfbec0a44112eaf844c20853647ca833e9a647bfae0582b2ff94b"},
{file = "pyarrow-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:185d121b50836379fe012753cf15c4ba9638bda9645183ab36246923875f8d1b"},
{file = "pyarrow-16.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:2e51ca1d6ed7f2e9d5c3c83decf27b0d17bb207a7dea986e8dc3e24f80ff7d6f"},
{file = "pyarrow-16.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06ebccb6f8cb7357de85f60d5da50e83507954af617d7b05f48af1621d331c9a"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04707f1979815f5e49824ce52d1dceb46e2f12909a48a6a753fe7cafbc44a0c"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d32000693deff8dc5df444b032b5985a48592c0697cb6e3071a5d59888714e2"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8785bb10d5d6fd5e15d718ee1d1f914fe768bf8b4d1e5e9bf253de8a26cb1628"},
{file = "pyarrow-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e1369af39587b794873b8a307cc6623a3b1194e69399af0efd05bb202195a5a7"},
{file = "pyarrow-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:febde33305f1498f6df85e8020bca496d0e9ebf2093bab9e0f65e2b4ae2b3444"},
{file = "pyarrow-16.1.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b5f5705ab977947a43ac83b52ade3b881eb6e95fcc02d76f501d549a210ba77f"},
{file = "pyarrow-16.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0d27bf89dfc2576f6206e9cd6cf7a107c9c06dc13d53bbc25b0bd4556f19cf5f"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d07de3ee730647a600037bc1d7b7994067ed64d0eba797ac74b2bc77384f4c2"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbef391b63f708e103df99fbaa3acf9f671d77a183a07546ba2f2c297b361e83"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19741c4dbbbc986d38856ee7ddfdd6a00fc3b0fc2d928795b95410d38bb97d15"},
{file = "pyarrow-16.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f2c5fb249caa17b94e2b9278b36a05ce03d3180e6da0c4c3b3ce5b2788f30eed"},
{file = "pyarrow-16.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:e6b6d3cd35fbb93b70ade1336022cc1147b95ec6af7d36906ca7fe432eb09710"},
{file = "pyarrow-16.1.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:18da9b76a36a954665ccca8aa6bd9f46c1145f79c0bb8f4f244f5f8e799bca55"},
{file = "pyarrow-16.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99f7549779b6e434467d2aa43ab2b7224dd9e41bdde486020bae198978c9e05e"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f07fdffe4fd5b15f5ec15c8b64584868d063bc22b86b46c9695624ca3505b7b4"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfe389a08ea374972bd4065d5f25d14e36b43ebc22fc75f7b951f24378bf0b5"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b20bd67c94b3a2ea0a749d2a5712fc845a69cb5d52e78e6449bbd295611f3aa"},
{file = "pyarrow-16.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ba8ac20693c0bb0bf4b238751d4409e62852004a8cf031c73b0e0962b03e45e3"},
{file = "pyarrow-16.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:31a1851751433d89a986616015841977e0a188662fcffd1a5677453f1df2de0a"},
{file = "pyarrow-16.1.0.tar.gz", hash = "sha256:15fbb22ea96d11f0b5768504a3f961edab25eaf4197c341720c4a387f6c60315"},
{file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e21488d5cfd3d8b500b3238a6c4b075efabc18f0f6d80b29239737ebd69caa6c"},
{file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b516dad76f258a702f7ca0250885fc93d1fa5ac13ad51258e39d402bd9e2e1e4"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f443122c8e31f4c9199cb23dca29ab9427cef990f283f80fe15b8e124bcc49b"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a03da7f2758645d17b7b4f83c8bffeae5bbb7f974523fe901f36288d2eab71"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ba17845efe3aa358ec266cf9cc2800fa73038211fb27968bfa88acd09261a470"},
{file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c35813c11a059056a22a3bef520461310f2f7eea5c8a11ef9de7062a23f8d56"},
{file = "pyarrow-18.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9736ba3c85129d72aefa21b4f3bd715bc4190fe4426715abfff90481e7d00812"},
{file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854"},
{file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe"},
{file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0"},
{file = "pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a"},
{file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f3a76670b263dc41d0ae877f09124ab96ce10e4e48f3e3e4257273cee61ad0d"},
{file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:da31fbca07c435be88a0c321402c4e31a2ba61593ec7473630769de8346b54ee"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543ad8459bc438efc46d29a759e1079436290bd583141384c6f7a1068ed6f992"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0743e503c55be0fdb5c08e7d44853da27f19dc854531c0570f9f394ec9671d54"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d4b3d2a34780645bed6414e22dda55a92e0fcd1b8a637fba86800ad737057e33"},
{file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c52f81aa6f6575058d8e2c782bf79d4f9fdc89887f16825ec3a66607a5dd8e30"},
{file = "pyarrow-18.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ad4892617e1a6c7a551cfc827e072a633eaff758fa09f21c4ee548c30bcaf99"},
{file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84e314d22231357d473eabec709d0ba285fa706a72377f9cc8e1cb3c8013813b"},
{file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f591704ac05dfd0477bb8f8e0bd4b5dc52c1cadf50503858dce3a15db6e46ff2"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acb7564204d3c40babf93a05624fc6a8ec1ab1def295c363afc40b0c9e66c191"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74de649d1d2ccb778f7c3afff6085bd5092aed4c23df9feeb45dd6b16f3811aa"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f96bd502cb11abb08efea6dab09c003305161cb6c9eafd432e35e76e7fa9b90c"},
{file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:36ac22d7782554754a3b50201b607d553a8d71b78cdf03b33c1125be4b52397c"},
{file = "pyarrow-18.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:25dbacab8c5952df0ca6ca0af28f50d45bd31c1ff6fcf79e2d120b4a65ee7181"},
{file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a276190309aba7bc9d5bd2933230458b3521a4317acfefe69a354f2fe59f2bc"},
{file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ad514dbfcffe30124ce655d72771ae070f30bf850b48bc4d9d3b25993ee0e386"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aebc13a11ed3032d8dd6e7171eb6e86d40d67a5639d96c35142bd568b9299324"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cf5c05f3cee251d80e98726b5c7cc9f21bab9e9783673bac58e6dfab57ecc8"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:11b676cd410cf162d3f6a70b43fb9e1e40affbc542a1e9ed3681895f2962d3d9"},
{file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b76130d835261b38f14fc41fdfb39ad8d672afb84c447126b84d5472244cfaba"},
{file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b331e477e40f07238adc7ba7469c36b908f07c89b95dd4bd3a0ec84a3d1e21e"},
{file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2c4dd0c9010a25ba03e198fe743b1cc03cd33c08190afff371749c52ccbbaf76"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f97b31b4c4e21ff58c6f330235ff893cc81e23da081b1a4b1c982075e0ed4e9"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a4813cb8ecf1809871fd2d64a8eff740a1bd3691bbe55f01a3cf6c5ec869754"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:05a5636ec3eb5cc2a36c6edb534a38ef57b2ab127292a716d00eabb887835f1e"},
{file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73eeed32e724ea3568bb06161cad5fa7751e45bc2228e33dcb10c614044165c7"},
{file = "pyarrow-18.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:a1880dd6772b685e803011a6b43a230c23b566859a6e0c9a276c1e0faf4f4052"},
{file = "pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73"},
]
[package.dependencies]
numpy = ">=1.16.6"
[package.extras]
test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"]
[[package]]
name = "pyarrow"
version = "22.0.0"
description = "Python library for Apache Arrow"
optional = false
python-versions = ">=3.10"
groups = ["powermon"]
markers = "python_version == \"3.14\""
files = [
{file = "pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88"},
{file = "pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace"},
{file = "pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce"},
{file = "pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48"},
{file = "pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340"},
{file = "pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653"},
{file = "pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84"},
{file = "pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a"},
{file = "pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e"},
{file = "pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215"},
{file = "pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d"},
{file = "pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8"},
{file = "pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016"},
{file = "pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c"},
{file = "pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d"},
{file = "pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8"},
{file = "pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5"},
{file = "pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe"},
{file = "pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e"},
{file = "pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9"},
{file = "pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d"},
{file = "pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a"},
{file = "pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901"},
{file = "pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691"},
{file = "pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a"},
{file = "pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6"},
{file = "pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941"},
{file = "pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145"},
{file = "pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1"},
{file = "pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f"},
{file = "pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d"},
{file = "pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f"},
{file = "pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746"},
{file = "pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95"},
{file = "pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc"},
{file = "pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d"},
{file = "pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9"},
{file = "pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7"},
{file = "pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde"},
{file = "pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc"},
{file = "pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0"},
{file = "pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730"},
{file = "pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2"},
{file = "pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70"},
{file = "pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754"},
{file = "pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91"},
{file = "pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c"},
{file = "pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80"},
{file = "pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae"},
{file = "pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9"},
]
[[package]]
name = "pyarrow-stubs"
@@ -6117,4 +6185,4 @@ tunnel = ["pytap2"]
[metadata]
lock-version = "2.1"
python-versions = "^3.9,<3.15"
content-hash = "674308d6eb7c3730031cc3e73c98b2413c7f59002a9317bfad387bc34a17c64d"
content-hash = "7dd0dc28e13f7569f1a8cc28453d5983a6f742790d9328bafe86d1a2914d88ad"
+5 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "meshtastic"
version = "2.7.10"
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"
@@ -51,7 +51,10 @@ optional = true
riden = { git = "https://github.com/geeksville/riden.git#1.2.1" }
ppk2-api = "^0.9.2"
parse = "^1.20.2"
pyarrow = "^16.1.0"
pyarrow = [
{ version = "^18.0.0", python = "<3.14" },
{ version = "^22.0.0", python = ">=3.14" }
]
platformdirs = "^4.2.2"
# If you are doing power analysis you might want these extra devtools
+2 -1
View File
@@ -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