mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-22 03:02:08 +10:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ccb2c918f | ||
|
|
6b49b7228a | ||
|
|
989b8620ba | ||
|
|
173ac58ed7 | ||
|
|
83adfd417a | ||
|
|
a0951f23c3 | ||
|
|
e19f531059 | ||
|
|
24d64a0013 | ||
|
|
3a87fc82c0 | ||
|
|
478444eb02 | ||
|
|
ad23c42fcc | ||
|
|
195f42af82 | ||
|
|
089af764ec | ||
|
|
7be5426f34 | ||
|
|
9ec63b5eb2 | ||
|
|
22a9346fe0 | ||
|
|
c194666885 | ||
|
|
c0e52e6e1c | ||
|
|
11df30a85f | ||
|
|
8d8ff21e7c | ||
|
|
9c72767c01 | ||
|
|
c0425d7444 | ||
|
|
d7db0f5829 | ||
|
|
d7cb5d7885 | ||
|
|
ffd144da83 | ||
|
|
1e8c9e7071 | ||
|
|
3c83e01d0e | ||
|
|
dc3947117e | ||
|
|
87d0850f95 | ||
|
|
5831952636 | ||
|
|
a6d61413c3 | ||
|
|
e393a5c410 | ||
|
|
8f2ecbdb4d | ||
|
|
6c28d11cee | ||
|
|
69f1b502cc | ||
|
|
3a498fbbe4 | ||
|
|
8f7dea0580 | ||
|
|
322f0262a8 | ||
|
|
00762393cf | ||
|
|
866c89f801 | ||
|
|
8c248927c8 | ||
|
|
188d895eb4 | ||
|
|
6f476f3475 | ||
|
|
50e1fe88e8 | ||
|
|
f9bedd8adc | ||
|
|
5a619c9031 | ||
|
|
8d3f9222ff | ||
|
|
f5335f22ea | ||
|
|
4a3f449555 | ||
|
|
608713470b | ||
|
|
f475be19c6 | ||
|
|
27cc76d5ed |
@@ -13,6 +13,7 @@ Meshtastic is an open-source LoRa mesh networking project for long-range, low-po
|
|||||||
- **RP2040/RP2350** - Raspberry Pi Pico variants
|
- **RP2040/RP2350** - Raspberry Pi Pico variants
|
||||||
- **STM32WL** - STM32 with integrated LoRa
|
- **STM32WL** - STM32 with integrated LoRa
|
||||||
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
|
- **Linux/Portduino** - Native Linux builds (Raspberry Pi, etc.)
|
||||||
|
- **macOS native** - Headless `meshtasticd` on Apple Silicon / x86_64; see `variants/native/portduino/platformio.ini` for Homebrew prereqs + CH341 LoRa setup
|
||||||
|
|
||||||
### Supported Radio Chips
|
### Supported Radio Chips
|
||||||
|
|
||||||
@@ -369,7 +370,7 @@ To reduce avoidable agent mistakes, assume these tools are available (or install
|
|||||||
- **Required CLI basics**: `bash`, `git`, `find`, `grep`, `sed`, `awk`, `xargs`
|
- **Required CLI basics**: `bash`, `git`, `find`, `grep`, `sed`, `awk`, `xargs`
|
||||||
- **Strongly recommended**: `rg` (ripgrep) for fast file/text search, `jq` for JSON processing
|
- **Strongly recommended**: `rg` (ripgrep) for fast file/text search, `jq` for JSON processing
|
||||||
- **Build/test tools**: `python3`, `pip`, virtualenv (`python3 -m venv`), `platformio` (`pio`)
|
- **Build/test tools**: `python3`, `pip`, virtualenv (`python3 -m venv`), `platformio` (`pio`)
|
||||||
- **Containerized native testing**: `docker` (especially important on macOS / non-Linux hosts)
|
- **Containerized native testing**: `docker` (fallback for non-Linux hosts; macOS can also build natively via `pio run -e native-macos`)
|
||||||
|
|
||||||
Fallback expectations for agents:
|
Fallback expectations for agents:
|
||||||
|
|
||||||
@@ -388,6 +389,7 @@ Build commands:
|
|||||||
pio run -e tbeam # Build specific target
|
pio run -e tbeam # Build specific target
|
||||||
pio run -e tbeam -t upload # Build and upload
|
pio run -e tbeam -t upload # Build and upload
|
||||||
pio run -e native # Build native/Linux version
|
pio run -e native # Build native/Linux version
|
||||||
|
pio run -e native-macos # Build headless macOS meshtasticd (Homebrew prereqs in variants/native/portduino/platformio.ini)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build Manifest
|
### Build Manifest
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Build MacOS Binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
macos_ver:
|
||||||
|
required: false
|
||||||
|
default: "26" # ARM64
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-MacOS:
|
||||||
|
runs-on: macos-${{ inputs.macos_ver }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
|
||||||
|
|
||||||
|
- name: Get release version string
|
||||||
|
run: |
|
||||||
|
echo "long=$(./bin/buildinfo.py long)" >> $GITHUB_OUTPUT
|
||||||
|
id: version
|
||||||
|
|
||||||
|
- name: Build for MacOS
|
||||||
|
run: |
|
||||||
|
platformio run -e native-macos
|
||||||
|
env:
|
||||||
|
PKG_VERSION: ${{ steps.version.outputs.long }}
|
||||||
|
# Errors in this step should not fail the entire workflow while MacOS support is in development.
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: List output files
|
||||||
|
run: ls -lah .pio/build/native-macos/
|
||||||
|
|
||||||
|
- name: Store binaries as an artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: firmware-macos-${{ inputs.macos_ver }}-${{ steps.version.outputs.long }}
|
||||||
|
overwrite: true
|
||||||
|
path: |
|
||||||
|
.pio/build/native-macos/meshtasticd
|
||||||
@@ -73,7 +73,9 @@ jobs:
|
|||||||
- name: Sanitize platform string
|
- name: Sanitize platform string
|
||||||
id: sanitize_platform
|
id: sanitize_platform
|
||||||
# Replace slashes with underscores
|
# Replace slashes with underscores
|
||||||
run: echo "cleaned_platform=${{ inputs.platform }}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
env:
|
||||||
|
plat: ${{ inputs.platform }}
|
||||||
|
run: echo "cleaned_platform=${plat}" | sed 's/\//_/g' >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Docker login
|
- name: Docker login
|
||||||
if: ${{ inputs.push }}
|
if: ${{ inputs.push }}
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
docker-debian-riscv64:
|
||||||
|
uses: ./.github/workflows/docker_build.yml
|
||||||
|
with:
|
||||||
|
distro: debian
|
||||||
|
platform: linux/riscv64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
push: true
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
docker-alpine-amd64:
|
docker-alpine-amd64:
|
||||||
uses: ./.github/workflows/docker_build.yml
|
uses: ./.github/workflows/docker_build.yml
|
||||||
with:
|
with:
|
||||||
@@ -70,16 +79,27 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
docker-alpine-riscv64:
|
||||||
|
uses: ./.github/workflows/docker_build.yml
|
||||||
|
with:
|
||||||
|
distro: alpine
|
||||||
|
platform: linux/riscv64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
push: true
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
docker-manifest:
|
docker-manifest:
|
||||||
needs:
|
needs:
|
||||||
# Debian
|
# Debian
|
||||||
- docker-debian-amd64
|
- docker-debian-amd64
|
||||||
- docker-debian-arm64
|
- docker-debian-arm64
|
||||||
- docker-debian-armv7
|
- docker-debian-armv7
|
||||||
|
- docker-debian-riscv64
|
||||||
# Alpine
|
# Alpine
|
||||||
- docker-alpine-amd64
|
- docker-alpine-amd64
|
||||||
- docker-alpine-arm64
|
- docker-alpine-arm64
|
||||||
- docker-alpine-armv7
|
- docker-alpine-armv7
|
||||||
|
- docker-alpine-riscv64
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -162,6 +182,7 @@ jobs:
|
|||||||
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
|
||||||
|
meshtastic/meshtasticd@${{ needs.docker-debian-riscv64.outputs.digest }}
|
||||||
|
|
||||||
- name: Docker meta (Alpine)
|
- name: Docker meta (Alpine)
|
||||||
id: meta_alpine
|
id: meta_alpine
|
||||||
@@ -182,3 +203,4 @@ jobs:
|
|||||||
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.outputs.digest }}
|
||||||
meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }}
|
meshtastic/meshtasticd@${{ needs.docker-alpine-armv7.outputs.digest }}
|
||||||
|
meshtastic/meshtasticd@${{ needs.docker-alpine-riscv64.outputs.digest }}
|
||||||
|
|||||||
@@ -116,6 +116,20 @@ jobs:
|
|||||||
build_location: local
|
build_location: local
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
MacOS:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
macos_ver:
|
||||||
|
- "26" # ARM64
|
||||||
|
# - '26-intel' # x86_64
|
||||||
|
- "15" # ARM64
|
||||||
|
# - '15-intel' # x86_64
|
||||||
|
uses: ./.github/workflows/build_macos_bin.yml
|
||||||
|
with:
|
||||||
|
macos_ver: ${{ matrix.macos_ver }}
|
||||||
|
# secrets: inherit
|
||||||
|
|
||||||
package-pio-deps-native-tft:
|
package-pio-deps-native-tft:
|
||||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
||||||
uses: ./.github/workflows/package_pio_deps.yml
|
uses: ./.github/workflows/package_pio_deps.yml
|
||||||
@@ -286,6 +300,7 @@ jobs:
|
|||||||
- gather-artifacts
|
- gather-artifacts
|
||||||
- build-debian-src
|
- build-debian-src
|
||||||
- package-pio-deps-native-tft
|
- package-pio-deps-native-tft
|
||||||
|
# - MacOS
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|||||||
+3
-3
@@ -4,12 +4,12 @@ cli:
|
|||||||
plugins:
|
plugins:
|
||||||
sources:
|
sources:
|
||||||
- id: trunk
|
- id: trunk
|
||||||
ref: v1.7.6
|
ref: v1.8.0
|
||||||
uri: https://github.com/trunk-io/plugins
|
uri: https://github.com/trunk-io/plugins
|
||||||
lint:
|
lint:
|
||||||
enabled:
|
enabled:
|
||||||
- checkov@3.2.525
|
- checkov@3.2.525
|
||||||
- renovate@43.142.0
|
- renovate@43.150.0
|
||||||
- prettier@3.8.3
|
- prettier@3.8.3
|
||||||
- trufflehog@3.95.2
|
- trufflehog@3.95.2
|
||||||
- yamllint@1.38.0
|
- yamllint@1.38.0
|
||||||
@@ -36,7 +36,7 @@ lint:
|
|||||||
- bin/**
|
- bin/**
|
||||||
runtimes:
|
runtimes:
|
||||||
enabled:
|
enabled:
|
||||||
- python@3.10.8
|
- python@3.14.4
|
||||||
- go@1.21.0
|
- go@1.21.0
|
||||||
- node@22.16.0
|
- node@22.16.0
|
||||||
actions:
|
actions:
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ This file (`AGENTS.md`) is a short pointer + quick reference for agents that don
|
|||||||
## Quick command reference
|
## Quick command reference
|
||||||
|
|
||||||
| Action | Command |
|
| Action | Command |
|
||||||
| -------------------------------- | ----------------------------------------------------------------------------------- |
|
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||||
|
| Build native macOS host binary | `pio run -e native-macos` (Homebrew prereqs + CH341 LoRa setup in `variants/native/portduino/platformio.ini`) |
|
||||||
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
| Clean + rebuild | `pio run -e <env> -t clean && pio run -e <env>` |
|
||||||
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
| Flash a device | `pio run -e <env> -t upload --upload-port <port>` (or use the `pio_flash` MCP tool) |
|
||||||
| Run firmware unit tests (native) | `pio test -e native` |
|
| Run firmware unit tests (native) | `pio test -e native` |
|
||||||
@@ -121,6 +122,7 @@ Sequence these; don't parallelize on the same port.
|
|||||||
- **Device fully wedged (no DFU)?** `mcp__meshtastic__uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles it via USB hub PPPS. Needs `uhubctl` installed (`brew install uhubctl` / `apt install uhubctl`); on Linux without udev rules, permission errors fail fast, so use `sudo uhubctl` yourself or configure udev access.
|
- **Device fully wedged (no DFU)?** `mcp__meshtastic__uhubctl_cycle(role="nrf52", confirm=True)` hard-power-cycles it via USB hub PPPS. Needs `uhubctl` installed (`brew install uhubctl` / `apt install uhubctl`); on Linux without udev rules, permission errors fail fast, so use `sudo uhubctl` yourself or configure udev access.
|
||||||
- **Port busy?** `lsof <port>` to find the holder. Usually a stale `pio device monitor` or zombie `meshtastic_mcp` process. Kill it.
|
- **Port busy?** `lsof <port>` to find the holder. Usually a stale `pio device monitor` or zombie `meshtastic_mcp` process. Kill it.
|
||||||
- **Multiple MCP servers running?** `ps aux | grep meshtastic_mcp` — zombies hold ports. Kill all but the one your host spawned.
|
- **Multiple MCP servers running?** `ps aux | grep meshtastic_mcp` — zombies hold ports. Kill all but the one your host spawned.
|
||||||
|
- **macOS: `LIBUSB_ERROR_BUSY` on a CH341 LoRa adapter?** A third-party WCH `CH34xVCPDriver` is claiming interface 0. Find the bundle ID with `ioreg -p IOUSB -l -w 0 | grep -B2 -A30 0x5512`, then `sudo kmutil unload -b <bundleID>`. Apple's bundled CH34x kext targets the CH340 UART (PID 0x7523), not the SPI bridge — it's never the culprit.
|
||||||
|
|
||||||
## Environment variables (test harness)
|
## Environment variables (test harness)
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -3,7 +3,8 @@
|
|||||||
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
# trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions
|
||||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||||
|
|
||||||
FROM python:3.14-alpine3.22 AS builder
|
# Ensure the Alpine version is updated in both stages of the container!
|
||||||
|
FROM python:3.14-alpine3.23 AS builder
|
||||||
ARG PIO_ENV=native
|
ARG PIO_ENV=native
|
||||||
ENV PIP_ROOT_USER_ACTION=ignore
|
ENV PIP_ROOT_USER_ACTION=ignore
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,941 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import termios
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import tty
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, TextIO
|
||||||
|
|
||||||
|
|
||||||
|
START1 = 0x94
|
||||||
|
START2 = 0xC3
|
||||||
|
HEADER_LEN = 4
|
||||||
|
DEFAULT_API_PORT = 4403
|
||||||
|
DEFAULT_HOP_LIMIT = 0
|
||||||
|
LOCAL_ESCAPE_BYTE = b"\x1d" # Ctrl+]
|
||||||
|
MISSING_SEQ_RETRY_INTERVAL_SEC = 1.0
|
||||||
|
INPUT_BATCH_WINDOW_SEC = .5
|
||||||
|
INPUT_BATCH_MAX_BYTES = 64
|
||||||
|
HEARTBEAT_IDLE_DELAY_SEC = 5.0
|
||||||
|
HEARTBEAT_REPEAT_SEC = 15.0
|
||||||
|
HEARTBEAT_POLL_INTERVAL_SEC = 0.25
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Tiny DMShell client for Meshtastic native TCP API",
|
||||||
|
epilog=(
|
||||||
|
"Examples:\n"
|
||||||
|
" bin/dmshell_client.py --to !170896f7\n"
|
||||||
|
" bin/dmshell_client.py --to 0x170896f7 --command 'uname -a' --command 'id'"
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument("--host", default="127.0.0.1", help="meshtasticd API host")
|
||||||
|
parser.add_argument("--port", type=int, default=DEFAULT_API_PORT, help="meshtasticd API port")
|
||||||
|
parser.add_argument(
|
||||||
|
"--serial",
|
||||||
|
nargs="?",
|
||||||
|
const="auto",
|
||||||
|
default=None,
|
||||||
|
help="use USB serial transport (optionally provide device path, default: auto-detect)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--baud", type=int, default=115200, help="serial baud rate when using --serial")
|
||||||
|
parser.add_argument("--to", required=True, help="destination node number, e.g. !170896f7 or 0x170896f7")
|
||||||
|
parser.add_argument("--channel", type=int, default=0, help="channel index to use")
|
||||||
|
parser.add_argument("--cols", type=int, default=None, help="initial terminal columns (default: detect local terminal)")
|
||||||
|
parser.add_argument("--rows", type=int, default=None, help="initial terminal rows (default: detect local terminal)")
|
||||||
|
parser.add_argument("--command", action="append", default=[], help="send a command line after opening")
|
||||||
|
parser.add_argument("--close-after", type=float, default=2.0, help="seconds to wait before closing in command mode")
|
||||||
|
parser.add_argument("--timeout", type=float, default=10.0, help="seconds to wait for API/session events")
|
||||||
|
parser.add_argument("--verbose", action="store_true", help="print extra protocol events")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_proto_modules() -> object:
|
||||||
|
try:
|
||||||
|
import google.protobuf # noqa: F401
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("python package 'protobuf' is required to run this client") from exc
|
||||||
|
|
||||||
|
protoc = shutil.which("protoc")
|
||||||
|
if not protoc:
|
||||||
|
raise SystemExit("'protoc' is required to generate temporary Python protobuf bindings")
|
||||||
|
|
||||||
|
out_dir = Path(tempfile.mkdtemp(prefix="meshtastic_dmshell_proto_"))
|
||||||
|
proto_dir = repo_root() / "protobufs"
|
||||||
|
|
||||||
|
# Compile all required protos for DMShell client (mesh and dependencies)
|
||||||
|
# Excludes nanopb.proto and other complex build artifacts
|
||||||
|
required_protos = [
|
||||||
|
"mesh.proto",
|
||||||
|
"channel.proto",
|
||||||
|
"config.proto",
|
||||||
|
"device_ui.proto",
|
||||||
|
"module_config.proto",
|
||||||
|
"atak.proto",
|
||||||
|
"portnums.proto",
|
||||||
|
"telemetry.proto",
|
||||||
|
"xmodem.proto",
|
||||||
|
]
|
||||||
|
proto_files = [proto_dir / "meshtastic" / name for name in required_protos]
|
||||||
|
for pf in proto_files:
|
||||||
|
if not pf.exists():
|
||||||
|
raise SystemExit(f"could not find required proto file: {pf}")
|
||||||
|
|
||||||
|
# Create __init__.py to make meshtastic a package
|
||||||
|
(out_dir / "meshtastic").mkdir(exist_ok=True)
|
||||||
|
(out_dir / "meshtastic" / "__init__.py").touch()
|
||||||
|
|
||||||
|
# Build protoc command with just the meshtastic proto directory as include path
|
||||||
|
# protoc will use its built-in includes for standard google protobuf types
|
||||||
|
cmd = [protoc, f"-I{proto_dir}", f"--python_out={out_dir}", *[str(path) for path in proto_files]]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"protoc stderr: {result.stderr}", file=sys.stderr)
|
||||||
|
print(f"protoc stdout: {result.stdout}", file=sys.stderr)
|
||||||
|
print(f"protoc command: {' '.join(cmd)}", file=sys.stderr)
|
||||||
|
raise SystemExit(f"protoc failed with return code {result.returncode}")
|
||||||
|
|
||||||
|
# Create _pb2_grpc module stub if not present (protoc 3.20+)
|
||||||
|
mesh_pb2_file = out_dir / "meshtastic" / "mesh_pb2.py"
|
||||||
|
if not mesh_pb2_file.exists():
|
||||||
|
raise SystemExit(f"protoc did not generate mesh_pb2.py in {out_dir / 'meshtastic'}")
|
||||||
|
|
||||||
|
sys.path.insert(0, str(out_dir))
|
||||||
|
try:
|
||||||
|
from meshtastic import mesh_pb2, portnums_pb2 # type: ignore
|
||||||
|
except ImportError as exc:
|
||||||
|
print(f"Failed to import protobuf modules. Output dir contents:", file=sys.stderr)
|
||||||
|
for item in (out_dir / "meshtastic").iterdir():
|
||||||
|
print(f" {item.name}", file=sys.stderr)
|
||||||
|
raise SystemExit(f"could not import meshtastic proto modules: {exc}") from exc
|
||||||
|
|
||||||
|
# Return an object that has both modules accessible
|
||||||
|
class ProtoModules:
|
||||||
|
pass
|
||||||
|
|
||||||
|
pb2 = ProtoModules()
|
||||||
|
pb2.mesh = mesh_pb2
|
||||||
|
pb2.portnums = portnums_pb2
|
||||||
|
return pb2
|
||||||
|
|
||||||
|
|
||||||
|
def parse_node_num(raw: str) -> int:
|
||||||
|
value = raw.strip()
|
||||||
|
if value.startswith("!"):
|
||||||
|
value = value[1:]
|
||||||
|
if value.lower().startswith("0x"):
|
||||||
|
return int(value, 16)
|
||||||
|
if any(ch in "abcdefABCDEF" for ch in value):
|
||||||
|
return int(value, 16)
|
||||||
|
return int(value, 10)
|
||||||
|
|
||||||
|
|
||||||
|
class SerialTransport:
|
||||||
|
def __init__(self, serial_obj):
|
||||||
|
self._serial = serial_obj
|
||||||
|
|
||||||
|
def recv(self, length: int) -> bytes:
|
||||||
|
return self._serial.read(length)
|
||||||
|
|
||||||
|
def sendall(self, data: bytes) -> None:
|
||||||
|
self._serial.write(data)
|
||||||
|
self._serial.flush()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._serial.close()
|
||||||
|
|
||||||
|
|
||||||
|
def detect_meshtastic_serial_port() -> str:
|
||||||
|
try:
|
||||||
|
from serial.tools import list_ports
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("python package 'pyserial' is required for --serial mode") from exc
|
||||||
|
|
||||||
|
ports = list(list_ports.comports())
|
||||||
|
if not ports:
|
||||||
|
raise SystemExit("no serial ports found for --serial mode")
|
||||||
|
|
||||||
|
scored: list[tuple[int, str]] = []
|
||||||
|
for port in ports:
|
||||||
|
text = " ".join(
|
||||||
|
filter(
|
||||||
|
None,
|
||||||
|
[port.device, port.description, port.manufacturer, port.product, port.hwid],
|
||||||
|
)
|
||||||
|
).lower()
|
||||||
|
score = 0
|
||||||
|
if "meshtastic" in text:
|
||||||
|
score += 100
|
||||||
|
if "lora" in text or "mesh" in text:
|
||||||
|
score += 10
|
||||||
|
if "ttyacm" in (port.device or "").lower() or "ttyusb" in (port.device or "").lower():
|
||||||
|
score += 1
|
||||||
|
scored.append((score, port.device))
|
||||||
|
|
||||||
|
scored.sort(reverse=True)
|
||||||
|
best_score, best_device = scored[0]
|
||||||
|
if best_score <= 0 and len(scored) > 1:
|
||||||
|
raise SystemExit(
|
||||||
|
"could not confidently auto-detect a Meshtastic serial port; pass --serial /dev/ttyXXX explicitly"
|
||||||
|
)
|
||||||
|
return best_device
|
||||||
|
|
||||||
|
|
||||||
|
def open_transport(args: argparse.Namespace):
|
||||||
|
if args.serial is None:
|
||||||
|
sock = socket.create_connection((args.host, args.port), timeout=args.timeout)
|
||||||
|
sock.settimeout(None)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
serial_path = args.serial
|
||||||
|
if serial_path == "auto":
|
||||||
|
serial_path = detect_meshtastic_serial_port()
|
||||||
|
print(f"[dmshell] using serial port {serial_path}", file=sys.stderr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit("python package 'pyserial' is required for --serial mode") from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
serial_obj = serial.Serial(serial_path, baudrate=args.baud, timeout=None, write_timeout=2)
|
||||||
|
except Exception as exc:
|
||||||
|
raise SystemExit(f"failed to open serial device {serial_path}: {exc}") from exc
|
||||||
|
|
||||||
|
return SerialTransport(serial_obj)
|
||||||
|
|
||||||
|
|
||||||
|
def recv_exact(transport, length: int) -> bytes:
|
||||||
|
chunks = bytearray()
|
||||||
|
while len(chunks) < length:
|
||||||
|
piece = transport.recv(length - len(chunks))
|
||||||
|
if not piece:
|
||||||
|
raise ConnectionError("connection closed by transport")
|
||||||
|
chunks.extend(piece)
|
||||||
|
return bytes(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def detect_local_terminal_size() -> tuple[int, int]:
|
||||||
|
size = shutil.get_terminal_size(fallback=(100, 40))
|
||||||
|
cols = max(1, int(size.columns))
|
||||||
|
rows = max(1, int(size.lines))
|
||||||
|
return cols, rows
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_initial_terminal_size(cols_override: Optional[int], rows_override: Optional[int]) -> tuple[int, int]:
|
||||||
|
detected_cols, detected_rows = detect_local_terminal_size()
|
||||||
|
cols = detected_cols if cols_override is None else max(1, cols_override)
|
||||||
|
rows = detected_rows if rows_override is None else max(1, rows_override)
|
||||||
|
return cols, rows
|
||||||
|
|
||||||
|
|
||||||
|
def recv_stream_frame(transport) -> bytes:
|
||||||
|
while True:
|
||||||
|
start = recv_exact(transport, 1)[0]
|
||||||
|
if start != START1:
|
||||||
|
continue
|
||||||
|
if recv_exact(transport, 1)[0] != START2:
|
||||||
|
continue
|
||||||
|
header = recv_exact(transport, 2)
|
||||||
|
length = (header[0] << 8) | header[1]
|
||||||
|
return recv_exact(transport, length)
|
||||||
|
|
||||||
|
|
||||||
|
def send_stream_frame(transport, payload: bytes) -> None:
|
||||||
|
if len(payload) > 0xFFFF:
|
||||||
|
raise ValueError("payload too large for stream API")
|
||||||
|
header = bytes((START1, START2, (len(payload) >> 8) & 0xFF, len(payload) & 0xFF))
|
||||||
|
transport.sendall(header + payload)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SentShellFrame:
|
||||||
|
op: int
|
||||||
|
session_id: int
|
||||||
|
seq: int
|
||||||
|
ack_seq: int
|
||||||
|
payload: bytes = b""
|
||||||
|
cols: int = 0
|
||||||
|
rows: int = 0
|
||||||
|
flags: int = 0
|
||||||
|
last_tx_seq: int = 0
|
||||||
|
last_rx_seq: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SessionState:
|
||||||
|
pb2: object # ProtoModules with mesh and portnums attributes
|
||||||
|
target: int
|
||||||
|
channel: int
|
||||||
|
verbose: bool
|
||||||
|
session_id: int = field(default_factory=lambda: random.randint(1, 0x7FFFFFFF))
|
||||||
|
next_seq: int = 1
|
||||||
|
last_rx_seq: int = 0
|
||||||
|
next_expected_rx_seq: int = 1
|
||||||
|
highest_seen_rx_seq: int = 0
|
||||||
|
active: bool = False
|
||||||
|
stopped: bool = False
|
||||||
|
opened_event: threading.Event = field(default_factory=threading.Event)
|
||||||
|
closed_event: threading.Event = field(default_factory=threading.Event)
|
||||||
|
event_queue: "queue.Queue[str]" = field(default_factory=queue.Queue)
|
||||||
|
tx_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||||
|
socket_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||||
|
tx_history: deque[SentShellFrame] = field(default_factory=lambda: deque(maxlen=50))
|
||||||
|
pending_rx_frames: dict[int, object] = field(default_factory=dict)
|
||||||
|
last_requested_missing_seq: int = 0
|
||||||
|
last_missing_request_time: float = 0.0
|
||||||
|
requested_missing_seqs: set[int] = field(default_factory=set)
|
||||||
|
replay_log_lock: threading.Lock = field(default_factory=threading.Lock)
|
||||||
|
replay_log_file: Optional[TextIO] = None
|
||||||
|
replay_log_path: Optional[Path] = None
|
||||||
|
last_transport_activity_time: float = field(default_factory=time.monotonic)
|
||||||
|
last_heartbeat_sent_time: float = 0.0
|
||||||
|
|
||||||
|
def alloc_seq(self) -> int:
|
||||||
|
with self.tx_lock:
|
||||||
|
value = self.next_seq
|
||||||
|
self.next_seq += 1
|
||||||
|
return value
|
||||||
|
|
||||||
|
def current_ack_seq(self) -> int:
|
||||||
|
with self.tx_lock:
|
||||||
|
return self.last_rx_seq
|
||||||
|
|
||||||
|
def highest_sent_seq(self) -> int:
|
||||||
|
with self.tx_lock:
|
||||||
|
return max(0, self.next_seq - 1)
|
||||||
|
|
||||||
|
def note_outbound_packet(self, heartbeat: bool = False) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
now = time.monotonic()
|
||||||
|
if heartbeat:
|
||||||
|
self.last_heartbeat_sent_time = now
|
||||||
|
else:
|
||||||
|
self.last_transport_activity_time = now
|
||||||
|
|
||||||
|
def note_inbound_packet(self) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
self.last_transport_activity_time = time.monotonic()
|
||||||
|
|
||||||
|
def heartbeat_due(self) -> bool:
|
||||||
|
with self.tx_lock:
|
||||||
|
now = time.monotonic()
|
||||||
|
if (now - self.last_transport_activity_time) < HEARTBEAT_IDLE_DELAY_SEC:
|
||||||
|
return False
|
||||||
|
if self.last_heartbeat_sent_time <= self.last_transport_activity_time:
|
||||||
|
return True
|
||||||
|
return (now - self.last_heartbeat_sent_time) >= HEARTBEAT_REPEAT_SEC
|
||||||
|
|
||||||
|
def note_peer_reported_tx_seq(self, seq: int) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
if seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
|
||||||
|
def note_received_seq(self, seq: int) -> tuple[str, Optional[int]]:
|
||||||
|
with self.tx_lock:
|
||||||
|
if seq == 0:
|
||||||
|
return ("process", None)
|
||||||
|
if seq < self.next_expected_rx_seq:
|
||||||
|
if self.highest_seen_rx_seq >= self.next_expected_rx_seq:
|
||||||
|
return ("gap", self.next_expected_rx_seq)
|
||||||
|
return ("duplicate", None)
|
||||||
|
if seq > self.next_expected_rx_seq:
|
||||||
|
if seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
return ("gap", self.next_expected_rx_seq)
|
||||||
|
self.last_rx_seq = seq
|
||||||
|
self.next_expected_rx_seq = seq + 1
|
||||||
|
if self.last_requested_missing_seq != 0 and self.next_expected_rx_seq > self.last_requested_missing_seq:
|
||||||
|
self.last_requested_missing_seq = 0
|
||||||
|
if seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
if self.highest_seen_rx_seq < self.next_expected_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = 0
|
||||||
|
return ("process", None)
|
||||||
|
|
||||||
|
def remember_out_of_order_frame(self, shell) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
if shell.seq <= self.next_expected_rx_seq:
|
||||||
|
return
|
||||||
|
if shell.seq not in self.pending_rx_frames:
|
||||||
|
self.pending_rx_frames[shell.seq] = shell
|
||||||
|
if shell.seq > self.highest_seen_rx_seq:
|
||||||
|
self.highest_seen_rx_seq = shell.seq
|
||||||
|
|
||||||
|
def pop_next_buffered_frame(self):
|
||||||
|
with self.tx_lock:
|
||||||
|
return self.pending_rx_frames.pop(self.next_expected_rx_seq, None)
|
||||||
|
|
||||||
|
def pending_missing_seq(self) -> Optional[int]:
|
||||||
|
with self.tx_lock:
|
||||||
|
if self.highest_seen_rx_seq >= self.next_expected_rx_seq:
|
||||||
|
return self.next_expected_rx_seq
|
||||||
|
return None
|
||||||
|
|
||||||
|
def request_missing_seq_once(self) -> Optional[int]:
|
||||||
|
with self.tx_lock:
|
||||||
|
if self.highest_seen_rx_seq < self.next_expected_rx_seq:
|
||||||
|
return None
|
||||||
|
now = time.monotonic()
|
||||||
|
if (
|
||||||
|
self.last_requested_missing_seq == self.next_expected_rx_seq
|
||||||
|
and (now - self.last_missing_request_time) < MISSING_SEQ_RETRY_INTERVAL_SEC
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
self.last_requested_missing_seq = self.next_expected_rx_seq
|
||||||
|
self.last_missing_request_time = now
|
||||||
|
return self.last_requested_missing_seq
|
||||||
|
|
||||||
|
def set_receive_cursor(self, seq: int) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
self.last_rx_seq = seq
|
||||||
|
self.next_expected_rx_seq = seq + 1
|
||||||
|
self.highest_seen_rx_seq = seq
|
||||||
|
|
||||||
|
def open_replay_log(self, session_id: int) -> None:
|
||||||
|
with self.replay_log_lock:
|
||||||
|
if self.replay_log_file is not None:
|
||||||
|
return
|
||||||
|
path = Path.cwd() / f"{session_id:08x}.log"
|
||||||
|
self.replay_log_file = path.open("a", encoding="utf-8")
|
||||||
|
self.replay_log_path = path
|
||||||
|
self.replay_log_file.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} session_open session=0x{session_id:08x}\n")
|
||||||
|
self.replay_log_file.flush()
|
||||||
|
|
||||||
|
def log_replay_event(self, event: str, seq: int, detail: str = "") -> None:
|
||||||
|
with self.replay_log_lock:
|
||||||
|
if self.replay_log_file is None:
|
||||||
|
return
|
||||||
|
extra = f" {detail}" if detail else ""
|
||||||
|
self.replay_log_file.write(
|
||||||
|
f"{time.strftime('%Y-%m-%d %H:%M:%S')} {event} seq={seq}{extra}\n"
|
||||||
|
)
|
||||||
|
self.replay_log_file.flush()
|
||||||
|
|
||||||
|
def note_missing_seq_requested(self, seq: int, reason: str) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
self.requested_missing_seqs.add(seq)
|
||||||
|
self.log_replay_event("missing_requested", seq, f"reason={reason}")
|
||||||
|
|
||||||
|
def note_replayed_seq_received(self, seq: int) -> None:
|
||||||
|
with self.tx_lock:
|
||||||
|
was_requested = seq in self.requested_missing_seqs
|
||||||
|
if was_requested:
|
||||||
|
self.requested_missing_seqs.remove(seq)
|
||||||
|
if was_requested:
|
||||||
|
self.log_replay_event("replay_received", seq)
|
||||||
|
|
||||||
|
def close_replay_log(self) -> None:
|
||||||
|
with self.replay_log_lock:
|
||||||
|
if self.replay_log_file is None:
|
||||||
|
return
|
||||||
|
self.replay_log_file.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} session_close\n")
|
||||||
|
self.replay_log_file.flush()
|
||||||
|
self.replay_log_file.close()
|
||||||
|
self.replay_log_file = None
|
||||||
|
|
||||||
|
def remember_sent_frame(self, frame: SentShellFrame) -> None:
|
||||||
|
if frame.seq == 0 or frame.op == self.pb2.mesh.RemoteShell.ACK:
|
||||||
|
return
|
||||||
|
with self.tx_lock:
|
||||||
|
self.tx_history.append(frame)
|
||||||
|
|
||||||
|
def prune_sent_frames(self, ack_seq: int) -> None:
|
||||||
|
if ack_seq <= 0:
|
||||||
|
return
|
||||||
|
with self.tx_lock:
|
||||||
|
self.tx_history = deque((frame for frame in self.tx_history if frame.seq > ack_seq), maxlen=50)
|
||||||
|
|
||||||
|
def replay_frames_from(self, start_seq: int) -> list[SentShellFrame]:
|
||||||
|
with self.tx_lock:
|
||||||
|
return [frame for frame in self.tx_history if frame.seq >= start_seq]
|
||||||
|
|
||||||
|
|
||||||
|
def send_toradio(transport, toradio) -> None:
|
||||||
|
send_stream_frame(transport, toradio.SerializeToString())
|
||||||
|
|
||||||
|
|
||||||
|
def make_toradio_packet(pb2, state: SessionState, shell_msg) -> object:
|
||||||
|
packet = pb2.mesh.MeshPacket()
|
||||||
|
packet.id = random.randint(1, 0x7FFFFFFF)
|
||||||
|
packet.to = state.target
|
||||||
|
# The 'from' field is a reserved keyword in Python, so use setattr
|
||||||
|
setattr(packet, "from", 0)
|
||||||
|
packet.channel = state.channel
|
||||||
|
packet.hop_limit = DEFAULT_HOP_LIMIT
|
||||||
|
packet.want_ack = False
|
||||||
|
packet.decoded.portnum = pb2.portnums.REMOTE_SHELL_APP
|
||||||
|
packet.decoded.payload = shell_msg.SerializeToString()
|
||||||
|
packet.decoded.want_response = False
|
||||||
|
packet.decoded.dest = state.target
|
||||||
|
packet.decoded.source = 0
|
||||||
|
|
||||||
|
toradio = pb2.mesh.ToRadio()
|
||||||
|
toradio.packet.CopyFrom(packet)
|
||||||
|
return toradio
|
||||||
|
|
||||||
|
|
||||||
|
def send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state: SessionState,
|
||||||
|
op: int,
|
||||||
|
payload: bytes = b"",
|
||||||
|
cols: int = 0,
|
||||||
|
rows: int = 0,
|
||||||
|
session_id: Optional[int] = None,
|
||||||
|
ack_seq: Optional[int] = None,
|
||||||
|
seq: Optional[int] = None,
|
||||||
|
flags: int = 0,
|
||||||
|
last_tx_seq: int = 0,
|
||||||
|
last_rx_seq: int = 0,
|
||||||
|
remember: bool = True,
|
||||||
|
heartbeat: bool = False,
|
||||||
|
) -> int:
|
||||||
|
if seq is None:
|
||||||
|
seq = 0 if op == state.pb2.mesh.RemoteShell.ACK else state.alloc_seq()
|
||||||
|
if ack_seq is None:
|
||||||
|
ack_seq = state.current_ack_seq()
|
||||||
|
if session_id is None:
|
||||||
|
session_id = state.session_id
|
||||||
|
|
||||||
|
shell = state.pb2.mesh.RemoteShell()
|
||||||
|
shell.op = op
|
||||||
|
shell.session_id = session_id
|
||||||
|
shell.seq = seq
|
||||||
|
shell.ack_seq = ack_seq
|
||||||
|
shell.cols = cols
|
||||||
|
shell.rows = rows
|
||||||
|
shell.flags = flags
|
||||||
|
shell.last_tx_seq = last_tx_seq
|
||||||
|
shell.last_rx_seq = last_rx_seq
|
||||||
|
if payload:
|
||||||
|
shell.payload = payload
|
||||||
|
with state.socket_lock:
|
||||||
|
send_toradio(transport, make_toradio_packet(state.pb2, state, shell))
|
||||||
|
if remember:
|
||||||
|
state.remember_sent_frame(
|
||||||
|
SentShellFrame(
|
||||||
|
op=op,
|
||||||
|
session_id=session_id,
|
||||||
|
seq=seq,
|
||||||
|
ack_seq=ack_seq,
|
||||||
|
payload=payload,
|
||||||
|
cols=cols,
|
||||||
|
rows=rows,
|
||||||
|
flags=flags,
|
||||||
|
last_tx_seq=last_tx_seq,
|
||||||
|
last_rx_seq=last_rx_seq,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.note_outbound_packet(heartbeat=heartbeat)
|
||||||
|
return seq
|
||||||
|
|
||||||
|
|
||||||
|
def send_ack_frame(transport, state: SessionState, replay_from: Optional[int] = None) -> None:
|
||||||
|
send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state,
|
||||||
|
state.pb2.mesh.RemoteShell.ACK,
|
||||||
|
seq=0,
|
||||||
|
last_rx_seq=0 if replay_from is None else replay_from - 1,
|
||||||
|
remember=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def replay_frames_from(transport, state: SessionState, start_seq: int) -> None:
|
||||||
|
frame = next((f for f in state.replay_frames_from(start_seq) if f.seq == start_seq), None)
|
||||||
|
if frame is None:
|
||||||
|
#state.event_queue.put(f"replay unavailable from seq={start_seq}")
|
||||||
|
state.log_replay_event("replay_unavailable", start_seq)
|
||||||
|
return
|
||||||
|
state.log_replay_event("replay_sent", start_seq)
|
||||||
|
#state.event_queue.put(f"replay frame seq={start_seq}")
|
||||||
|
send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state,
|
||||||
|
frame.op,
|
||||||
|
payload=frame.payload,
|
||||||
|
cols=frame.cols,
|
||||||
|
rows=frame.rows,
|
||||||
|
session_id=frame.session_id,
|
||||||
|
ack_seq=frame.ack_seq,
|
||||||
|
seq=frame.seq,
|
||||||
|
flags=frame.flags,
|
||||||
|
last_tx_seq=frame.last_tx_seq,
|
||||||
|
last_rx_seq=frame.last_rx_seq,
|
||||||
|
remember=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_config_complete(transport, pb2, timeout: float, verbose: bool) -> None:
|
||||||
|
nonce = random.randint(1, 0x7FFFFFFF)
|
||||||
|
toradio = pb2.mesh.ToRadio()
|
||||||
|
toradio.want_config_id = nonce
|
||||||
|
send_toradio(transport, toradio)
|
||||||
|
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
while time.time() < deadline:
|
||||||
|
fromradio = pb2.mesh.FromRadio()
|
||||||
|
fromradio.ParseFromString(recv_stream_frame(transport))
|
||||||
|
variant = fromradio.WhichOneof("payload_variant")
|
||||||
|
if verbose and variant:
|
||||||
|
print(f"[api] fromradio {variant}", file=sys.stderr)
|
||||||
|
if variant == "config_complete_id" and fromradio.config_complete_id == nonce:
|
||||||
|
return
|
||||||
|
raise TimeoutError("timed out waiting for config handshake to complete")
|
||||||
|
|
||||||
|
|
||||||
|
def decode_shell_packet(state: SessionState, packet) -> Optional[object]:
|
||||||
|
if packet.WhichOneof("payload_variant") != "decoded":
|
||||||
|
return None
|
||||||
|
if packet.decoded.portnum != state.pb2.portnums.REMOTE_SHELL_APP:
|
||||||
|
return None
|
||||||
|
shell = state.pb2.mesh.RemoteShell()
|
||||||
|
shell.ParseFromString(packet.decoded.payload)
|
||||||
|
return shell
|
||||||
|
|
||||||
|
|
||||||
|
def reader_loop(transport, state: SessionState) -> None:
|
||||||
|
def handle_in_order_shell(shell) -> bool:
|
||||||
|
state.note_replayed_seq_received(shell.seq)
|
||||||
|
if shell.op == state.pb2.mesh.RemoteShell.OPEN_OK:
|
||||||
|
state.session_id = shell.session_id
|
||||||
|
state.open_replay_log(state.session_id)
|
||||||
|
state.set_receive_cursor(shell.seq)
|
||||||
|
state.active = True
|
||||||
|
state.opened_event.set()
|
||||||
|
state.event_queue.put(
|
||||||
|
f"opened session=0x{shell.session_id:08x} cols={shell.cols} rows={shell.rows}"
|
||||||
|
)
|
||||||
|
if state.replay_log_path is not None:
|
||||||
|
state.event_queue.put(f"replay log: {state.replay_log_path}")
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.OUTPUT:
|
||||||
|
if shell.payload:
|
||||||
|
sys.stdout.buffer.write(shell.payload)
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.ERROR:
|
||||||
|
message = shell.payload.decode("utf-8", errors="replace")
|
||||||
|
if state.replay_log_file is None:
|
||||||
|
state.open_replay_log(shell.session_id or state.session_id)
|
||||||
|
sanitized = message.replace("\n", "\\n")
|
||||||
|
state.log_replay_event("error_received", shell.seq, f"message={sanitized}")
|
||||||
|
state.event_queue.put(f"remote error: {message}")
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.CLOSED:
|
||||||
|
message = shell.payload.decode("utf-8", errors="replace")
|
||||||
|
state.event_queue.put(f"session closed: {message}")
|
||||||
|
state.closed_event.set()
|
||||||
|
state.active = False
|
||||||
|
return True
|
||||||
|
elif shell.op == state.pb2.mesh.RemoteShell.PONG:
|
||||||
|
remote_last_tx_seq = shell.last_tx_seq
|
||||||
|
remote_last_rx_seq = shell.last_rx_seq
|
||||||
|
local_latest_tx_seq = state.highest_sent_seq()
|
||||||
|
if remote_last_rx_seq != 0 and remote_last_rx_seq < local_latest_tx_seq:
|
||||||
|
replay_frames_from(transport, state, remote_last_rx_seq + 1)
|
||||||
|
if remote_last_tx_seq > state.current_ack_seq():
|
||||||
|
state.note_peer_reported_tx_seq(remote_last_tx_seq)
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "heartbeat_status")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
#state.event_queue.put("pong")
|
||||||
|
return False
|
||||||
|
|
||||||
|
while not state.stopped:
|
||||||
|
try:
|
||||||
|
fromradio = state.pb2.mesh.FromRadio()
|
||||||
|
fromradio.ParseFromString(recv_stream_frame(transport))
|
||||||
|
except Exception as exc:
|
||||||
|
if not state.stopped:
|
||||||
|
state.event_queue.put(f"connection error: {exc}")
|
||||||
|
state.closed_event.set()
|
||||||
|
return
|
||||||
|
|
||||||
|
variant = fromradio.WhichOneof("payload_variant")
|
||||||
|
if variant == "packet":
|
||||||
|
shell = decode_shell_packet(state, fromradio.packet)
|
||||||
|
if not shell:
|
||||||
|
continue
|
||||||
|
state.note_inbound_packet()
|
||||||
|
#state.prune_sent_frames(shell.ack_seq)
|
||||||
|
if shell.op == state.pb2.mesh.RemoteShell.ACK:
|
||||||
|
#state.event_queue.put("peer requested replay")
|
||||||
|
replay_from = shell.last_rx_seq + 1 if shell.last_rx_seq > 0 else None
|
||||||
|
if replay_from is not None:
|
||||||
|
#state.event_queue.put(f"peer requested replay from seq={replay_from}")
|
||||||
|
replay_frames_from(transport, state, replay_from)
|
||||||
|
continue
|
||||||
|
|
||||||
|
action, missing_from = state.note_received_seq(shell.seq)
|
||||||
|
if action == "duplicate":
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "duplicate")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
continue
|
||||||
|
if action == "gap":
|
||||||
|
state.remember_out_of_order_frame(shell)
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "gap")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if handle_in_order_shell(shell):
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
buffered_shell = state.pop_next_buffered_frame()
|
||||||
|
if buffered_shell is None:
|
||||||
|
break
|
||||||
|
buffered_action, _ = state.note_received_seq(buffered_shell.seq)
|
||||||
|
if buffered_action != "process":
|
||||||
|
state.remember_out_of_order_frame(buffered_shell)
|
||||||
|
break
|
||||||
|
if handle_in_order_shell(buffered_shell):
|
||||||
|
return
|
||||||
|
|
||||||
|
req = state.request_missing_seq_once()
|
||||||
|
if req is not None:
|
||||||
|
state.note_missing_seq_requested(req, "post_process_gap")
|
||||||
|
send_ack_frame(transport, state, replay_from=req)
|
||||||
|
elif state.verbose and variant:
|
||||||
|
state.event_queue.put(f"fromradio {variant}")
|
||||||
|
|
||||||
|
|
||||||
|
def drain_events(state: SessionState) -> None:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
event = state.event_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
return
|
||||||
|
print(f"[dmshell] {event}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def heartbeat_loop(transport, state: SessionState) -> None:
|
||||||
|
while not state.stopped and not state.closed_event.is_set():
|
||||||
|
if not state.active:
|
||||||
|
time.sleep(HEARTBEAT_POLL_INTERVAL_SEC)
|
||||||
|
continue
|
||||||
|
if state.heartbeat_due():
|
||||||
|
try:
|
||||||
|
send_shell_frame(
|
||||||
|
transport,
|
||||||
|
state,
|
||||||
|
state.pb2.mesh.RemoteShell.PING,
|
||||||
|
last_tx_seq=state.highest_sent_seq(),
|
||||||
|
last_rx_seq=state.current_ack_seq(),
|
||||||
|
remember=True,
|
||||||
|
heartbeat=True,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
if not state.stopped:
|
||||||
|
state.event_queue.put(f"heartbeat error: {exc}")
|
||||||
|
state.closed_event.set()
|
||||||
|
return
|
||||||
|
time.sleep(HEARTBEAT_POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
|
|
||||||
|
def run_command_mode(transport, state: SessionState, commands: list[str], close_after: float) -> None:
|
||||||
|
for command in commands:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.INPUT, (command + "\n").encode("utf-8"))
|
||||||
|
time.sleep(close_after)
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
state.closed_event.wait(timeout=close_after + 5.0)
|
||||||
|
|
||||||
|
|
||||||
|
def run_interactive_mode(transport, state: SessionState) -> None:
|
||||||
|
def read_local_command() -> str:
|
||||||
|
prompt = "\r\n[dmshell] local command (resume|close|ping|resize C R): "
|
||||||
|
sys.stderr.write(prompt)
|
||||||
|
sys.stderr.flush()
|
||||||
|
buf = bytearray()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ch = os.read(sys.stdin.fileno(), 1)
|
||||||
|
if not ch:
|
||||||
|
sys.stderr.write("\r\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
return "close"
|
||||||
|
|
||||||
|
b = ch[0]
|
||||||
|
if b in (10, 13):
|
||||||
|
sys.stderr.write("\r\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
return buf.decode("utf-8", errors="replace").strip()
|
||||||
|
|
||||||
|
if b in (8, 127):
|
||||||
|
if buf:
|
||||||
|
buf.pop()
|
||||||
|
sys.stderr.write("\b \b")
|
||||||
|
sys.stderr.flush()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if b < 32:
|
||||||
|
continue
|
||||||
|
|
||||||
|
buf.append(b)
|
||||||
|
sys.stderr.write(chr(b))
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
def handle_local_command(cmd: str) -> bool:
|
||||||
|
if cmd in ("", "resume"):
|
||||||
|
return True
|
||||||
|
if cmd == "close":
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
return False
|
||||||
|
if cmd == "ping":
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.PING)
|
||||||
|
return True
|
||||||
|
if cmd.startswith("resize "):
|
||||||
|
parts = cmd.split()
|
||||||
|
if len(parts) != 3:
|
||||||
|
state.event_queue.put("usage: resize COLS ROWS")
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
cols = int(parts[1])
|
||||||
|
rows = int(parts[2])
|
||||||
|
except ValueError:
|
||||||
|
state.event_queue.put("usage: resize COLS ROWS")
|
||||||
|
return True
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.RESIZE, cols=cols, rows=rows)
|
||||||
|
return True
|
||||||
|
|
||||||
|
state.event_queue.put(f"unknown local command: {cmd}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(
|
||||||
|
"Raw input mode active. All keys (including Ctrl+C/Ctrl+X) are sent to remote. Ctrl+] for local commands.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
# Fallback for non-TTY stdin: still send input as it arrives.
|
||||||
|
while not state.closed_event.is_set():
|
||||||
|
drain_events(state)
|
||||||
|
data = sys.stdin.buffer.read(INPUT_BATCH_MAX_BYTES)
|
||||||
|
if not data:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
break
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.INPUT, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_attrs = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
while not state.closed_event.is_set():
|
||||||
|
drain_events(state)
|
||||||
|
ready, _, _ = select.select([sys.stdin], [], [], 0.05)
|
||||||
|
if not ready:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = os.read(fd, 1)
|
||||||
|
if not data:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.CLOSE)
|
||||||
|
break
|
||||||
|
|
||||||
|
if data == LOCAL_ESCAPE_BYTE:
|
||||||
|
keep_running = handle_local_command(read_local_command())
|
||||||
|
if not keep_running:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Coalesce a short burst of bytes to reduce packet overhead for fast typing.
|
||||||
|
batched = bytearray(data)
|
||||||
|
enter_local_command = False
|
||||||
|
deadline = time.monotonic() + INPUT_BATCH_WINDOW_SEC
|
||||||
|
while len(batched) < INPUT_BATCH_MAX_BYTES:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
more_ready, _, _ = select.select([sys.stdin], [], [], remaining)
|
||||||
|
if not more_ready:
|
||||||
|
break
|
||||||
|
next_byte = os.read(fd, 1)
|
||||||
|
if not next_byte:
|
||||||
|
break
|
||||||
|
if next_byte == LOCAL_ESCAPE_BYTE:
|
||||||
|
enter_local_command = True
|
||||||
|
break
|
||||||
|
batched.extend(next_byte)
|
||||||
|
if next_byte == b'\r' or next_byte == b'\t':
|
||||||
|
break
|
||||||
|
deadline = time.monotonic() + INPUT_BATCH_WINDOW_SEC
|
||||||
|
|
||||||
|
if batched:
|
||||||
|
send_shell_frame(transport, state, state.pb2.mesh.RemoteShell.INPUT, bytes(batched))
|
||||||
|
|
||||||
|
if enter_local_command:
|
||||||
|
keep_running = handle_local_command(read_local_command())
|
||||||
|
if not keep_running:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
pb2 = load_proto_modules()
|
||||||
|
|
||||||
|
state = SessionState(
|
||||||
|
pb2=pb2,
|
||||||
|
target=parse_node_num(args.to),
|
||||||
|
channel=args.channel,
|
||||||
|
verbose=args.verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
cols, rows = resolve_initial_terminal_size(args.cols, args.rows)
|
||||||
|
|
||||||
|
transport = open_transport(args)
|
||||||
|
try:
|
||||||
|
wait_for_config_complete(transport, pb2, args.timeout, args.verbose)
|
||||||
|
|
||||||
|
reader = threading.Thread(target=reader_loop, args=(transport, state), daemon=True)
|
||||||
|
reader.start()
|
||||||
|
|
||||||
|
send_shell_frame(transport, state, pb2.mesh.RemoteShell.OPEN, cols=cols, rows=rows)
|
||||||
|
if not state.opened_event.wait(timeout=args.timeout):
|
||||||
|
raise SystemExit("timed out waiting for OPEN_OK from remote DMShell")
|
||||||
|
|
||||||
|
heartbeat = threading.Thread(target=heartbeat_loop, args=(transport, state), daemon=True)
|
||||||
|
heartbeat.start()
|
||||||
|
|
||||||
|
drain_events(state)
|
||||||
|
if args.command:
|
||||||
|
run_command_mode(transport, state, args.command, args.close_after)
|
||||||
|
else:
|
||||||
|
run_interactive_mode(transport, state)
|
||||||
|
|
||||||
|
state.stopped = True
|
||||||
|
drain_events(state)
|
||||||
|
reader.join(timeout=1.0)
|
||||||
|
heartbeat.join(timeout=1.0)
|
||||||
|
state.close_replay_log()
|
||||||
|
finally:
|
||||||
|
transport.close()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Vendored
+1
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
|
set -e
|
||||||
export DEBEMAIL="jbennett@incomsystems.biz"
|
export DEBEMAIL="jbennett@incomsystems.biz"
|
||||||
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
|
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
|
||||||
export PLATFORMIO_PACKAGES_DIR=pio/packages
|
export PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||||
|
|||||||
+1
-1
@@ -125,7 +125,7 @@ lib_deps =
|
|||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/728932970996ec91bdb93cb6dae29c2cb70c66e2.zip
|
https://github.com/meshtastic/device-ui/archive/4bf593a82100b911ff816dddf7158ffdee2114cd.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
|
|||||||
+1
-1
Submodule protobufs updated: 249a80855a...1d6f1a71ff
+65
-285
@@ -505,15 +505,12 @@ bool GPS::setup()
|
|||||||
int msglen = 0;
|
int msglen = 0;
|
||||||
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
|
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||||
if (probeTries < GPS_PROBETRIES) {
|
if (probeTries < GPS_PROBETRIES) {
|
||||||
bootString = "Probing GPS...";
|
|
||||||
gnssModel = probe(serialSpeeds[speedSelect]);
|
gnssModel = probe(serialSpeeds[speedSelect]);
|
||||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||||
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
||||||
speedSelect = 0;
|
speedSelect = 0;
|
||||||
++probeTries;
|
++probeTries;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
currentStep = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Rare Serial Speeds
|
// Rare Serial Speeds
|
||||||
@@ -525,8 +522,6 @@ bool GPS::setup()
|
|||||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
currentStep = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -543,133 +538,86 @@ bool GPS::setup()
|
|||||||
* t-beam-s3-core uses the same L76K GNSS module as t-echo.
|
* t-beam-s3-core uses the same L76K GNSS module as t-echo.
|
||||||
* Unlike t-echo, L76K uses 9600 baud rate for communication by default.
|
* Unlike t-echo, L76K uses 9600 baud rate for communication by default.
|
||||||
* */
|
* */
|
||||||
if (currentStep == 0) {
|
|
||||||
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
|
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
|
||||||
_serial_gps->write("$PCAS04,7*1E\r\n");
|
_serial_gps->write("$PCAS04,7*1E\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// only ask for RMC and GGA
|
// only ask for RMC and GGA
|
||||||
_serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n");
|
_serial_gps->write("$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
|
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
|
||||||
_serial_gps->write("$PCAS11,3*1E\r\n");
|
_serial_gps->write("$PCAS11,3*1E\r\n");
|
||||||
currentDelay = 250;
|
delay(250);
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_MTK_L76B) {
|
} else if (gnssModel == GNSS_MODEL_MTK_L76B) {
|
||||||
// Waveshare Pico-GPS hat uses the L76B with 9600 baud
|
// Waveshare Pico-GPS hat uses the L76B with 9600 baud
|
||||||
// Initialize the L76B Chip, use GPS + GLONASS
|
// Initialize the L76B Chip, use GPS + GLONASS
|
||||||
// See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29
|
// See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29
|
||||||
if (currentStep == 0) {
|
|
||||||
_serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n");
|
_serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n");
|
||||||
currentStep++;
|
|
||||||
currentDelay = 1000;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// Above command will reset the GPS and takes longer before it will accept new commands
|
// Above command will reset the GPS and takes longer before it will accept new commands
|
||||||
|
delay(1000);
|
||||||
// only ask for RMC and GGA (GNRMC and GNGGA)
|
// only ask for RMC and GGA (GNRMC and GNGGA)
|
||||||
// See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1
|
// See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1
|
||||||
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
|
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
// Enable SBAS
|
// Enable SBAS
|
||||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 3) {
|
|
||||||
// Enable PPS for 2D/3D fix only
|
// Enable PPS for 2D/3D fix only
|
||||||
_serial_gps->write("$PMTK285,3,100*3F\r\n");
|
_serial_gps->write("$PMTK285,3,100*3F\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 4) {
|
|
||||||
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
|
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
|
||||||
_serial_gps->write("$PMTK886,1*29\r\n");
|
_serial_gps->write("$PMTK886,1*29\r\n");
|
||||||
currentDelay = 250;
|
delay(250);
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_MTK_PA1010D) {
|
} else if (gnssModel == GNSS_MODEL_MTK_PA1010D) {
|
||||||
// PA1010D is used in the Pimoroni GPS board.
|
// PA1010D is used in the Pimoroni GPS board.
|
||||||
if (currentStep == 0) {
|
|
||||||
// Enable all constellations.
|
// Enable all constellations.
|
||||||
_serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n");
|
_serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n");
|
||||||
currentStep++;
|
|
||||||
currentDelay = 1000;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// Above command will reset the GPS and takes longer before it will accept new commands
|
// Above command will reset the GPS and takes longer before it will accept new commands
|
||||||
|
delay(1000);
|
||||||
// Only ask for RMC and GGA (GNRMC and GNGGA)
|
// Only ask for RMC and GGA (GNRMC and GNGGA)
|
||||||
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
|
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
// Enable SBAS / WAAS
|
// Enable SBAS / WAAS
|
||||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||||
currentDelay = 250;
|
delay(250);
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_MTK_PA1616S) {
|
} else if (gnssModel == GNSS_MODEL_MTK_PA1616S) {
|
||||||
// PA1616S is used in some GPS breakout boards from Adafruit
|
// PA1616S is used in some GPS breakout boards from Adafruit
|
||||||
// PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here.
|
// PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here.
|
||||||
if (currentStep == 0) {
|
|
||||||
_serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n");
|
_serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n");
|
||||||
currentStep++;
|
|
||||||
currentDelay = 1000;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// Above command will reset the GPS and takes longer before it will accept new commands
|
// Above command will reset the GPS and takes longer before it will accept new commands
|
||||||
|
delay(1000);
|
||||||
// Only ask for RMC and GGA (GNRMC and GNGGA)
|
// Only ask for RMC and GGA (GNRMC and GNGGA)
|
||||||
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
|
_serial_gps->write("$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
// Enable SBAS / WAAS
|
// Enable SBAS / WAAS
|
||||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||||
currentDelay = 250;
|
delay(250);
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_ATGM336H) {
|
} else if (gnssModel == GNSS_MODEL_ATGM336H) {
|
||||||
if (currentStep == 0) {
|
|
||||||
// Set the initial configuration of the device - these _should_ work for most AT6558 devices
|
// Set the initial configuration of the device - these _should_ work for most AT6558 devices
|
||||||
msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
|
msglen = makeCASPacket(0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
_serial_gps->write(UBXscratch, msglen);
|
||||||
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
|
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
|
||||||
LOG_WARN("ATGM336H: Could not set Config");
|
LOG_WARN("ATGM336H: Could not set Config");
|
||||||
}
|
}
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// Set the update frequency to 1Hz
|
// Set the update frequency to 1Hz
|
||||||
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
|
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
_serial_gps->write(UBXscratch, msglen);
|
||||||
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
|
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
|
||||||
LOG_WARN("ATGM336H: Could not set Update Frequency");
|
LOG_WARN("ATGM336H: Could not set Update Frequency");
|
||||||
}
|
}
|
||||||
currentStep++;
|
|
||||||
return false;
|
// Set the NEMA output messages
|
||||||
} else if (currentStep == 2) {
|
// Ask for only RMC and GGA
|
||||||
// Set the NEMA output messages - Ask for only RMC and GGA
|
uint8_t fields[] = {CAS_NEMA_RMC, CAS_NEMA_GGA};
|
||||||
|
for (unsigned int i = 0; i < sizeof(fields); i++) {
|
||||||
// Construct a CAS-CFG-MSG packet
|
// Construct a CAS-CFG-MSG packet
|
||||||
uint8_t cas_cfg_msg_packet[] = {0x4e, CAS_NEMA_RMC, 0x01, 0x00};
|
uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00};
|
||||||
msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
|
msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
_serial_gps->write(UBXscratch, msglen);
|
||||||
if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) {
|
if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) {
|
||||||
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", CAS_NEMA_RMC);
|
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]);
|
||||||
}
|
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 3) {
|
|
||||||
uint8_t cas_cfg_msg_packet[] = {0x4e, CAS_NEMA_GGA, 0x01, 0x00};
|
|
||||||
msglen = makeCASPacket(0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
|
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
|
||||||
if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) {
|
|
||||||
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", CAS_NEMA_GGA);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (gnssModel == GNSS_MODEL_UC6580) {
|
} else if (gnssModel == GNSS_MODEL_UC6580) {
|
||||||
@@ -677,36 +625,22 @@ bool GPS::setup()
|
|||||||
// use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS
|
// use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS
|
||||||
// This will reset the receiver, so wait a bit afterwards
|
// This will reset the receiver, so wait a bit afterwards
|
||||||
// The paranoid will wait for the OK*04 confirmation response after each command.
|
// The paranoid will wait for the OK*04 confirmation response after each command.
|
||||||
if (currentStep == 0) {
|
|
||||||
_serial_gps->write("$CFGSYS,h35155\r\n");
|
_serial_gps->write("$CFGSYS,h35155\r\n");
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// Must be done after the CFGSYS command
|
// Must be done after the CFGSYS command
|
||||||
// Turn off GSV messages, we don't really care about which and where the sats are, maybe someday.
|
// Turn off GSV messages, we don't really care about which and where the sats are, maybe someday.
|
||||||
_serial_gps->write("$CFGMSG,0,3,0\r\n");
|
_serial_gps->write("$CFGMSG,0,3,0\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
// Turn off GSA messages, TinyGPS++ doesn't use this message.
|
// Turn off GSA messages, TinyGPS++ doesn't use this message.
|
||||||
_serial_gps->write("$CFGMSG,0,2,0\r\n");
|
_serial_gps->write("$CFGMSG,0,2,0\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 3) {
|
|
||||||
// Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care.
|
// Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care.
|
||||||
_serial_gps->write("$CFGMSG,6,0,0\r\n");
|
_serial_gps->write("$CFGMSG,6,0,0\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 4) {
|
|
||||||
_serial_gps->write("$CFGMSG,6,1,0\r\n");
|
_serial_gps->write("$CFGMSG,6,1,0\r\n");
|
||||||
currentDelay = 250;
|
delay(250);
|
||||||
}
|
|
||||||
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
|
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
|
||||||
if (currentStep == 0) {
|
|
||||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
|
||||||
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
|
config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_NP_865) {
|
||||||
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
|
_serial_gps->write("$PAIR066,1,0,1,0,0,1*3B\r\n"); // Enable GPS+GALILEO+NAVIC
|
||||||
@@ -717,98 +651,37 @@ bool GPS::setup()
|
|||||||
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||||
// 1 1 1 1 0 0
|
// 1 1 1 1 0 0
|
||||||
}
|
}
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// Configure NMEA (sentences will output once per fix)
|
// Configure NMEA (sentences will output once per fix)
|
||||||
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
|
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
|
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 3) {
|
|
||||||
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF
|
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 4) {
|
|
||||||
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF
|
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 5) {
|
|
||||||
_serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON
|
_serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 6) {
|
|
||||||
_serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF
|
_serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 7) {
|
|
||||||
_serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON
|
_serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON
|
||||||
currentStep++;
|
|
||||||
currentDelay = 250;
|
delay(250);
|
||||||
return false;
|
|
||||||
} else if (currentStep == 8) {
|
|
||||||
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
|
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_UBLOX6) {
|
} else if (gnssModel == GNSS_MODEL_UBLOX6) {
|
||||||
if (currentStep == 0) {
|
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 3) {
|
|
||||||
// Turn off unwanted NMEA messages, set update rate
|
// Turn off unwanted NMEA messages, set update rate
|
||||||
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 4) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 5) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 6) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 7) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 8) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 9) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 10) {
|
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
|
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 11) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 12) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 13) {
|
|
||||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
|
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
_serial_gps->write(UBXscratch, msglen);
|
||||||
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
||||||
@@ -816,9 +689,7 @@ bool GPS::setup()
|
|||||||
} else {
|
} else {
|
||||||
LOG_INFO("GNSS module config saved!");
|
LOG_INFO("GNSS module config saved!");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) {
|
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) {
|
||||||
if (currentStep == 0) {
|
|
||||||
if (gnssModel == GNSS_MODEL_UBLOX7) {
|
if (gnssModel == GNSS_MODEL_UBLOX7) {
|
||||||
LOG_DEBUG("Set GPS+SBAS");
|
LOG_DEBUG("Set GPS+SBAS");
|
||||||
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7);
|
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7);
|
||||||
@@ -827,6 +698,7 @@ bool GPS::setup()
|
|||||||
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8);
|
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8);
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
_serial_gps->write(UBXscratch, msglen);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) {
|
if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) {
|
||||||
// It's not critical if the module doesn't acknowledge this configuration.
|
// It's not critical if the module doesn't acknowledge this configuration.
|
||||||
LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?");
|
LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?");
|
||||||
@@ -840,82 +712,45 @@ bool GPS::setup()
|
|||||||
// commands for the M8 it tends to be more... 1 sec should be enough ;>)
|
// commands for the M8 it tends to be more... 1 sec should be enough ;>)
|
||||||
delay(1000);
|
delay(1000);
|
||||||
}
|
}
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// Disable Text Info messages //6,7,8,9
|
// Disable Text Info messages //6,7,8,9
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
if (gnssModel == GNSS_MODEL_UBLOX8) { // 8
|
if (gnssModel == GNSS_MODEL_UBLOX8) { // 8
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500);
|
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_8, "enable interference resistance", 500);
|
||||||
} else { // 7,9
|
|
||||||
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
|
||||||
}
|
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 3) {
|
|
||||||
if (gnssModel == GNSS_MODEL_UBLOX8) { // 8
|
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
|
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
|
||||||
} else { // 7,9
|
} else { // 6,7,9
|
||||||
|
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
||||||
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
||||||
}
|
}
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 4) {
|
|
||||||
// Turn off unwanted NMEA messages, set update rate
|
// Turn off unwanted NMEA messages, set update rate
|
||||||
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 5) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 6) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 7) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 8) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 9) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 10) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 11) {
|
|
||||||
if (ublox_info.protocol_version >= 18) {
|
if (ublox_info.protocol_version >= 18) {
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
|
SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
|
||||||
} else {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500);
|
|
||||||
}
|
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 12) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 13) {
|
|
||||||
// For M8 we want to enable NMEA version 4.10 so we can see the additional sats.
|
// For M8 we want to enable NMEA version 4.10 so we can see the additional sats.
|
||||||
if (ublox_info.protocol_version >= 18 && gnssModel == GNSS_MODEL_UBLOX8) {
|
if (gnssModel == GNSS_MODEL_UBLOX8) {
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
|
SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
|
||||||
currentStep++;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500);
|
||||||
|
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
||||||
|
}
|
||||||
|
|
||||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
|
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
_serial_gps->write(UBXscratch, msglen);
|
||||||
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
||||||
@@ -923,92 +758,44 @@ bool GPS::setup()
|
|||||||
} else {
|
} else {
|
||||||
LOG_INFO("GNSS module configuration saved!");
|
LOG_INFO("GNSS module configuration saved!");
|
||||||
}
|
}
|
||||||
} else if (currentStep == 14) {
|
|
||||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE), _message_SAVE);
|
|
||||||
_serial_gps->write(UBXscratch, msglen);
|
|
||||||
if (getACK(0x06, 0x09, 2000) != GNSS_RESPONSE_OK) {
|
|
||||||
LOG_WARN("Unable to save GNSS module config");
|
|
||||||
} else {
|
|
||||||
LOG_INFO("GNSS module configuration saved!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_UBLOX10) {
|
} else if (gnssModel == GNSS_MODEL_UBLOX10) {
|
||||||
if (currentStep == 0) {
|
|
||||||
LOG_INFO("Configuring M10 GPS step 0");
|
|
||||||
delay(1000);
|
delay(1000);
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
LOG_INFO("Configuring M10 GPS step 1");
|
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 2) {
|
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 3) {
|
|
||||||
// Next disable Info txt messages in BBR layer
|
// Next disable Info txt messages in BBR layer
|
||||||
clearBuffer();
|
clearBuffer();
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 4) {
|
|
||||||
// Do M10 configuration for Power Management.
|
// Do M10 configuration for Power Management.
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 5) {
|
|
||||||
// Next enable powersave in BBR layer
|
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 6) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 7) {
|
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 8) {
|
|
||||||
// Here is where the init commands should go to do further M10 initialization.
|
// Here is where the init commands should go to do further M10 initialization.
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300);
|
||||||
currentStep++;
|
delay(750); // will cause a receiver restart so wait a bit
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 9) {
|
|
||||||
// will cause a receiver restart so wait a bit
|
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300);
|
||||||
currentStep++;
|
delay(750); // will cause a receiver restart so wait a bit
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 10) {
|
|
||||||
// Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic
|
// Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic
|
||||||
// sleep.
|
// sleep.
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 11) {
|
|
||||||
// Next enable wanted NMEA messages in RAM layer
|
// Next enable wanted NMEA messages in RAM layer
|
||||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500);
|
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500);
|
||||||
currentStep++;
|
delay(750);
|
||||||
currentDelay = 750;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 12) {
|
|
||||||
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
|
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
|
||||||
// BBR will survive a restart, and power off for a while, but modules with small backup
|
// BBR will survive a restart, and power off for a while, but modules with small backup
|
||||||
// batteries or super caps will not retain the config for a long power off time.
|
// batteries or super caps will not retain the config for a long power off time.
|
||||||
@@ -1019,21 +806,14 @@ bool GPS::setup()
|
|||||||
} else {
|
} else {
|
||||||
LOG_INFO("GNSS module configuration saved!");
|
LOG_INFO("GNSS module configuration saved!");
|
||||||
}
|
}
|
||||||
didSerialInit = true;
|
|
||||||
}
|
|
||||||
} else if (gnssModel == GNSS_MODEL_CM121) {
|
} else if (gnssModel == GNSS_MODEL_CM121) {
|
||||||
// only ask for RMC and GGA
|
// only ask for RMC and GGA
|
||||||
if (currentStep == 0) {
|
|
||||||
// enable GGA
|
// enable GGA
|
||||||
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
|
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
|
||||||
currentStep++;
|
delay(250);
|
||||||
currentDelay = 250;
|
|
||||||
return false;
|
|
||||||
} else if (currentStep == 1) {
|
|
||||||
// enable RMC
|
// enable RMC
|
||||||
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
|
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
|
||||||
currentDelay = 250;
|
delay(250);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
didSerialInit = true;
|
didSerialInit = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,8 +128,6 @@ class GPS : private concurrency::OSThread
|
|||||||
// Let the GPS hardware save power between updates
|
// Let the GPS hardware save power between updates
|
||||||
void down();
|
void down();
|
||||||
|
|
||||||
bool initFinished() const { return GPSInitFinished; };
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GPS() : concurrency::OSThread("GPS") {}
|
GPS() : concurrency::OSThread("GPS") {}
|
||||||
|
|
||||||
|
|||||||
+18
-18
@@ -524,6 +524,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
delay(100);
|
delay(100);
|
||||||
#endif
|
#endif
|
||||||
#if !ARCH_PORTDUINO
|
#if !ARCH_PORTDUINO
|
||||||
|
#if defined(USE_ST7789) && defined(VTFT_CTRL)
|
||||||
|
// Ensure panel power rail is enabled before sending wake commands.
|
||||||
|
pinMode(VTFT_CTRL, OUTPUT);
|
||||||
|
digitalWrite(VTFT_CTRL, LOW);
|
||||||
|
#endif
|
||||||
dispdev->displayOn();
|
dispdev->displayOn();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -545,10 +550,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
ui->init();
|
ui->init();
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ST7789) && defined(VTFT_LEDA)
|
#if defined(USE_ST7789) && defined(VTFT_LEDA)
|
||||||
#ifdef VTFT_CTRL
|
|
||||||
pinMode(VTFT_CTRL, OUTPUT);
|
|
||||||
digitalWrite(VTFT_CTRL, LOW);
|
|
||||||
#endif
|
|
||||||
ui->init();
|
ui->init();
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
||||||
@@ -589,23 +590,22 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_ST7789
|
#ifdef USE_ST7789
|
||||||
SPI1.end();
|
SPI1.end();
|
||||||
#if defined(ARCH_ESP32)
|
// Keep TFT control pins in deterministic states while timed-off.
|
||||||
|
// Floating/default pin states can corrupt panel edge rows on wake.
|
||||||
#ifdef VTFT_LEDA
|
#ifdef VTFT_LEDA
|
||||||
pinMode(VTFT_LEDA, ANALOG);
|
pinMode(VTFT_LEDA, OUTPUT);
|
||||||
|
digitalWrite(VTFT_LEDA, !TFT_BACKLIGHT_ON);
|
||||||
#endif
|
#endif
|
||||||
#ifdef VTFT_CTRL
|
#ifdef VTFT_CTRL
|
||||||
pinMode(VTFT_CTRL, ANALOG);
|
pinMode(VTFT_CTRL, OUTPUT);
|
||||||
#endif
|
digitalWrite(VTFT_CTRL, HIGH);
|
||||||
pinMode(ST7789_RESET, ANALOG);
|
|
||||||
pinMode(ST7789_RS, ANALOG);
|
|
||||||
pinMode(ST7789_NSS, ANALOG);
|
|
||||||
#else
|
|
||||||
nrf_gpio_cfg_default(VTFT_LEDA);
|
|
||||||
nrf_gpio_cfg_default(VTFT_CTRL);
|
|
||||||
nrf_gpio_cfg_default(ST7789_RESET);
|
|
||||||
nrf_gpio_cfg_default(ST7789_RS);
|
|
||||||
nrf_gpio_cfg_default(ST7789_NSS);
|
|
||||||
#endif
|
#endif
|
||||||
|
pinMode(ST7789_RESET, OUTPUT);
|
||||||
|
digitalWrite(ST7789_RESET, HIGH);
|
||||||
|
pinMode(ST7789_RS, OUTPUT);
|
||||||
|
digitalWrite(ST7789_RS, HIGH);
|
||||||
|
pinMode(ST7789_NSS, OUTPUT);
|
||||||
|
digitalWrite(ST7789_NSS, HIGH);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ST7796
|
#ifdef USE_ST7796
|
||||||
SPI1.end();
|
SPI1.end();
|
||||||
@@ -886,7 +886,7 @@ int32_t Screen::runOnce()
|
|||||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||||
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
||||||
static bool showingBootScreen = true;
|
static bool showingBootScreen = true;
|
||||||
if (showingBootScreen && (!gps || gps->initFinished()) && (millis() > (logo_timeout + serialSinceMsec))) {
|
if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) {
|
||||||
LOG_INFO("Done with boot screen");
|
LOG_INFO("Done with boot screen");
|
||||||
stopBootScreen();
|
stopBootScreen();
|
||||||
showingBootScreen = false;
|
showingBootScreen = false;
|
||||||
|
|||||||
@@ -1508,24 +1508,6 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
|
|||||||
display->drawString(x + getStringCenteredX(title) + 1, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - 5, title);
|
display->drawString(x + getStringCenteredX(title) + 1, y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - 5, title);
|
||||||
}
|
}
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
if (bootString != nullptr) {
|
|
||||||
static uint8_t bootStringStep = 0;
|
|
||||||
char stringCharacter = '.';
|
|
||||||
uint32_t stringWidth = getStringCenteredX(bootString);
|
|
||||||
if (bootStringStep == 0) {
|
|
||||||
stringCharacter = '.';
|
|
||||||
bootStringStep++;
|
|
||||||
} else if (bootStringStep == 1) {
|
|
||||||
stringCharacter = 'o';
|
|
||||||
bootStringStep++;
|
|
||||||
} else if (bootStringStep == 2) {
|
|
||||||
stringCharacter = 'O';
|
|
||||||
bootStringStep = 0;
|
|
||||||
}
|
|
||||||
char tmpBootString[40];
|
|
||||||
snprintf(tmpBootString, sizeof(tmpBootString), "%s %c", bootString, stringCharacter);
|
|
||||||
display->drawString(x + stringWidth, y + SCREEN_HEIGHT - 2 * FONT_HEIGHT_MEDIUM, tmpBootString);
|
|
||||||
}
|
|
||||||
// Draw region in upper left
|
// Draw region in upper left
|
||||||
if (upperMsg) {
|
if (upperMsg) {
|
||||||
display->drawString(x + 5, y + 5, upperMsg);
|
display->drawString(x + 5, y + 5, upperMsg);
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr;
|
|||||||
#include <string>
|
#include <string>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const char *bootString =
|
|
||||||
nullptr; // Pointer to a string that will be drawn on the boot screen, if set (used for GPS probing status)
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
#ifdef DEBUG_PARTITION_TABLE
|
#ifdef DEBUG_PARTITION_TABLE
|
||||||
#include "esp_partition.h"
|
#include "esp_partition.h"
|
||||||
|
|||||||
@@ -101,5 +101,3 @@ void scannerToSensorsMap(const std::unique_ptr<ScanI2CTwoWire> &i2cScanner, Scan
|
|||||||
|
|
||||||
// We default to 4MHz SPI, SPI mode 0
|
// We default to 4MHz SPI, SPI mode 0
|
||||||
extern SPISettings spiSettings;
|
extern SPISettings spiSettings;
|
||||||
|
|
||||||
extern const char *bootString;
|
|
||||||
|
|||||||
+17
-7
@@ -2,18 +2,21 @@
|
|||||||
|
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
|
|
||||||
|
// Convert seconds to ms, clamping at INT32_MAX (~24.86 days)
|
||||||
|
static inline uint32_t secondsToMsClamped(uint32_t secs)
|
||||||
|
{
|
||||||
|
constexpr uint32_t MAX_MS = static_cast<uint32_t>(INT32_MAX);
|
||||||
|
return (secs > MAX_MS / 1000U) ? MAX_MS : secs * 1000U;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval)
|
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval, uint32_t defaultInterval)
|
||||||
{
|
{
|
||||||
if (configuredInterval > 0)
|
return secondsToMsClamped(configuredInterval > 0 ? configuredInterval : defaultInterval);
|
||||||
return configuredInterval * 1000;
|
|
||||||
return defaultInterval * 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval)
|
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval)
|
||||||
{
|
{
|
||||||
if (configuredInterval > 0)
|
return secondsToMsClamped(configuredInterval > 0 ? configuredInterval : default_broadcast_interval_secs);
|
||||||
return configuredInterval * 1000;
|
|
||||||
return default_broadcast_interval_secs * 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue)
|
uint32_t Default::getConfiguredOrDefault(uint32_t configured, uint32_t defaultValue)
|
||||||
@@ -47,7 +50,14 @@ uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t d
|
|||||||
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER))
|
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER))
|
||||||
return getConfiguredOrDefaultMs(configured, defaultValue);
|
return getConfiguredOrDefaultMs(configured, defaultValue);
|
||||||
|
|
||||||
return getConfiguredOrDefaultMs(configured, defaultValue) * congestionScalingCoefficient(numOnlineNodes);
|
// Saturate at INT32_MAX to match secondsToMsClamped: float→uint32_t when
|
||||||
|
// out of range is UB, and the result is consumed as an int32_t downstream.
|
||||||
|
constexpr uint32_t MAX_MS = static_cast<uint32_t>(INT32_MAX);
|
||||||
|
uint32_t base = getConfiguredOrDefaultMs(configured, defaultValue);
|
||||||
|
float coef = congestionScalingCoefficient(numOnlineNodes);
|
||||||
|
if (static_cast<double>(base) * static_cast<double>(coef) >= static_cast<double>(MAX_MS))
|
||||||
|
return MAX_MS;
|
||||||
|
return base * coef;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue)
|
uint32_t Default::getConfiguredOrMinimumValue(uint32_t configured, uint32_t minValue)
|
||||||
|
|||||||
+2
-2
@@ -1205,11 +1205,11 @@ void NodeDB::loadFromDisk()
|
|||||||
spiLock->unlock();
|
spiLock->unlock();
|
||||||
#endif
|
#endif
|
||||||
#ifdef FSCom
|
#ifdef FSCom
|
||||||
#ifdef FACTORY_INSTALL
|
#if defined(FACTORY_INSTALL) && !defined(ARCH_PORTDUINO)
|
||||||
spiLock->lock();
|
spiLock->lock();
|
||||||
if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) {
|
if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) {
|
||||||
LOG_WARN("Factory Install Reset!");
|
LOG_WARN("Factory Install Reset!");
|
||||||
FSCom.format();
|
rmDir("/prefs");
|
||||||
FSCom.mkdir("/prefs");
|
FSCom.mkdir("/prefs");
|
||||||
File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE);
|
File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE);
|
||||||
if (f2) {
|
if (f2) {
|
||||||
|
|||||||
@@ -179,6 +179,8 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
|||||||
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
||||||
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
|
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
|
||||||
|
|
||||||
|
uint8_t packetsInTxQueue() { return txQueue.getMaxLen() - txQueue.getFree(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
|
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
|
||||||
* @return true if len bytes were produced, false otherwise.
|
* @return true if len bytes were produced, false otherwise.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ extern const pb_msgdesc_t meshtastic_ChannelSet_msg;
|
|||||||
|
|
||||||
/* Maximum encoded size of messages (where known) */
|
/* Maximum encoded size of messages (where known) */
|
||||||
#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size
|
#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size
|
||||||
#define meshtastic_ChannelSet_size 682
|
#define meshtastic_ChannelSet_size 685
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
} /* extern "C" */
|
} /* extern "C" */
|
||||||
|
|||||||
@@ -618,6 +618,8 @@ typedef struct _meshtastic_Config_LoRaConfig {
|
|||||||
bool config_ok_to_mqtt;
|
bool config_ok_to_mqtt;
|
||||||
/* Set where LORA FEM is enabled, disabled, or not present */
|
/* Set where LORA FEM is enabled, disabled, or not present */
|
||||||
meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode;
|
meshtastic_Config_LoRaConfig_FEM_LNA_Mode fem_lna_mode;
|
||||||
|
/* Don't use radiolib to initialize the radio, instead listen for a serialHal connection */
|
||||||
|
bool serial_hal_only;
|
||||||
} meshtastic_Config_LoRaConfig;
|
} meshtastic_Config_LoRaConfig;
|
||||||
|
|
||||||
typedef struct _meshtastic_Config_BluetoothConfig {
|
typedef struct _meshtastic_Config_BluetoothConfig {
|
||||||
@@ -779,7 +781,7 @@ extern "C" {
|
|||||||
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
|
#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0}
|
||||||
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
|
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
|
||||||
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||||
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN}
|
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
||||||
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
#define meshtastic_Config_BluetoothConfig_init_default {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||||
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
#define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||||
#define meshtastic_Config_SessionkeyConfig_init_default {0}
|
#define meshtastic_Config_SessionkeyConfig_init_default {0}
|
||||||
@@ -790,7 +792,7 @@ extern "C" {
|
|||||||
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
|
#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0}
|
||||||
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
|
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
|
||||||
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_DeprecatedGpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0, 0, 0}
|
||||||
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN}
|
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0, _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN, 0}
|
||||||
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
#define meshtastic_Config_BluetoothConfig_init_zero {0, _meshtastic_Config_BluetoothConfig_PairingMode_MIN, 0}
|
||||||
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
#define meshtastic_Config_SecurityConfig_init_zero {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0}
|
||||||
#define meshtastic_Config_SessionkeyConfig_init_zero {0}
|
#define meshtastic_Config_SessionkeyConfig_init_zero {0}
|
||||||
@@ -877,6 +879,7 @@ extern "C" {
|
|||||||
#define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104
|
#define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104
|
||||||
#define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105
|
#define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105
|
||||||
#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106
|
#define meshtastic_Config_LoRaConfig_fem_lna_mode_tag 106
|
||||||
|
#define meshtastic_Config_LoRaConfig_serial_hal_only_tag 107
|
||||||
#define meshtastic_Config_BluetoothConfig_enabled_tag 1
|
#define meshtastic_Config_BluetoothConfig_enabled_tag 1
|
||||||
#define meshtastic_Config_BluetoothConfig_mode_tag 2
|
#define meshtastic_Config_BluetoothConfig_mode_tag 2
|
||||||
#define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3
|
#define meshtastic_Config_BluetoothConfig_fixed_pin_tag 3
|
||||||
@@ -1029,7 +1032,8 @@ X(a, STATIC, SINGULAR, BOOL, pa_fan_disabled, 15) \
|
|||||||
X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \
|
X(a, STATIC, REPEATED, UINT32, ignore_incoming, 103) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \
|
X(a, STATIC, SINGULAR, BOOL, ignore_mqtt, 104) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \
|
X(a, STATIC, SINGULAR, BOOL, config_ok_to_mqtt, 105) \
|
||||||
X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106)
|
X(a, STATIC, SINGULAR, UENUM, fem_lna_mode, 106) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, serial_hal_only, 107)
|
||||||
#define meshtastic_Config_LoRaConfig_CALLBACK NULL
|
#define meshtastic_Config_LoRaConfig_CALLBACK NULL
|
||||||
#define meshtastic_Config_LoRaConfig_DEFAULT NULL
|
#define meshtastic_Config_LoRaConfig_DEFAULT NULL
|
||||||
|
|
||||||
@@ -1086,7 +1090,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
|
|||||||
#define meshtastic_Config_BluetoothConfig_size 10
|
#define meshtastic_Config_BluetoothConfig_size 10
|
||||||
#define meshtastic_Config_DeviceConfig_size 100
|
#define meshtastic_Config_DeviceConfig_size 100
|
||||||
#define meshtastic_Config_DisplayConfig_size 36
|
#define meshtastic_Config_DisplayConfig_size 36
|
||||||
#define meshtastic_Config_LoRaConfig_size 88
|
#define meshtastic_Config_LoRaConfig_size 91
|
||||||
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
|
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
|
||||||
#define meshtastic_Config_NetworkConfig_size 204
|
#define meshtastic_Config_NetworkConfig_size 204
|
||||||
#define meshtastic_Config_PositionConfig_size 62
|
#define meshtastic_Config_PositionConfig_size 62
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
|
|||||||
/* Maximum encoded size of messages (where known) */
|
/* Maximum encoded size of messages (where known) */
|
||||||
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
||||||
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
|
||||||
#define meshtastic_BackupPreferences_size 2429
|
#define meshtastic_BackupPreferences_size 2432
|
||||||
#define meshtastic_ChannelFile_size 718
|
#define meshtastic_ChannelFile_size 718
|
||||||
#define meshtastic_DeviceState_size 1737
|
#define meshtastic_DeviceState_size 1737
|
||||||
#define meshtastic_NodeInfoLite_size 196
|
#define meshtastic_NodeInfoLite_size 196
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
|
|||||||
|
|
||||||
/* Maximum encoded size of messages (where known) */
|
/* Maximum encoded size of messages (where known) */
|
||||||
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
|
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalModuleConfig_size
|
||||||
#define meshtastic_LocalConfig_size 754
|
#define meshtastic_LocalConfig_size 757
|
||||||
#define meshtastic_LocalModuleConfig_size 820
|
#define meshtastic_LocalModuleConfig_size 820
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/* Automatically generated nanopb constant definitions */
|
||||||
|
/* Generated by nanopb-0.4.9.1 */
|
||||||
|
|
||||||
|
#include "meshtastic/serial_hal.pb.h"
|
||||||
|
#if PB_PROTO_HEADER_VERSION != 40
|
||||||
|
#error Regenerate this file with the current version of nanopb generator.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
PB_BIND(meshtastic_SerialHalCommand, meshtastic_SerialHalCommand, 2)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(meshtastic_SerialHalResponse, meshtastic_SerialHalResponse, 2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/* Automatically generated nanopb header */
|
||||||
|
/* Generated by nanopb-0.4.9.1 */
|
||||||
|
|
||||||
|
#ifndef PB_MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_INCLUDED
|
||||||
|
#define PB_MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_INCLUDED
|
||||||
|
#include <pb.h>
|
||||||
|
|
||||||
|
#if PB_PROTO_HEADER_VERSION != 40
|
||||||
|
#error Regenerate this file with the current version of nanopb generator.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Enum definitions */
|
||||||
|
typedef enum _meshtastic_SerialHalCommand_Type {
|
||||||
|
meshtastic_SerialHalCommand_Type_UNSET = 0,
|
||||||
|
meshtastic_SerialHalCommand_Type_PIN_MODE = 1,
|
||||||
|
meshtastic_SerialHalCommand_Type_DIGITAL_WRITE = 2,
|
||||||
|
meshtastic_SerialHalCommand_Type_DIGITAL_READ = 3,
|
||||||
|
meshtastic_SerialHalCommand_Type_ATTACH_INTERRUPT = 4,
|
||||||
|
meshtastic_SerialHalCommand_Type_DETACH_INTERRUPT = 5,
|
||||||
|
meshtastic_SerialHalCommand_Type_SPI_TRANSFER = 6,
|
||||||
|
meshtastic_SerialHalCommand_Type_NOOP = 7
|
||||||
|
} meshtastic_SerialHalCommand_Type;
|
||||||
|
|
||||||
|
typedef enum _meshtastic_SerialHalResponse_Result {
|
||||||
|
meshtastic_SerialHalResponse_Result_OK = 0,
|
||||||
|
meshtastic_SerialHalResponse_Result_ERROR = 1,
|
||||||
|
meshtastic_SerialHalResponse_Result_BAD_REQUEST = 2,
|
||||||
|
meshtastic_SerialHalResponse_Result_UNSUPPORTED = 3
|
||||||
|
} meshtastic_SerialHalResponse_Result;
|
||||||
|
|
||||||
|
/* Struct definitions */
|
||||||
|
typedef PB_BYTES_ARRAY_T(512) meshtastic_SerialHalCommand_data_t;
|
||||||
|
typedef struct _meshtastic_SerialHalCommand {
|
||||||
|
/* Host-assigned request id. Replies echo this id back in
|
||||||
|
SerialHalResponse.transaction_id. */
|
||||||
|
uint32_t transaction_id;
|
||||||
|
meshtastic_SerialHalCommand_Type type;
|
||||||
|
uint32_t pin;
|
||||||
|
uint32_t value;
|
||||||
|
uint32_t mode;
|
||||||
|
meshtastic_SerialHalCommand_data_t data;
|
||||||
|
} meshtastic_SerialHalCommand;
|
||||||
|
|
||||||
|
typedef PB_BYTES_ARRAY_T(512) meshtastic_SerialHalResponse_data_t;
|
||||||
|
typedef struct _meshtastic_SerialHalResponse {
|
||||||
|
/* Matches the originating SerialHalCommand.transaction_id for normal
|
||||||
|
request/response traffic.
|
||||||
|
|
||||||
|
A value of 0 indicates an unsolicited interrupt notification generated by
|
||||||
|
the device. In that case, the host should interpret value as the GPIO pin
|
||||||
|
that triggered. */
|
||||||
|
uint32_t transaction_id;
|
||||||
|
meshtastic_SerialHalResponse_Result result;
|
||||||
|
/* Used by DIGITAL_READ replies and interrupt notifications. For interrupt
|
||||||
|
notifications (transaction_id == 0), this carries the pin number. */
|
||||||
|
uint32_t value;
|
||||||
|
meshtastic_SerialHalResponse_data_t data;
|
||||||
|
char error[80];
|
||||||
|
} meshtastic_SerialHalResponse;
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Helper constants for enums */
|
||||||
|
#define _meshtastic_SerialHalCommand_Type_MIN meshtastic_SerialHalCommand_Type_UNSET
|
||||||
|
#define _meshtastic_SerialHalCommand_Type_MAX meshtastic_SerialHalCommand_Type_NOOP
|
||||||
|
#define _meshtastic_SerialHalCommand_Type_ARRAYSIZE ((meshtastic_SerialHalCommand_Type)(meshtastic_SerialHalCommand_Type_NOOP+1))
|
||||||
|
|
||||||
|
#define _meshtastic_SerialHalResponse_Result_MIN meshtastic_SerialHalResponse_Result_OK
|
||||||
|
#define _meshtastic_SerialHalResponse_Result_MAX meshtastic_SerialHalResponse_Result_UNSUPPORTED
|
||||||
|
#define _meshtastic_SerialHalResponse_Result_ARRAYSIZE ((meshtastic_SerialHalResponse_Result)(meshtastic_SerialHalResponse_Result_UNSUPPORTED+1))
|
||||||
|
|
||||||
|
#define meshtastic_SerialHalCommand_type_ENUMTYPE meshtastic_SerialHalCommand_Type
|
||||||
|
|
||||||
|
#define meshtastic_SerialHalResponse_result_ENUMTYPE meshtastic_SerialHalResponse_Result
|
||||||
|
|
||||||
|
|
||||||
|
/* Initializer values for message structs */
|
||||||
|
#define meshtastic_SerialHalCommand_init_default {0, _meshtastic_SerialHalCommand_Type_MIN, 0, 0, 0, {0, {0}}}
|
||||||
|
#define meshtastic_SerialHalResponse_init_default {0, _meshtastic_SerialHalResponse_Result_MIN, 0, {0, {0}}, ""}
|
||||||
|
#define meshtastic_SerialHalCommand_init_zero {0, _meshtastic_SerialHalCommand_Type_MIN, 0, 0, 0, {0, {0}}}
|
||||||
|
#define meshtastic_SerialHalResponse_init_zero {0, _meshtastic_SerialHalResponse_Result_MIN, 0, {0, {0}}, ""}
|
||||||
|
|
||||||
|
/* Field tags (for use in manual encoding/decoding) */
|
||||||
|
#define meshtastic_SerialHalCommand_transaction_id_tag 1
|
||||||
|
#define meshtastic_SerialHalCommand_type_tag 2
|
||||||
|
#define meshtastic_SerialHalCommand_pin_tag 3
|
||||||
|
#define meshtastic_SerialHalCommand_value_tag 4
|
||||||
|
#define meshtastic_SerialHalCommand_mode_tag 5
|
||||||
|
#define meshtastic_SerialHalCommand_data_tag 6
|
||||||
|
#define meshtastic_SerialHalResponse_transaction_id_tag 1
|
||||||
|
#define meshtastic_SerialHalResponse_result_tag 2
|
||||||
|
#define meshtastic_SerialHalResponse_value_tag 3
|
||||||
|
#define meshtastic_SerialHalResponse_data_tag 4
|
||||||
|
#define meshtastic_SerialHalResponse_error_tag 5
|
||||||
|
|
||||||
|
/* Struct field encoding specification for nanopb */
|
||||||
|
#define meshtastic_SerialHalCommand_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, transaction_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UENUM, type, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, pin, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, value, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, mode, 5) \
|
||||||
|
X(a, STATIC, SINGULAR, BYTES, data, 6)
|
||||||
|
#define meshtastic_SerialHalCommand_CALLBACK NULL
|
||||||
|
#define meshtastic_SerialHalCommand_DEFAULT NULL
|
||||||
|
|
||||||
|
#define meshtastic_SerialHalResponse_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, transaction_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UENUM, result, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, value, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, BYTES, data, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, STRING, error, 5)
|
||||||
|
#define meshtastic_SerialHalResponse_CALLBACK NULL
|
||||||
|
#define meshtastic_SerialHalResponse_DEFAULT NULL
|
||||||
|
|
||||||
|
extern const pb_msgdesc_t meshtastic_SerialHalCommand_msg;
|
||||||
|
extern const pb_msgdesc_t meshtastic_SerialHalResponse_msg;
|
||||||
|
|
||||||
|
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||||
|
#define meshtastic_SerialHalCommand_fields &meshtastic_SerialHalCommand_msg
|
||||||
|
#define meshtastic_SerialHalResponse_fields &meshtastic_SerialHalResponse_msg
|
||||||
|
|
||||||
|
/* Maximum encoded size of messages (where known) */
|
||||||
|
#define MESHTASTIC_MESHTASTIC_SERIAL_HAL_PB_H_MAX_SIZE meshtastic_SerialHalResponse_size
|
||||||
|
#define meshtastic_SerialHalCommand_size 541
|
||||||
|
#define meshtastic_SerialHalResponse_size 610
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} /* extern "C" */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
#include "DMShell.h"
|
||||||
|
|
||||||
|
#if defined(ARCH_PORTDUINO)
|
||||||
|
|
||||||
|
#include "Channels.h"
|
||||||
|
#include "MeshService.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "Throttle.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
#include "mesh/mesh-pb-constants.h"
|
||||||
|
#include "pb_decode.h"
|
||||||
|
#include "pb_encode.h"
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <pty.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
DMShellModule *dmShellModule;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr uint16_t PTY_COLS_DEFAULT = 120;
|
||||||
|
constexpr uint16_t PTY_ROWS_DEFAULT = 40;
|
||||||
|
constexpr size_t MAX_MESSAGE_SIZE = 200;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DMShellModule::DMShellModule()
|
||||||
|
: SinglePortModule("DMShellModule", meshtastic_PortNum_REMOTE_SHELL_APP), concurrency::OSThread("DMShell", 100)
|
||||||
|
{
|
||||||
|
LOG_WARN("DMShell enabled on Portduino: remote shell access is dangerous and intended for trusted debugging only");
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessMessage DMShellModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
|
{
|
||||||
|
meshtastic_RemoteShell frame = meshtastic_RemoteShell_init_zero;
|
||||||
|
if (!mp.pki_encrypted) {
|
||||||
|
LOG_WARN("DMShell: ignoring packet without PKI from 0x%x", mp.from);
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseFrame(mp, frame)) {
|
||||||
|
LOG_WARN("DMShell: ignoring malformed frame");
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.op == meshtastic_RemoteShell_OpCode_ACK) {
|
||||||
|
if (session.active && frame.session_id == session.sessionId && getFrom(&mp) == session.peer && frame.last_rx_seq > 0) {
|
||||||
|
resendFramesFrom(frame.last_rx_seq + 1);
|
||||||
|
}
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.op >= 64) {
|
||||||
|
LOG_WARN("DMShell: ignoring frame with op code %d, seq %d", frame.op, frame.seq);
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthorizedPacket(mp)) {
|
||||||
|
LOG_WARN("DMShell: unauthorized sender 0x%x, %u", mp.from, frame.op);
|
||||||
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp);
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.op == meshtastic_RemoteShell_OpCode_OPEN) {
|
||||||
|
LOG_WARN("DMShell: received OPEN from 0x%x sessionId=0x%x", mp.from, frame.session_id);
|
||||||
|
if (!openSession(mp, frame)) {
|
||||||
|
sendError("open_failed", getFrom(&mp));
|
||||||
|
}
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.active || frame.session_id != session.sessionId || getFrom(&mp) != session.peer) {
|
||||||
|
if (!session.active) {
|
||||||
|
LOG_WARN("DMShell: no active session, rejecting op %d from 0x%x", frame.op, mp.from);
|
||||||
|
} else {
|
||||||
|
LOG_WARN("DMShell: session ID mismatch (got 0x%x expected 0x%x) or peer mismatch (got 0x%x expected 0x%x), rejecting "
|
||||||
|
"op %d",
|
||||||
|
frame.session_id, session.sessionId, mp.from, session.peer, frame.op);
|
||||||
|
}
|
||||||
|
sendError("invalid_session", getFrom(&mp));
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldProcessIncomingFrame(frame)) {
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
|
||||||
|
switch (frame.op) {
|
||||||
|
case meshtastic_RemoteShell_OpCode_INPUT:
|
||||||
|
if (!writeSessionInput(frame)) {
|
||||||
|
sendError("input_write_failed");
|
||||||
|
} else {
|
||||||
|
uint8_t outBuf[MAX_MESSAGE_SIZE];
|
||||||
|
const ssize_t bytesRead = read(session.masterFd, outBuf, sizeof(outBuf));
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
LOG_WARN("DMShell: read %zd bytes from PTY", bytesRead);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_OUTPUT,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
assert(bytesRead <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, outBuf, bytesRead);
|
||||||
|
frame.payload.size = bytesRead;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case meshtastic_RemoteShell_OpCode_RESIZE:
|
||||||
|
if (frame.rows > 0 && frame.cols > 0) {
|
||||||
|
struct winsize ws = {};
|
||||||
|
ws.ws_row = frame.rows;
|
||||||
|
ws.ws_col = frame.cols;
|
||||||
|
if (session.masterFd >= 0) {
|
||||||
|
ioctl(session.masterFd, TIOCSWINSZ, &ws);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case meshtastic_RemoteShell_OpCode_PING: {
|
||||||
|
uint32_t peerLastRxSeq = frame.ack_seq;
|
||||||
|
if (frame.last_rx_seq > 0) {
|
||||||
|
peerLastRxSeq = frame.last_rx_seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t nextMissingForPeer = peerLastRxSeq + 1;
|
||||||
|
if (nextMissingForPeer > 0 && nextMissingForPeer < session.nextTxSeq) {
|
||||||
|
resendFramesFrom(nextMissingForPeer);
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_PONG,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
.last_tx_seq = session.nextTxSeq > 0 ? session.nextTxSeq - 1 : 0,
|
||||||
|
.last_rx_seq = session.lastAckedRxSeq,
|
||||||
|
};
|
||||||
|
frame.payload.size = 0;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case meshtastic_RemoteShell_OpCode_CLOSE:
|
||||||
|
closeSession("peer_close", true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendError("unsupported_op");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProcessMessage::STOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t DMShellModule::runOnce()
|
||||||
|
{
|
||||||
|
processPendingChildReap();
|
||||||
|
|
||||||
|
if (!session.active) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
reapChildIfExited();
|
||||||
|
if (!session.active) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Throttle::isWithinTimespanMs(session.lastActivityMs, SESSION_IDLE_TIMEOUT_MS) == false) {
|
||||||
|
closeSession("idle_timeout", true);
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RadioLibInterface::instance->packetsInTxQueue() > 1) {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t outBuf[MAX_MESSAGE_SIZE];
|
||||||
|
while (session.masterFd >= 0) {
|
||||||
|
const ssize_t bytesRead = read(session.masterFd, outBuf, sizeof(outBuf));
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
LOG_WARN("DMShell: read %zd bytes from PTY", bytesRead);
|
||||||
|
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_OUTPUT,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
assert(bytesRead <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, outBuf, bytesRead);
|
||||||
|
frame.payload.size = bytesRead;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
// continue;
|
||||||
|
// do we want to ack every data message, and only send the next on ack?
|
||||||
|
// would require some retry logic. Maybe re-use the wantAck bit
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead == 0) {
|
||||||
|
closeSession("pty_eof", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARN("DMShell: PTY read error errno=%d", errno);
|
||||||
|
closeSession("pty_read_error", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::parseFrame(const meshtastic_MeshPacket &mp, meshtastic_RemoteShell &outFrame)
|
||||||
|
{
|
||||||
|
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_RemoteShell_fields, &outFrame)) {
|
||||||
|
LOG_INFO("Received a DMShell message");
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Error decoding DMShell message!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::isAuthorizedPacket(const meshtastic_MeshPacket &mp) const
|
||||||
|
{
|
||||||
|
if (mp.from == 0) {
|
||||||
|
return !config.security.is_managed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
|
||||||
|
if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) {
|
||||||
|
return config.security.admin_channel_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mp.pki_encrypted) {
|
||||||
|
for (uint8_t i = 0; i < 3; ++i) {
|
||||||
|
if (config.security.admin_key[i].size == 32 &&
|
||||||
|
memcmp(mp.public_key.bytes, config.security.admin_key[i].bytes, 32) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::openSession(const meshtastic_MeshPacket &mp, const meshtastic_RemoteShell &frame)
|
||||||
|
{
|
||||||
|
if (session.active) {
|
||||||
|
closeSession("preempted", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
int masterFd = -1;
|
||||||
|
struct winsize ws = {};
|
||||||
|
if (frame.rows > 0) {
|
||||||
|
ws.ws_row = frame.rows;
|
||||||
|
} else {
|
||||||
|
ws.ws_row = PTY_ROWS_DEFAULT;
|
||||||
|
}
|
||||||
|
if (frame.cols > 0) {
|
||||||
|
ws.ws_col = frame.cols;
|
||||||
|
} else {
|
||||||
|
ws.ws_col = PTY_COLS_DEFAULT;
|
||||||
|
}
|
||||||
|
const pid_t childPid = forkpty(&masterFd, nullptr, nullptr, &ws);
|
||||||
|
if (childPid < 0) {
|
||||||
|
LOG_ERROR("DMShell: forkpty failed errno=%d", errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childPid == 0) {
|
||||||
|
const char *shell = getenv("SHELL");
|
||||||
|
if (!shell || !*shell) {
|
||||||
|
shell = "/bin/sh";
|
||||||
|
}
|
||||||
|
execl(shell, shell, "-i", static_cast<char *>(nullptr));
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int flags = fcntl(masterFd, F_GETFL, 0);
|
||||||
|
if (flags >= 0) {
|
||||||
|
fcntl(masterFd, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.active = true;
|
||||||
|
session.sessionId = (frame.session_id != 0) ? frame.session_id : static_cast<uint32_t>(random(1, 0x7fffffff));
|
||||||
|
session.peer = getFrom(&mp);
|
||||||
|
session.channel = mp.channel;
|
||||||
|
session.masterFd = masterFd;
|
||||||
|
session.childPid = childPid;
|
||||||
|
session.nextTxSeq = 1;
|
||||||
|
session.lastAckedRxSeq = frame.seq;
|
||||||
|
session.nextExpectedRxSeq = frame.seq + 1;
|
||||||
|
session.highestSeenRxSeq = frame.seq;
|
||||||
|
session.lastActivityMs = millis();
|
||||||
|
|
||||||
|
meshtastic_RemoteShell newFrame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_OPEN_OK,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = frame.seq,
|
||||||
|
.cols = ws.ws_col,
|
||||||
|
.rows = ws.ws_row,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
newFrame.payload.size = 0;
|
||||||
|
sendFrameToPeer(session.peer, newFrame, true);
|
||||||
|
|
||||||
|
LOG_INFO("DMShell: opened session=0x%x peer=0x%x pid=%d", session.sessionId, session.peer, session.childPid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::writeSessionInput(const meshtastic_RemoteShell &frame)
|
||||||
|
{
|
||||||
|
if (session.masterFd < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frame.payload.size == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssize_t bytesWritten = write(session.masterFd, frame.payload.bytes, frame.payload.size);
|
||||||
|
return bytesWritten >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::closeSession(const char *reason, bool notifyPeer)
|
||||||
|
{
|
||||||
|
if (!session.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyPeer) {
|
||||||
|
const size_t reasonLen = strnlen(reason, 256);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_CLOSED,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
assert(reasonLen <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, reason, reasonLen);
|
||||||
|
frame.payload.size = reasonLen;
|
||||||
|
sendFrameToPeer(session.peer, frame, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.masterFd >= 0) {
|
||||||
|
close(session.masterFd);
|
||||||
|
session.masterFd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.childPid > 0) {
|
||||||
|
// Run this to avoid forgetting a child
|
||||||
|
processPendingChildReap();
|
||||||
|
|
||||||
|
if (kill(session.childPid, SIGTERM) < 0 && errno != ESRCH) {
|
||||||
|
LOG_WARN("DMShell: failed to send SIGTERM to pid=%d errno=%d", session.childPid, errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingChildPid = session.childPid;
|
||||||
|
session.childPid = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("DMShell: closed session=0x%x reason=%s", session.sessionId, reason);
|
||||||
|
session = DMShellSession{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::reapChildIfExited()
|
||||||
|
{
|
||||||
|
if (!session.active || session.childPid <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
const pid_t result = waitpid(session.childPid, &status, WNOHANG);
|
||||||
|
if (result == session.childPid) {
|
||||||
|
closeSession("shell_exited", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::processPendingChildReap()
|
||||||
|
{
|
||||||
|
if (pendingChildPid <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
const pid_t result = waitpid(pendingChildPid, &status, WNOHANG);
|
||||||
|
|
||||||
|
if (result == pendingChildPid || (result < 0 && errno == ECHILD)) {
|
||||||
|
pendingChildPid = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result < 0) {
|
||||||
|
LOG_WARN("DMShell: waitpid failed for pid=%d errno=%d", pendingChildPid, errno);
|
||||||
|
pendingChildPid = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingChildPid > 0) {
|
||||||
|
if (kill(pendingChildPid, SIGKILL) < 0 && errno != ESRCH) {
|
||||||
|
LOG_WARN("DMShell: failed to send SIGKILL to pid=%d errno=%d", pendingChildPid, errno);
|
||||||
|
}
|
||||||
|
pendingChildPid = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::rememberSentFrame(meshtastic_RemoteShell frame)
|
||||||
|
{
|
||||||
|
if (frame.seq == 0 || frame.op == meshtastic_RemoteShell_OpCode_ACK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &entry = session.txHistory[session.txHistoryNext];
|
||||||
|
entry.valid = true;
|
||||||
|
entry.op = frame.op;
|
||||||
|
entry.sessionId = frame.session_id;
|
||||||
|
entry.seq = frame.seq;
|
||||||
|
entry.ackSeq = frame.ack_seq;
|
||||||
|
entry.cols = frame.cols;
|
||||||
|
entry.rows = frame.rows;
|
||||||
|
entry.flags = frame.flags;
|
||||||
|
entry.payloadLen = frame.payload.size;
|
||||||
|
if (frame.payload.size > 0) {
|
||||||
|
memcpy(entry.payload, frame.payload.bytes, frame.payload.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.txHistoryNext = (session.txHistoryNext + 1) % session.txHistory.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::resendFramesFrom(uint32_t startSeq)
|
||||||
|
{
|
||||||
|
if (startSeq == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DMShellSession::SentFrame *match = nullptr;
|
||||||
|
for (auto &entry : session.txHistory) {
|
||||||
|
if (!entry.valid || entry.seq != startSeq) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match = &entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
LOG_WARN("DMShell: replay request for seq=%u not found in history", startSeq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("DMShell: replaying frame seq=%u op=%d", match->seq, match->op);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = match->op,
|
||||||
|
.session_id = match->sessionId,
|
||||||
|
.seq = match->seq,
|
||||||
|
.ack_seq = match->ackSeq,
|
||||||
|
.cols = match->cols,
|
||||||
|
.rows = match->rows,
|
||||||
|
.flags = match->flags,
|
||||||
|
};
|
||||||
|
assert(match->payloadLen <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, match->payload, match->payloadLen);
|
||||||
|
frame.payload.size = match->payloadLen;
|
||||||
|
sendFrameToPeer(session.peer, frame, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::sendAck(uint32_t replayFromSeq)
|
||||||
|
{
|
||||||
|
if (replayFromSeq > 0) {
|
||||||
|
LOG_WARN("DMShell: requesting replay from seq=%u", replayFromSeq);
|
||||||
|
}
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_ACK,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = 0,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
.last_rx_seq = replayFromSeq - 1,
|
||||||
|
};
|
||||||
|
frame.payload.size = 0;
|
||||||
|
sendFrameToPeer(session.peer, frame, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DMShellModule::shouldProcessIncomingFrame(const meshtastic_RemoteShell &frame)
|
||||||
|
{
|
||||||
|
if (frame.seq == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.seq < session.nextExpectedRxSeq) {
|
||||||
|
if (session.highestSeenRxSeq >= session.nextExpectedRxSeq) {
|
||||||
|
sendAck(session.nextExpectedRxSeq);
|
||||||
|
} else {
|
||||||
|
sendAck();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.seq > session.nextExpectedRxSeq) {
|
||||||
|
if (frame.seq > session.highestSeenRxSeq) {
|
||||||
|
session.highestSeenRxSeq = frame.seq;
|
||||||
|
}
|
||||||
|
sendAck(session.nextExpectedRxSeq);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.lastAckedRxSeq = frame.seq;
|
||||||
|
session.nextExpectedRxSeq = frame.seq + 1;
|
||||||
|
if (frame.seq > session.highestSeenRxSeq) {
|
||||||
|
session.highestSeenRxSeq = frame.seq;
|
||||||
|
}
|
||||||
|
if (session.highestSeenRxSeq >= session.nextExpectedRxSeq) {
|
||||||
|
sendAck(session.nextExpectedRxSeq);
|
||||||
|
} else {
|
||||||
|
session.highestSeenRxSeq = 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::sendFrameToPeer(NodeNum peer, meshtastic_RemoteShell frame, bool remember)
|
||||||
|
{
|
||||||
|
meshtastic_MeshPacket *packet = allocDataPacket();
|
||||||
|
if (!packet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG_WARN("DMShell: building packet op=%u session=0x%x seq=%u payloadLen=%zu", frame.op, frame.session_id, frame.seq,
|
||||||
|
frame.payload.size);
|
||||||
|
const size_t encoded = pb_encode_to_bytes(packet->decoded.payload.bytes, sizeof(packet->decoded.payload.bytes),
|
||||||
|
meshtastic_RemoteShell_fields, &frame);
|
||||||
|
if (encoded == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packet->decoded.payload.size = encoded;
|
||||||
|
|
||||||
|
if (remember) {
|
||||||
|
rememberSentFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
packet->to = peer;
|
||||||
|
packet->hop_limit = 0;
|
||||||
|
packet->hop_start = 0;
|
||||||
|
packet->channel = 0;
|
||||||
|
packet->want_ack = false;
|
||||||
|
packet->pki_encrypted = true;
|
||||||
|
packet->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||||
|
service->sendToMesh(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DMShellModule::sendError(const char *message, NodeNum peer)
|
||||||
|
{
|
||||||
|
const size_t len = strnlen(message, MAX_MESSAGE_SIZE);
|
||||||
|
meshtastic_RemoteShell frame = {
|
||||||
|
.op = meshtastic_RemoteShell_OpCode_ERROR,
|
||||||
|
.session_id = session.sessionId,
|
||||||
|
.seq = session.nextTxSeq++,
|
||||||
|
.ack_seq = session.lastAckedRxSeq,
|
||||||
|
.cols = 0,
|
||||||
|
.rows = 0,
|
||||||
|
.flags = 0,
|
||||||
|
};
|
||||||
|
if (message && len > 0) {
|
||||||
|
assert(len <= sizeof(frame.payload.bytes));
|
||||||
|
memcpy(frame.payload.bytes, message, len);
|
||||||
|
frame.payload.size = len;
|
||||||
|
}
|
||||||
|
if (peer == 0) {
|
||||||
|
peer = session.peer;
|
||||||
|
}
|
||||||
|
sendFrameToPeer(peer, frame, true);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MeshModule.h"
|
||||||
|
#include "Router.h"
|
||||||
|
#include "SinglePortModule.h"
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#if defined(ARCH_PORTDUINO)
|
||||||
|
|
||||||
|
struct DMShellSession {
|
||||||
|
bool active = false;
|
||||||
|
uint32_t sessionId = 0;
|
||||||
|
NodeNum peer = 0;
|
||||||
|
uint8_t channel = 0;
|
||||||
|
int masterFd = -1;
|
||||||
|
int childPid = -1;
|
||||||
|
uint32_t nextTxSeq = 1;
|
||||||
|
uint32_t lastAckedRxSeq = 0;
|
||||||
|
uint32_t nextExpectedRxSeq = 1;
|
||||||
|
uint32_t highestSeenRxSeq = 0;
|
||||||
|
uint32_t lastActivityMs = 0;
|
||||||
|
struct SentFrame {
|
||||||
|
bool valid = false;
|
||||||
|
meshtastic_RemoteShell_OpCode op = meshtastic_RemoteShell_OpCode_ERROR;
|
||||||
|
uint32_t sessionId = 0;
|
||||||
|
uint32_t seq = 0;
|
||||||
|
uint32_t ackSeq = 0;
|
||||||
|
uint32_t cols = 0;
|
||||||
|
uint32_t rows = 0;
|
||||||
|
uint32_t flags = 0;
|
||||||
|
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
|
||||||
|
size_t payloadLen = 0;
|
||||||
|
};
|
||||||
|
std::array<SentFrame, 50> txHistory = {};
|
||||||
|
size_t txHistoryNext = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DMShellModule : private concurrency::OSThread, public SinglePortModule
|
||||||
|
{
|
||||||
|
|
||||||
|
public:
|
||||||
|
DMShellModule();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr uint32_t SESSION_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
DMShellSession session;
|
||||||
|
pid_t pendingChildPid = -1;
|
||||||
|
|
||||||
|
bool parseFrame(const meshtastic_MeshPacket &mp, meshtastic_RemoteShell &outFrame);
|
||||||
|
bool isAuthorizedPacket(const meshtastic_MeshPacket &mp) const;
|
||||||
|
bool openSession(const meshtastic_MeshPacket &mp, const meshtastic_RemoteShell &frame);
|
||||||
|
bool shouldProcessIncomingFrame(const meshtastic_RemoteShell &frame);
|
||||||
|
bool writeSessionInput(const meshtastic_RemoteShell &frame);
|
||||||
|
void closeSession(const char *reason, bool notifyPeer);
|
||||||
|
void reapChildIfExited();
|
||||||
|
void processPendingChildReap();
|
||||||
|
|
||||||
|
void rememberSentFrame(meshtastic_RemoteShell frame);
|
||||||
|
void resendFramesFrom(uint32_t startSeq);
|
||||||
|
void sendAck(uint32_t replayFromSeq = 0);
|
||||||
|
void sendFrameToPeer(NodeNum peer, meshtastic_RemoteShell frame, bool remember = true);
|
||||||
|
void sendError(const char *message, NodeNum peer = 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern DMShellModule *dmShellModule;
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -49,6 +49,7 @@
|
|||||||
#include "modules/WaypointModule.h"
|
#include "modules/WaypointModule.h"
|
||||||
#endif
|
#endif
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
|
#include "modules/DMShell.h"
|
||||||
#include "modules/Telemetry/HostMetrics.h"
|
#include "modules/Telemetry/HostMetrics.h"
|
||||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||||
#include "modules/StoreForwardModule.h"
|
#include "modules/StoreForwardModule.h"
|
||||||
@@ -195,6 +196,7 @@ void setupModules()
|
|||||||
#endif
|
#endif
|
||||||
#if ARCH_PORTDUINO
|
#if ARCH_PORTDUINO
|
||||||
new HostMetricsModule();
|
new HostMetricsModule();
|
||||||
|
dmShellModule = new DMShellModule();
|
||||||
#endif
|
#endif
|
||||||
#if HAS_TELEMETRY
|
#if HAS_TELEMETRY
|
||||||
new DeviceTelemetryModule();
|
new DeviceTelemetryModule();
|
||||||
|
|||||||
@@ -350,6 +350,8 @@ inline bool isConnectedToNetwork()
|
|||||||
return WiFi.isConnected();
|
return WiFi.isConnected();
|
||||||
#elif HAS_ETHERNET
|
#elif HAS_ETHERNET
|
||||||
return Ethernet.linkStatus() == LinkON;
|
return Ethernet.linkStatus() == LinkON;
|
||||||
|
#elif defined(ARCH_PORTDUINO)
|
||||||
|
return true;
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -127,6 +127,60 @@ void test_client_uses_public_channel_minimums()
|
|||||||
TEST_ASSERT_EQUAL_UINT32(60 * 60, position);
|
TEST_ASSERT_EQUAL_UINT32(60 * 60, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Saturation/clamp tests for getConfiguredOrDefaultMs[Scaled] ---
|
||||||
|
// These guard the INT32_MAX clamp added to avoid uint32 wrap of secs*1000 and
|
||||||
|
// to keep results safe to cast to int32_t for OSThread runOnce returns.
|
||||||
|
|
||||||
|
void test_ms_below_threshold()
|
||||||
|
{
|
||||||
|
// Ordinary value passes through unchanged.
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(60000U, Default::getConfiguredOrDefaultMs(60, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ms_at_threshold()
|
||||||
|
{
|
||||||
|
// INT32_MAX / 1000 = 2,147,483 — largest secs that does not clamp.
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(2147483000U, Default::getConfiguredOrDefaultMs(2147483U, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ms_just_above_threshold()
|
||||||
|
{
|
||||||
|
// One second over the boundary must saturate, not wrap.
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), Default::getConfiguredOrDefaultMs(2147484U, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ms_uint32_max()
|
||||||
|
{
|
||||||
|
// default_sds_secs == UINT32_MAX on non-routers must not wrap.
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), Default::getConfiguredOrDefaultMs(UINT32_MAX, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ms_default_clamps()
|
||||||
|
{
|
||||||
|
// Clamp also applies when the default-arg path is taken (configured == 0).
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), Default::getConfiguredOrDefaultMs(0, UINT32_MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_ms_result_is_int32_safe()
|
||||||
|
{
|
||||||
|
// Regression guard for runOnce returns: cast to int32_t must not go negative.
|
||||||
|
int32_t result = static_cast<int32_t>(Default::getConfiguredOrDefaultMs(UINT32_MAX, 0));
|
||||||
|
TEST_ASSERT_GREATER_OR_EQUAL_INT32(0, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_scaled_overflow_saturates()
|
||||||
|
{
|
||||||
|
// long_fast (SF11/BW250) with a 24h base and heavy congestion overflows
|
||||||
|
// the uint32 result without the double-precision guard. Must saturate.
|
||||||
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||||
|
config.lora.use_preset = false;
|
||||||
|
config.lora.spread_factor = 11;
|
||||||
|
config.lora.bandwidth = 250;
|
||||||
|
|
||||||
|
uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, ONE_DAY, 1000);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), res);
|
||||||
|
}
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
// Small delay to match other test mains
|
// Small delay to match other test mains
|
||||||
@@ -140,6 +194,13 @@ void setup()
|
|||||||
RUN_TEST(test_router_uses_router_minimums);
|
RUN_TEST(test_router_uses_router_minimums);
|
||||||
RUN_TEST(test_router_late_uses_router_minimums);
|
RUN_TEST(test_router_late_uses_router_minimums);
|
||||||
RUN_TEST(test_client_uses_public_channel_minimums);
|
RUN_TEST(test_client_uses_public_channel_minimums);
|
||||||
|
RUN_TEST(test_ms_below_threshold);
|
||||||
|
RUN_TEST(test_ms_at_threshold);
|
||||||
|
RUN_TEST(test_ms_just_above_threshold);
|
||||||
|
RUN_TEST(test_ms_uint32_max);
|
||||||
|
RUN_TEST(test_ms_default_clamps);
|
||||||
|
RUN_TEST(test_ms_result_is_int32_safe);
|
||||||
|
RUN_TEST(test_scaled_overflow_saturates);
|
||||||
exit(UNITY_END());
|
exit(UNITY_END());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "../test_helpers.h"
|
||||||
|
#include "mesh/mesh-pb-constants.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct BytesDecodeState {
|
||||||
|
uint8_t *buffer;
|
||||||
|
size_t capacity;
|
||||||
|
size_t length;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BytesEncodeState {
|
||||||
|
const uint8_t *buffer;
|
||||||
|
size_t length;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool decodeBytesField(pb_istream_t *stream, const pb_field_iter_t *field, void **arg)
|
||||||
|
{
|
||||||
|
(void)field;
|
||||||
|
auto *state = static_cast<BytesDecodeState *>(*arg);
|
||||||
|
if (!state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t fieldLen = stream->bytes_left;
|
||||||
|
if (fieldLen > state->capacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pb_read(stream, state->buffer, fieldLen)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->length = fieldLen;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool encodeBytesField(pb_ostream_t *stream, const pb_field_iter_t *field, void *const *arg)
|
||||||
|
{
|
||||||
|
auto *state = static_cast<const BytesEncodeState *>(*arg);
|
||||||
|
if (!state || !state->buffer || state->length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pb_encode_tag_for_field(stream, field)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pb_encode_string(stream, state->buffer, state->length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void assert_dmshell_roundtrip(meshtastic_RemoteShell_OpCode op, uint32_t sessionId, uint32_t seq, const uint8_t *payload,
|
||||||
|
size_t payloadLen, uint32_t cols = 0, uint32_t rows = 0)
|
||||||
|
{
|
||||||
|
meshtastic_RemoteShell tx = meshtastic_RemoteShell_init_zero;
|
||||||
|
tx.op = op;
|
||||||
|
tx.session_id = sessionId;
|
||||||
|
tx.seq = seq;
|
||||||
|
tx.cols = cols;
|
||||||
|
tx.rows = rows;
|
||||||
|
|
||||||
|
uint8_t encoded[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
|
||||||
|
size_t encodedLen = pb_encode_to_bytes(encoded, sizeof(encoded), meshtastic_RemoteShell_fields, &tx);
|
||||||
|
TEST_ASSERT_GREATER_THAN_UINT32(0, encodedLen);
|
||||||
|
|
||||||
|
meshtastic_RemoteShell rx = meshtastic_RemoteShell_init_zero;
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(pb_decode_from_bytes(encoded, encodedLen, meshtastic_RemoteShell_fields, &rx));
|
||||||
|
TEST_ASSERT_EQUAL(op, rx.op);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(sessionId, rx.session_id);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(seq, rx.seq);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(cols, rx.cols);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(rows, rx.rows);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void test_dmshell_open_roundtrip()
|
||||||
|
{
|
||||||
|
assert_dmshell_roundtrip(meshtastic_RemoteShell_OpCode_OPEN, 0x101, 1, nullptr, 0, 120, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_dmshell_input_roundtrip()
|
||||||
|
{
|
||||||
|
const uint8_t payload[] = {'l', 's', '\n'};
|
||||||
|
assert_dmshell_roundtrip(meshtastic_RemoteShell_OpCode_INPUT, 0x202, 2, payload, sizeof(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_dmshell_resize_roundtrip()
|
||||||
|
{
|
||||||
|
assert_dmshell_roundtrip(meshtastic_RemoteShell_OpCode_RESIZE, 0x303, 3, nullptr, 0, 180, 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_dmshell_close_roundtrip()
|
||||||
|
{
|
||||||
|
const uint8_t reason[] = {'b', 'y', 'e'};
|
||||||
|
assert_dmshell_roundtrip(meshtastic_RemoteShell_OpCode_CLOSE, 0x404, 4, reason, sizeof(reason));
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@ void test_telemetry_environment_metrics_complete_coverage();
|
|||||||
void test_telemetry_environment_metrics_unset_fields();
|
void test_telemetry_environment_metrics_unset_fields();
|
||||||
void test_encrypted_packet_serialization();
|
void test_encrypted_packet_serialization();
|
||||||
void test_empty_encrypted_packet();
|
void test_empty_encrypted_packet();
|
||||||
|
void test_dmshell_open_roundtrip();
|
||||||
|
void test_dmshell_input_roundtrip();
|
||||||
|
void test_dmshell_resize_roundtrip();
|
||||||
|
void test_dmshell_close_roundtrip();
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
@@ -52,6 +56,12 @@ void setup()
|
|||||||
RUN_TEST(test_encrypted_packet_serialization);
|
RUN_TEST(test_encrypted_packet_serialization);
|
||||||
RUN_TEST(test_empty_encrypted_packet);
|
RUN_TEST(test_empty_encrypted_packet);
|
||||||
|
|
||||||
|
// DMShell protobuf transport tests
|
||||||
|
RUN_TEST(test_dmshell_open_roundtrip);
|
||||||
|
RUN_TEST(test_dmshell_input_roundtrip);
|
||||||
|
RUN_TEST(test_dmshell_resize_roundtrip);
|
||||||
|
RUN_TEST(test_dmshell_close_roundtrip);
|
||||||
|
|
||||||
UNITY_END();
|
UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user