mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-04 11:28:11 +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
|
||||
- **STM32WL** - STM32 with integrated LoRa
|
||||
- **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
|
||||
|
||||
@@ -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`
|
||||
- **Strongly recommended**: `rg` (ripgrep) for fast file/text search, `jq` for JSON processing
|
||||
- **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:
|
||||
|
||||
@@ -388,6 +389,7 @@ Build commands:
|
||||
pio run -e tbeam # Build specific target
|
||||
pio run -e tbeam -t upload # Build and upload
|
||||
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
|
||||
|
||||
@@ -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
|
||||
id: sanitize_platform
|
||||
# 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
|
||||
if: ${{ inputs.push }}
|
||||
|
||||
@@ -43,6 +43,15 @@ jobs:
|
||||
push: true
|
||||
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:
|
||||
uses: ./.github/workflows/docker_build.yml
|
||||
with:
|
||||
@@ -70,16 +79,27 @@ jobs:
|
||||
push: true
|
||||
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:
|
||||
needs:
|
||||
# Debian
|
||||
- docker-debian-amd64
|
||||
- docker-debian-arm64
|
||||
- docker-debian-armv7
|
||||
- docker-debian-riscv64
|
||||
# Alpine
|
||||
- docker-alpine-amd64
|
||||
- docker-alpine-arm64
|
||||
- docker-alpine-armv7
|
||||
- docker-alpine-riscv64
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -162,6 +182,7 @@ jobs:
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-amd64.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-arm64.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-armv7.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-debian-riscv64.outputs.digest }}
|
||||
|
||||
- name: Docker meta (Alpine)
|
||||
id: meta_alpine
|
||||
@@ -182,3 +203,4 @@ jobs:
|
||||
meshtastic/meshtasticd@${{ needs.docker-alpine-amd64.outputs.digest }}
|
||||
meshtastic/meshtasticd@${{ needs.docker-alpine-arm64.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
|
||||
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:
|
||||
if: ${{ github.repository == 'meshtastic/firmware' && github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/package_pio_deps.yml
|
||||
@@ -286,6 +300,7 @@ jobs:
|
||||
- gather-artifacts
|
||||
- build-debian-src
|
||||
- package-pio-deps-native-tft
|
||||
# - MacOS
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
+3
-3
@@ -4,12 +4,12 @@ cli:
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.7.6
|
||||
ref: v1.8.0
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.525
|
||||
- renovate@43.142.0
|
||||
- renovate@43.150.0
|
||||
- prettier@3.8.3
|
||||
- trufflehog@3.95.2
|
||||
- yamllint@1.38.0
|
||||
@@ -36,7 +36,7 @@ lint:
|
||||
- bin/**
|
||||
runtimes:
|
||||
enabled:
|
||||
- python@3.10.8
|
||||
- python@3.14.4
|
||||
- go@1.21.0
|
||||
- node@22.16.0
|
||||
actions:
|
||||
|
||||
@@ -10,17 +10,18 @@ This file (`AGENTS.md`) is a short pointer + quick reference for agents that don
|
||||
|
||||
## Quick command reference
|
||||
|
||||
| Action | Command |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| Build a firmware variant | `pio run -e <env>` (e.g. `pio run -e rak4631`, `pio run -e heltec-v3`) |
|
||||
| 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) |
|
||||
| Run firmware unit tests (native) | `pio test -e native` |
|
||||
| Run MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||
| Format before commit | `trunk fmt` |
|
||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||
| Action | Command |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| 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>` |
|
||||
| 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 MCP hardware tests | `./mcp-server/run-tests.sh` |
|
||||
| Live TUI test runner | `mcp-server/.venv/bin/meshtastic-mcp-test-tui` |
|
||||
| Format before commit | `trunk fmt` |
|
||||
| Regenerate protobuf bindings | `bin/regen-protos.sh` |
|
||||
| Generate CI matrix | `./bin/generate_ci_matrix.py all [--level pr]` |
|
||||
|
||||
## MCP server (device + test automation)
|
||||
|
||||
@@ -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.
|
||||
- **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.
|
||||
- **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)
|
||||
|
||||
|
||||
+3
-2
@@ -3,7 +3,8 @@
|
||||
# trunk-ignore-all(hadolint/DL3018): Do not pin apk 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
|
||||
ENV PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
@@ -60,4 +61,4 @@ EXPOSE 4403
|
||||
|
||||
CMD [ "sh", "-cx", "meshtasticd --fsdir=/var/lib/meshtasticd" ]
|
||||
|
||||
HEALTHCHECK NONE
|
||||
HEALTHCHECK NONE
|
||||
|
||||
@@ -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
|
||||
set -e
|
||||
export DEBEMAIL="jbennett@incomsystems.biz"
|
||||
export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
|
||||
export PLATFORMIO_PACKAGES_DIR=pio/packages
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ lib_deps =
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
# 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
|
||||
[environmental_base]
|
||||
|
||||
+1
-1
Submodule protobufs updated: 249a80855a...1d6f1a71ff
+236
-456
@@ -505,15 +505,12 @@ bool GPS::setup()
|
||||
int msglen = 0;
|
||||
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (probeTries < GPS_PROBETRIES) {
|
||||
bootString = "Probing GPS...";
|
||||
gnssModel = probe(serialSpeeds[speedSelect]);
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
||||
speedSelect = 0;
|
||||
++probeTries;
|
||||
}
|
||||
} else {
|
||||
currentStep = 0;
|
||||
}
|
||||
}
|
||||
// Rare Serial Speeds
|
||||
@@ -525,8 +522,6 @@ bool GPS::setup()
|
||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
currentStep = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -543,133 +538,86 @@ bool GPS::setup()
|
||||
* 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.
|
||||
* */
|
||||
if (currentStep == 0) {
|
||||
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
|
||||
_serial_gps->write("$PCAS04,7*1E\r\n");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// 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");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
|
||||
_serial_gps->write("$PCAS11,3*1E\r\n");
|
||||
currentDelay = 250;
|
||||
}
|
||||
|
||||
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
|
||||
_serial_gps->write("$PCAS04,7*1E\r\n");
|
||||
delay(250);
|
||||
// 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");
|
||||
delay(250);
|
||||
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
|
||||
_serial_gps->write("$PCAS11,3*1E\r\n");
|
||||
delay(250);
|
||||
} else if (gnssModel == GNSS_MODEL_MTK_L76B) {
|
||||
// Waveshare Pico-GPS hat uses the L76B with 9600 baud
|
||||
// Initialize the L76B Chip, use GPS + GLONASS
|
||||
// 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");
|
||||
currentStep++;
|
||||
currentDelay = 1000;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// Above command will reset the GPS and takes longer before it will accept new commands
|
||||
// only ask for RMC and GGA (GNRMC and GNGGA)
|
||||
// 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");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
// Enable SBAS
|
||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 3) {
|
||||
// Enable PPS for 2D/3D fix only
|
||||
_serial_gps->write("$PMTK285,3,100*3F\r\n");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 4) {
|
||||
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
|
||||
_serial_gps->write("$PMTK886,1*29\r\n");
|
||||
currentDelay = 250;
|
||||
}
|
||||
_serial_gps->write("$PMTK353,1,1,0,0,0*2B\r\n");
|
||||
// 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)
|
||||
// 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");
|
||||
delay(250);
|
||||
// Enable SBAS
|
||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||
delay(250);
|
||||
// Enable PPS for 2D/3D fix only
|
||||
_serial_gps->write("$PMTK285,3,100*3F\r\n");
|
||||
delay(250);
|
||||
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
|
||||
_serial_gps->write("$PMTK886,1*29\r\n");
|
||||
delay(250);
|
||||
} else if (gnssModel == GNSS_MODEL_MTK_PA1010D) {
|
||||
// PA1010D is used in the Pimoroni GPS board.
|
||||
if (currentStep == 0) {
|
||||
// Enable all constellations.
|
||||
_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
|
||||
// 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");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
// Enable SBAS / WAAS
|
||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||
currentDelay = 250;
|
||||
}
|
||||
|
||||
// Enable all constellations.
|
||||
_serial_gps->write("$PMTK353,1,1,1,1,1*2A\r\n");
|
||||
// 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)
|
||||
_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");
|
||||
delay(250);
|
||||
// Enable SBAS / WAAS
|
||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||
delay(250);
|
||||
} else if (gnssModel == GNSS_MODEL_MTK_PA1616S) {
|
||||
// PA1616S is used in some GPS breakout boards from Adafruit
|
||||
// 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");
|
||||
currentStep++;
|
||||
currentDelay = 1000;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// Above command will reset the GPS and takes longer before it will accept new commands
|
||||
// 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");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
// Enable SBAS / WAAS
|
||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||
currentDelay = 250;
|
||||
}
|
||||
_serial_gps->write("$PMTK353,1,0,0,0,0*2A\r\n");
|
||||
// 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)
|
||||
_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");
|
||||
delay(250);
|
||||
// Enable SBAS / WAAS
|
||||
_serial_gps->write("$PMTK301,2*2E\r\n");
|
||||
delay(250);
|
||||
} else if (gnssModel == GNSS_MODEL_ATGM336H) {
|
||||
if (currentStep == 0) {
|
||||
// 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);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not set Config");
|
||||
}
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// Set the update frequency to 1Hz
|
||||
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not set Update Frequency");
|
||||
}
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
// Set the NEMA output messages - Ask for only RMC and GGA
|
||||
// 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);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x07, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not set Config");
|
||||
}
|
||||
|
||||
// Set the update frequency to 1Hz
|
||||
msglen = makeCASPacket(0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x04, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not set Update Frequency");
|
||||
}
|
||||
|
||||
// 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
|
||||
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);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
if (getACKCas(0x06, 0x01, 250) != GNSS_RESPONSE_OK) {
|
||||
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", CAS_NEMA_RMC);
|
||||
}
|
||||
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);
|
||||
LOG_WARN("ATGM336H: Could not enable NMEA MSG: %d", fields[i]);
|
||||
}
|
||||
}
|
||||
} else if (gnssModel == GNSS_MODEL_UC6580) {
|
||||
@@ -677,363 +625,195 @@ bool GPS::setup()
|
||||
// use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS
|
||||
// This will reset the receiver, so wait a bit afterwards
|
||||
// The paranoid will wait for the OK*04 confirmation response after each command.
|
||||
if (currentStep == 0) {
|
||||
_serial_gps->write("$CFGSYS,h35155\r\n");
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// 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.
|
||||
_serial_gps->write("$CFGMSG,0,3,0\r\n");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
// Turn off GSA messages, TinyGPS++ doesn't use this message.
|
||||
_serial_gps->write("$CFGMSG,0,2,0\r\n");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 3) {
|
||||
// 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");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 4) {
|
||||
_serial_gps->write("$CFGMSG,6,1,0\r\n");
|
||||
currentDelay = 250;
|
||||
}
|
||||
_serial_gps->write("$CFGSYS,h35155\r\n");
|
||||
delay(750);
|
||||
// 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.
|
||||
_serial_gps->write("$CFGMSG,0,3,0\r\n");
|
||||
delay(250);
|
||||
// Turn off GSA messages, TinyGPS++ doesn't use this message.
|
||||
_serial_gps->write("$CFGMSG,0,2,0\r\n");
|
||||
delay(250);
|
||||
// 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");
|
||||
delay(250);
|
||||
_serial_gps->write("$CFGMSG,6,1,0\r\n");
|
||||
delay(250);
|
||||
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_AG3335, GNSS_MODEL_AG3352)) {
|
||||
if (currentStep == 0) {
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
|
||||
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
|
||||
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||
// 1 0 1 0 0 1
|
||||
} else {
|
||||
_serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
|
||||
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||
// 1 1 1 1 0 0
|
||||
}
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// Configure NMEA (sentences will output once per fix)
|
||||
_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
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 3) {
|
||||
_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
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 5) {
|
||||
_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
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 7) {
|
||||
_serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 8) {
|
||||
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
|
||||
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_IN ||
|
||||
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
|
||||
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||
// 1 0 1 0 0 1
|
||||
} else {
|
||||
_serial_gps->write("$PAIR066,1,1,1,1,0,0*3A\r\n"); // Enable GPS+GLONASS+GALILEO+BDS
|
||||
// GPS GLONASS GALILEO BDS QZSS NAVIC
|
||||
// 1 1 1 1 0 0
|
||||
}
|
||||
// Configure NMEA (sentences will output once per fix)
|
||||
_serial_gps->write("$PAIR062,0,1*3F\r\n"); // GGA ON
|
||||
_serial_gps->write("$PAIR062,1,0*3F\r\n"); // GLL OFF
|
||||
_serial_gps->write("$PAIR062,2,0*3C\r\n"); // GSA OFF
|
||||
_serial_gps->write("$PAIR062,3,0*3D\r\n"); // GSV OFF
|
||||
_serial_gps->write("$PAIR062,4,1*3B\r\n"); // RMC ON
|
||||
_serial_gps->write("$PAIR062,5,0*3B\r\n"); // VTG OFF
|
||||
_serial_gps->write("$PAIR062,6,0*38\r\n"); // ZDA ON
|
||||
|
||||
delay(250);
|
||||
_serial_gps->write("$PAIR513*3D\r\n"); // save configuration
|
||||
} else if (gnssModel == GNSS_MODEL_UBLOX6) {
|
||||
if (currentStep == 0) {
|
||||
clearBuffer();
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
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
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 5) {
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 7) {
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 9) {
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 10) {
|
||||
clearBuffer();
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 12) {
|
||||
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);
|
||||
_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 config saved!");
|
||||
}
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
||||
|
||||
// Turn off unwanted NMEA messages, set update rate
|
||||
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_AID, "disable UBX-AID", 500);
|
||||
|
||||
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 config saved!");
|
||||
}
|
||||
} else if (IS_ONE_OF(gnssModel, GNSS_MODEL_UBLOX7, GNSS_MODEL_UBLOX8, GNSS_MODEL_UBLOX9)) {
|
||||
if (currentStep == 0) {
|
||||
if (gnssModel == GNSS_MODEL_UBLOX7) {
|
||||
LOG_DEBUG("Set GPS+SBAS");
|
||||
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
} else { // 8,9
|
||||
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
}
|
||||
|
||||
if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) {
|
||||
// It's not critical if the module doesn't acknowledge this configuration.
|
||||
LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?");
|
||||
} else {
|
||||
if (gnssModel == GNSS_MODEL_UBLOX7) {
|
||||
LOG_DEBUG("Set GPS+SBAS");
|
||||
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_7), _message_GNSS_7);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
LOG_INFO("GPS+SBAS configured");
|
||||
} else { // 8,9
|
||||
msglen = makeUBXPacket(0x06, 0x3e, sizeof(_message_GNSS_8), _message_GNSS_8);
|
||||
_serial_gps->write(UBXscratch, msglen);
|
||||
LOG_INFO("GPS+SBAS+GLONASS+Galileo configured");
|
||||
}
|
||||
if (getACK(0x06, 0x3e, 800) == GNSS_RESPONSE_NAK) {
|
||||
// It's not critical if the module doesn't acknowledge this configuration.
|
||||
LOG_DEBUG("reconfigure GNSS - defaults maintained. Is this module GPS-only?");
|
||||
} else {
|
||||
if (gnssModel == GNSS_MODEL_UBLOX7) {
|
||||
LOG_INFO("GPS+SBAS configured");
|
||||
} else { // 8,9
|
||||
LOG_INFO("GPS+SBAS+GLONASS+Galileo configured");
|
||||
}
|
||||
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next
|
||||
// commands for the M8 it tends to be more... 1 sec should be enough ;>)
|
||||
delay(1000);
|
||||
}
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// Disable Text Info messages //6,7,8,9
|
||||
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next
|
||||
// commands for the M8 it tends to be more... 1 sec should be enough ;>)
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
// Disable Text Info messages //6,7,8,9
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
||||
|
||||
if (gnssModel == GNSS_MODEL_UBLOX8) { // 8
|
||||
clearBuffer();
|
||||
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
|
||||
clearBuffer();
|
||||
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();
|
||||
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
|
||||
} else { // 7,9
|
||||
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
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 6) {
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 8) {
|
||||
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);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 10) {
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||
currentStep++;
|
||||
return false;
|
||||
} else if (currentStep == 11) {
|
||||
if (ublox_info.protocol_version >= 18) {
|
||||
clearBuffer();
|
||||
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, 0x39, _message_JAM_8, "enable interference resistance", 500);
|
||||
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
|
||||
} 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);
|
||||
}
|
||||
// Turn off unwanted NMEA messages, set update rate
|
||||
SEND_UBX_PACKET(0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
||||
SEND_UBX_PACKET(0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||
|
||||
if (ublox_info.protocol_version >= 18) {
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x86, _message_PMS, "enable powersave 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.
|
||||
if (ublox_info.protocol_version >= 18 && gnssModel == GNSS_MODEL_UBLOX8) {
|
||||
if (gnssModel == GNSS_MODEL_UBLOX8) {
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
|
||||
currentStep++;
|
||||
return false;
|
||||
}
|
||||
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 (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 {
|
||||
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);
|
||||
_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) {
|
||||
if (currentStep == 0) {
|
||||
LOG_INFO("Configuring M10 GPS step 0");
|
||||
delay(1000);
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
LOG_INFO("Configuring M10 GPS step 1");
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 2) {
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 3) {
|
||||
// Next disable Info txt messages in BBR layer
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 4) {
|
||||
// Do M10 configuration for Power Management.
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
|
||||
currentStep++;
|
||||
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);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 6) {
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 7) {
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 8) {
|
||||
// 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);
|
||||
currentStep++;
|
||||
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);
|
||||
currentStep++;
|
||||
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
|
||||
// sleep.
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
|
||||
currentStep++;
|
||||
currentDelay = 750;
|
||||
return false;
|
||||
} else if (currentStep == 11) {
|
||||
// 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);
|
||||
currentStep++;
|
||||
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.
|
||||
// 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.
|
||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10);
|
||||
_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!");
|
||||
}
|
||||
didSerialInit = true;
|
||||
delay(1000);
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
|
||||
delay(750);
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
|
||||
delay(750);
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
|
||||
delay(750);
|
||||
// Next disable Info txt messages in BBR layer
|
||||
clearBuffer();
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
|
||||
delay(750);
|
||||
// Do M10 configuration for Power Management.
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
|
||||
delay(750);
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
|
||||
delay(750);
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
|
||||
delay(750);
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
|
||||
delay(750);
|
||||
// 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);
|
||||
delay(750); // 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);
|
||||
delay(750); // will cause a receiver restart so wait a bit
|
||||
|
||||
// Done with initialization, Now enable wanted NMEA messages in BBR layer so they will survive a periodic
|
||||
// sleep.
|
||||
SEND_UBX_PACKET(0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
|
||||
delay(750);
|
||||
// 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);
|
||||
delay(750);
|
||||
|
||||
// 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
|
||||
// batteries or super caps will not retain the config for a long power off time.
|
||||
msglen = makeUBXPacket(0x06, 0x09, sizeof(_message_SAVE_10), _message_SAVE_10);
|
||||
_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_CM121) {
|
||||
// only ask for RMC and GGA
|
||||
if (currentStep == 0) {
|
||||
// enable GGA
|
||||
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
|
||||
currentStep++;
|
||||
currentDelay = 250;
|
||||
return false;
|
||||
} else if (currentStep == 1) {
|
||||
// enable RMC
|
||||
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
|
||||
currentDelay = 250;
|
||||
}
|
||||
// enable GGA
|
||||
_serial_gps->write("$CFGMSG,0,0,1,1*1B\r\n");
|
||||
delay(250);
|
||||
// enable RMC
|
||||
_serial_gps->write("$CFGMSG,0,4,1,1*1F\r\n");
|
||||
delay(250);
|
||||
}
|
||||
didSerialInit = true;
|
||||
}
|
||||
|
||||
@@ -128,8 +128,6 @@ class GPS : private concurrency::OSThread
|
||||
// Let the GPS hardware save power between updates
|
||||
void down();
|
||||
|
||||
bool initFinished() const { return GPSInitFinished; };
|
||||
|
||||
private:
|
||||
GPS() : concurrency::OSThread("GPS") {}
|
||||
|
||||
|
||||
+18
-18
@@ -524,6 +524,11 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
delay(100);
|
||||
#endif
|
||||
#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();
|
||||
#endif
|
||||
|
||||
@@ -545,10 +550,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
ui->init();
|
||||
#endif
|
||||
#if defined(USE_ST7789) && defined(VTFT_LEDA)
|
||||
#ifdef VTFT_CTRL
|
||||
pinMode(VTFT_CTRL, OUTPUT);
|
||||
digitalWrite(VTFT_CTRL, LOW);
|
||||
#endif
|
||||
ui->init();
|
||||
#ifdef ESP_PLATFORM
|
||||
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
||||
@@ -589,23 +590,22 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
#endif
|
||||
#ifdef USE_ST7789
|
||||
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
|
||||
pinMode(VTFT_LEDA, ANALOG);
|
||||
pinMode(VTFT_LEDA, OUTPUT);
|
||||
digitalWrite(VTFT_LEDA, !TFT_BACKLIGHT_ON);
|
||||
#endif
|
||||
#ifdef VTFT_CTRL
|
||||
pinMode(VTFT_CTRL, ANALOG);
|
||||
#endif
|
||||
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);
|
||||
pinMode(VTFT_CTRL, OUTPUT);
|
||||
digitalWrite(VTFT_CTRL, HIGH);
|
||||
#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
|
||||
#ifdef USE_ST7796
|
||||
SPI1.end();
|
||||
@@ -886,7 +886,7 @@ int32_t Screen::runOnce()
|
||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
||||
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");
|
||||
stopBootScreen();
|
||||
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->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
|
||||
if (upperMsg) {
|
||||
display->drawString(x + 5, y + 5, upperMsg);
|
||||
|
||||
@@ -83,8 +83,6 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr;
|
||||
#include <string>
|
||||
#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 DEBUG_PARTITION_TABLE
|
||||
#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
|
||||
extern SPISettings spiSettings;
|
||||
|
||||
extern const char *bootString;
|
||||
|
||||
+18
-8
@@ -2,18 +2,21 @@
|
||||
|
||||
#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)
|
||||
{
|
||||
if (configuredInterval > 0)
|
||||
return configuredInterval * 1000;
|
||||
return defaultInterval * 1000;
|
||||
return secondsToMsClamped(configuredInterval > 0 ? configuredInterval : defaultInterval);
|
||||
}
|
||||
|
||||
uint32_t Default::getConfiguredOrDefaultMs(uint32_t configuredInterval)
|
||||
{
|
||||
if (configuredInterval > 0)
|
||||
return configuredInterval * 1000;
|
||||
return default_broadcast_interval_secs * 1000;
|
||||
return secondsToMsClamped(configuredInterval > 0 ? configuredInterval : default_broadcast_interval_secs);
|
||||
}
|
||||
|
||||
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))
|
||||
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)
|
||||
@@ -66,4 +76,4 @@ uint8_t Default::getConfiguredOrDefaultHopLimit(uint8_t configured)
|
||||
#else
|
||||
return (configured >= HOP_MAX) ? HOP_MAX : config.lora.hop_limit;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1205,11 +1205,11 @@ void NodeDB::loadFromDisk()
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
#ifdef FSCom
|
||||
#ifdef FACTORY_INSTALL
|
||||
#if defined(FACTORY_INSTALL) && !defined(ARCH_PORTDUINO)
|
||||
spiLock->lock();
|
||||
if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) {
|
||||
LOG_WARN("Factory Install Reset!");
|
||||
FSCom.format();
|
||||
rmDir("/prefs");
|
||||
FSCom.mkdir("/prefs");
|
||||
File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE);
|
||||
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. */
|
||||
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.
|
||||
* @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) */
|
||||
#define MESHTASTIC_MESHTASTIC_APPONLY_PB_H_MAX_SIZE meshtastic_ChannelSet_size
|
||||
#define meshtastic_ChannelSet_size 682
|
||||
#define meshtastic_ChannelSet_size 685
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
|
||||
@@ -618,6 +618,8 @@ typedef struct _meshtastic_Config_LoRaConfig {
|
||||
bool config_ok_to_mqtt;
|
||||
/* Set where LORA FEM is enabled, disabled, or not present */
|
||||
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;
|
||||
|
||||
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_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_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_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}
|
||||
@@ -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_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_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_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}
|
||||
@@ -877,6 +879,7 @@ extern "C" {
|
||||
#define meshtastic_Config_LoRaConfig_ignore_mqtt_tag 104
|
||||
#define meshtastic_Config_LoRaConfig_config_ok_to_mqtt_tag 105
|
||||
#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_mode_tag 2
|
||||
#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, SINGULAR, BOOL, ignore_mqtt, 104) \
|
||||
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_DEFAULT NULL
|
||||
|
||||
@@ -1086,7 +1090,7 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
|
||||
#define meshtastic_Config_BluetoothConfig_size 10
|
||||
#define meshtastic_Config_DeviceConfig_size 100
|
||||
#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_size 204
|
||||
#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) */
|
||||
/* meshtastic_NodeDatabase_size depends on runtime parameters */
|
||||
#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_DeviceState_size 1737
|
||||
#define meshtastic_NodeInfoLite_size 196
|
||||
|
||||
@@ -205,7 +205,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
|
||||
|
||||
/* Maximum encoded size of messages (where known) */
|
||||
#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
|
||||
|
||||
#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"
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
#include "modules/DMShell.h"
|
||||
#include "modules/Telemetry/HostMetrics.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STOREFORWARD
|
||||
#include "modules/StoreForwardModule.h"
|
||||
@@ -195,6 +196,7 @@ void setupModules()
|
||||
#endif
|
||||
#if ARCH_PORTDUINO
|
||||
new HostMetricsModule();
|
||||
dmShellModule = new DMShellModule();
|
||||
#endif
|
||||
#if HAS_TELEMETRY
|
||||
new DeviceTelemetryModule();
|
||||
|
||||
@@ -350,6 +350,8 @@ inline bool isConnectedToNetwork()
|
||||
return WiFi.isConnected();
|
||||
#elif HAS_ETHERNET
|
||||
return Ethernet.linkStatus() == LinkON;
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
|
||||
@@ -127,6 +127,60 @@ void test_client_uses_public_channel_minimums()
|
||||
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()
|
||||
{
|
||||
// Small delay to match other test mains
|
||||
@@ -140,6 +194,13 @@ void setup()
|
||||
RUN_TEST(test_router_uses_router_minimums);
|
||||
RUN_TEST(test_router_late_uses_router_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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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_encrypted_packet_serialization();
|
||||
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()
|
||||
{
|
||||
@@ -52,6 +56,12 @@ void setup()
|
||||
RUN_TEST(test_encrypted_packet_serialization);
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user