Compare commits

...
Author SHA1 Message Date
Ben MeadorsandClaude Opus 4.7 68e92c4e2c Remove stray submodule gitlink for .claude worktree
A 160000 (gitlink) entry for .claude/worktrees/naughty-payne-60fdb7
pointing at f2923590bc was accidentally committed in 9db15780f. The
path isn't a real submodule — it's a Claude Code agent worktree that
shouldn't be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:12:46 -05:00
Ben MeadorsandCopilot Autofix powered by AI bed55c55ff Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-06 18:58:51 -05:00
Ben Meadors b864ea0cdf Merge branch 'master' into gps-backoff 2026-05-06 15:35:25 -05:00
Ben Meadors 220bb4d186 Smart pointers and memory management cleanup (#10400)
* Refactor memory management in Syslog and StoreForwardModule

* Implement destructor for Lock

* Refactor RotaryEncoder and PacketHistory to use smart pointers for better memory management

* CH341 should use unique_ptr for improved memory management

* Fix checks in PH

* Improve Syslog::vlogf to handle variable argument lists more safely

* Fix initOk method to use nullptr for null pointer check
2026-05-06 15:33:59 -05:00
Ben Meadors 9db15780f9 Enhance GPS search failure handling backoff logic 2026-05-06 13:44:09 -05:00
jessm33andjessm33 6e810741f3 Fix GPS initialization logic for Portduino configuration (#10395)
Co-authored-by: jessm33 <root@example.com>
2026-05-05 17:28:22 -05:00
Ben Meadors 603cce2988 Add informSearchFailed method to update GPS power state handling (#10394) 2026-05-05 10:12:50 -05:00
renovate[bot] d559af8477 Update LovyanGFX to v1.2.21 (#10373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 12:29:04 +02:00
Ben Meadors 41f53177a1 Use OBS instead of launchpad (#10375) 2026-05-02 09:25:24 -04:00
github-actions[bot]andvidplace7 2d761f6453 Upgrade trunk (#10364)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-05-02 07:23:49 -05:00
renovate[bot] 7cb071c780 Update platform-native digest to cab4b21 (#10372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-01 18:25:08 -04:00
Austin Lane 0240a00d09 MacOS: Re-Add Orcania/Yder 2026-05-01 10:55:32 -04:00
Austin c0fcf807c0 MacOS: Correct pkg-config name openssl for ulfius. (#10369) 2026-05-01 10:42:17 -04:00
55f40ecdfd Add ulfius webserver support to macos native target (#10366)
* Add ulfius webserver support to macos native target

* fix: update PiWebServer docs for macOS and add explicit cstring include

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/3ce82582-23e0-4afe-b22f-b24f81721488

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: add --cflags to openssl@3 pkg-config and fix apt package name

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/1a6c59aa-4393-4134-8cee-61eeee0e9127

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-01 08:56:49 -05:00
Jason P 90744ee0b7 Update PhoneAPI.cpp to reduce chattiness (#10367) 2026-05-01 08:46:53 -05:00
AustinandCopilot 4ee9598107 Docker: Install grpcio-tools from distro (#10358)
Use distro provided Python at build time (instead of the `python` images from dockerhub) and install `grpcio-tools` using the distro provided packages.

This should speed up build times, ESPECIALLY on riscv64 (where prebuilt `grpcio-tools` wheels are not provided on pip).

Co-authored-by: Copilot <copilot@github.com>
2026-04-30 15:22:11 -05:00
Ben Meadors 7066abbb86 Fix MAC_from_string to use input parameter instead of global config for MAC address parsing (#10356)
* Fix MAC_from_string to use input parameter instead of global config for MAC address parsing

* Enhance MAC_from_string validation and error handling

* Add missing include for <cctype> in PortduinoGlue.cpp
2026-04-30 13:52:42 -05:00
Ben Meadors 21cef8c2e5 Add TCP support for Meshtastic MCP interface / tests and update docs (#10355)
* Add TCP support for Meshtastic MCP interface / tests and update docs

* Address TCP endpoint validation and error handling in connection

* TCP connection handling and device listing logic

* Fix docstring formatting in normalize_tcp_endpoint function
2026-04-30 13:51:29 -05:00
github-actions[bot]andthebentern 173ac58ed7 Update protobufs (#10357)
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
2026-04-30 10:45:20 -05:00
github-actions[bot]andvidplace7 83adfd417a Upgrade trunk (#10354)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-04-30 06:39:52 -05:00
AustinandCopilot 24d64a0013 Docker: Build for riscv64 (#10345)
Upstream support has been added in Debian and Alpine.
Only build as part of `docker_manifest` (Beta/Alpha/Daily) releases, because these will take a **while** thanks to qemu.

Co-authored-by: Copilot <copilot@github.com>
2026-04-29 21:04:49 -05:00
Ben Meadors 3a87fc82c0 Add documentation for macOS support in Copilot and Agent instructions 2026-04-29 19:54:05 -05:00
Austin 478444eb02 Docker-Alpine: Align version between build/main stages (#10347)
FROM python:3.14-alpine3.23 AS builder
FROM alpine:3.23

the alpine version needs to match in both stages 😅
2026-04-29 20:31:59 -04:00
renovate[bot] ad23c42fcc Update meshtastic/device-ui digest to 4bf593a (#10346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 19:09:21 -05:00
Ben Meadors 195f42af82 Doesn't FSCom 2026-04-29 16:57:21 -05:00
Ben Meadors 089af764ec Replace FSCom.format() with FSCom.rmDir() for directory cleanup in NodeDB::loadFromDisk() 2026-04-29 16:41:21 -05:00
Austin 7be5426f34 Do not FACTORY_INSTALL on ARCH_PORTDUINO (#10343) 2026-04-29 13:00:01 -05:00
github-actions[bot]andvidplace7 9ec63b5eb2 Upgrade trunk (#10336)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-04-29 12:55:48 -05:00
Austin 22a9346fe0 Debian: Correctly fail upon failure (#10341)
Fake success is BS! We should fail when we fail.
Fixes issues with Debian sourcedebs silently failing to build ocassionally (due to github 502s, etc).
2026-04-29 11:16:25 -05:00
renovate[bot] c0e52e6e1c Update meshtastic/device-ui digest to 1ddcc9d (#10328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 19:10:17 -05:00
github-actions[bot]andvidplace7 11df30a85f Upgrade trunk (#10324)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-04-28 19:04:16 -05:00
Ben Meadors 8d8ff21e7c Add clamping logic for milliseconds conversion and unit tests (#10326)
* Add clamping logic for milliseconds conversion and unit tests

* Simplify comments in secondsToMsClamped function

Removed detailed comments about seconds to milliseconds conversion.
2026-04-28 19:03:50 -05:00
9c72767c01 macOS: enable CH341 LoRa-hardware path (fix serial truncation, document setup) (#10320)
* macOS: enable CH341 LoRa-hardware path — fix serial truncation, document setup

Verified on Apple Silicon with a CH341A USB-SPI bridge (VID 0x1A86,
PID 0x5512) wired to an SX1262 (Meshstick variant) that the existing
`pine64/libch341-spi-userspace` lib_dep works on macOS as-is — Apple's
bundled CH34x driver only matches the CH340 *UART* variant
(PID 0x7523), so the CH341A's interface 0 is left unclaimed and
libusb opens / configures / claims it directly via IOUSBHostInterface.
End-to-end test: meshtasticd boots, libusb claim succeeds, SX1262 init
returns 0, TCP API serves the meshtastic CLI's --info / --sendtext flow.

Two changes:

1. **`PortduinoGlue.cpp:497`**: pass `sizeof(serial)` (= 9) instead of
   the literal `8` to `Ch341Hal::getSerialString()`. The function in
   `USBHal.h:61-68` treats `len` as buffer size and reserves one slot
   for the null terminator (`bytesCopied = (len - 1) < 8 ? (len - 1) : 8`),
   so passing 8 produced a 7-char serial — which then broke the
   `strlen(serial) == 8` check at line 502, skipping the auto-MAC
   derivation from serial + product string. On Linux this was masked
   by the BlueZ HCI MAC fallback in `getMacAddr()` at lines 139-157,
   but on macOS that fallback is `__linux__`-guarded so the serial path
   is mandatory and the truncation left `mac_address` empty, causing
   the daemon to exit with `*** Blank MAC Address not allowed!`.

2. **`variants/native/portduino/platformio.ini`**: expand the
   `[env:native-macos]` comment block with a "Real LoRa hardware on
   macOS" section. Documents:
   - Why no upstream library change is needed (Apple kext targets
     CH340/UART, not CH341A/SPI; libusb's `#ifdef __linux__` skip is
     correct for macOS in this case).
   - How to point `meshtasticd` at an existing platform-agnostic
     `bin/config.d/lora-*.yaml` for CH341 hardware.
   - The auto-MAC-derivation contract (now working with this fix).
   - `ioreg` and `LIBUSB_DEBUG=4` diagnostic recipes for the failure
     mode where a third-party WCH `CH34xVCPDriver` *would* claim
     interface 0 (`kmutil unload -b <bundleID>` workaround).

No upstream library forks, no PR chain, no additional lib_deps —
the existing `pine64/libch341-spi-userspace` + libusb-1.0 stack does
the right thing on macOS already.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-28 10:54:01 -05:00
AustinandCopilot c0425d7444 Actions: Build MacOS binary (#10319)
Preliminary CI for the MacOS builds

Co-authored-by: Copilot <copilot@github.com>
2026-04-27 13:33:19 -05:00
d7db0f5829 add heltec-v4-r8 board (#10268)
* add heltec-v4-r8 board

* Fixed default SPI pin and macro definition errors.

* update platformio.ini according device-ui LGFX display definitions

Co-authored-by: Copilot <copilot@github.com>

* fix commit reference

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: mverch67 <manuel.verch@gmx.de>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
2026-04-27 09:47:41 -05:00
renovate[bot] 048e5187ba Update platform-native digest to 4ea5e09 (#10314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 07:50:13 -05:00
renovate[bot]andBen Meadors 4234fe6f86 Update meshtastic/device-ui digest to 7289329 (#10313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-04-27 07:50:01 -05:00
126861fd16 Native MacOS hello world (#10309)
* Native MacOS hello world

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update variants/native/portduino/platformio.ini

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: ensure null-termination in getSerialString() and handle len==0

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/e5647919-2255-48ad-bcaa-7a2c2fdbf212

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-04-27 07:07:49 -05:00
github-actions[bot]andvidplace7 47a6c4c6a0 Upgrade trunk (#10317)
Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com>
2026-04-27 06:22:50 -05:00
87f1f9d349 fix(Router): localize p_encrypted to prevent recursive-overwrite leak (#10311)
Router::handleReceived stores its allocCopy of the encrypted packet in the
class member p_encrypted. callModules() invokes module replies that re-enter
the router via MeshService::sendToMesh -> Router::sendLocal, which on a
broadcast reply recursively calls handleReceived. The inner call overwrites
the outer's p_encrypted without releasing it; on the way out it nulls the
member, the outer release(p_encrypted) now releases nullptr, and the original
allocation is permanently leaked. ~381 B per recursion.

Promote p_encrypted to a function-local so each invocation owns its own copy
for its full lifetime. The MQTT-publish null check at the call site (added by
PR #9136 as a workaround for this bug) stays in place because allocCopy can
still legitimately return nullptr on packetPool exhaustion.

Copilot's review of PR #8999 (the original introduction) flagged this exact
pattern at merge time:
  "Storing p_encrypted as a class member can cause issues with recursive or
  concurrent calls to handleReceived() since each call would overwrite the
  previous packet pointer."

The historical reason for the member (S&F needing to retain the encrypted
copy across calls) was satisfied differently by PR #9799 (ServerAPI converted
to std::unique_ptr + cleanup on connection close), so the member is no longer
load-bearing.

Reproduces issues #9632 / #10101 / #8729 (heap leak when MeshMonitor
connected; TCP drops on Station G2 / LILYGO ServerAPI dump abort).

Hardware A/B on Station G2 under sustained TCP-API retry storm (open :4403,
request config, disconnect mid-stream, repeat at ~0.6/s) - 9 min run:

  | Build         | heapFree drift | rebootCount delta |
  | this patch    | -1.5 KB (noise)| 0                 |
  | stock 2.7.13  | -73 KB (8.1KB/min) | +1 (OOM crash) |

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-04-26 21:03:34 -05:00
4ccdd80090 Add search duration check for exceeding 15 minutes (#10293)
* Add search duration check for exceeding 15 minutes

Added a condition to check if the search duration exceeds 15 minutes, indicating too long of a search.

* trunk

* Fix searchedTooLong: move 15-min cap before UINT32_MAX check, cache elapsed, add constexpr

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/b7f74430-9e7e-4a6f-8095-6176c1eee972

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Update src/gps/GPSUpdateScheduling.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove dead UINT32_MAX branch from searchedTooLong

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/6dad5b56-902e-4d0e-90c1-038a9c2df364

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-25 20:42:14 -05:00
83 changed files with 2104 additions and 248 deletions
+5 -1
View File
@@ -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
@@ -573,6 +575,8 @@ Grouped by purpose. Full argument shapes in `mcp-server/README.md`; a few high-v
`confirm=True` is a tool-level gate on top of whatever permission prompt your MCP host shows. **Don't bypass it** by asking the host to auto-approve — it exists specifically because MCP hosts sometimes remember "always allow this tool" and that's dangerous for `factory_reset`, `erase_and_flash`, `uhubctl_power(action='off')`, and `uhubctl_cycle`.
**TCP / native-host nodes.** Setting `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` makes `list_devices` surface a `meshtasticd` daemon (e.g. the `native-macos` build) as a synthetic `tcp://host:port` entry, and `connect()` routes through `meshtastic.tcp_interface.TCPInterface` instead of `SerialInterface`. Every read/write/admin tool that flows through `connect()` works against the daemon transparently. USB-only tools (`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`, `serial_open`, `esptool_*`, `nrfutil_*`, `picotool_*`) raise a clear `ConnectionError` when handed a `tcp://` port; `pio_flash` against a `native*` env raises a `FlashError` (no upload step — use `build` and run the binary directly). The pytest harness still assumes USB-attached devices per role; TCP-aware fixtures are deferred. See `mcp-server/README.md` § "TCP / native-host nodes".
### Hardware test suite (`mcp-server/run-tests.sh`)
The wrapper auto-detects connected devices (VID → role map: `0x239A` → `nrf52`, `0x303A`/`0x10C4` → `esp32s3`), maps each role to a PlatformIO env (`nrf52` → `rak4631`, `esp32s3` → `heltec-v3`, overridable via `MESHTASTIC_MCP_ENV_<ROLE>`), then invokes pytest. Zero pre-flight config needed from the operator.
+8 -3
View File
@@ -32,10 +32,15 @@ jobs:
shell: bash
working-directory: meshtasticd
run: |
# Build-tools (notably platformio) come from the Meshtastic project
# on the OpenSUSE Build Service:
# https://build.opensuse.org/project/show/network:Meshtastic:build-tools
echo 'deb http://download.opensuse.org/repositories/network:/Meshtastic:/build-tools/xUbuntu_24.04/ /' \
| sudo tee /etc/apt/sources.list.d/network:Meshtastic:build-tools.list
curl -fsSL https://download.opensuse.org/repositories/network:Meshtastic:build-tools/xUbuntu_24.04/Release.key \
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/network_Meshtastic_build-tools.gpg >/dev/null
sudo apt-get update -y --fix-missing
sudo apt-get install -y software-properties-common build-essential devscripts equivs
sudo add-apt-repository ppa:meshtastic/build-tools -y
sudo apt-get update -y --fix-missing
sudo apt-get install -y build-essential devscripts equivs
sudo mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
- name: Import GPG key
+51
View File
@@ -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 ulfius
- 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
+3 -1
View File
@@ -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 }}
+22
View File
@@ -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 }}
+15
View File
@@ -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
+5 -5
View File
@@ -4,19 +4,19 @@ 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.524
- renovate@43.141.0
- checkov@3.2.526
- renovate@43.150.0
- prettier@3.8.3
- trufflehog@3.95.2
- yamllint@1.38.0
- bandit@1.9.4
- trivy@0.70.0
- taplo@0.10.0
- ruff@0.15.11
- ruff@0.15.12
- isort@8.0.1
- markdownlint@0.48.0
- oxipng@10.1.1
@@ -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:
+27 -24
View File
@@ -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,19 +122,21 @@ 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)
| Var | Purpose |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
| Var | Purpose |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MESHTASTIC_MCP_ENV_<ROLE>` | Override PlatformIO env for a role (e.g. `MESHTASTIC_MCP_ENV_NRF52=rak4631-dap`). Default map: `nrf52→rak4631`, `esp32s3→heltec-v3`. |
| `MESHTASTIC_MCP_SEED` | PSK seed for the session test profile. Defaults to `mcp-<user>-<host>`. |
| `MESHTASTIC_MCP_FLASH_LOG` | File path to tee pio/esptool/nrfutil/picotool output. `run-tests.sh` sets this to `tests/flash.log` so the TUI can stream live flash progress. |
| `MESHTASTIC_MCP_TCP_HOST` | `host` or `host:port` of a `meshtasticd` daemon (e.g. the `native-macos` build). Surfaces it in `list_devices` as `tcp://host:port` so `connect()`-based tools target it transparently. Default port 4403. |
| `MESHTASTIC_UHUBCTL_BIN` | Absolute path to `uhubctl` binary. Default: PATH lookup. |
| `MESHTASTIC_UHUBCTL_LOCATION_<ROLE>` | Pin a role to a specific uhubctl hub location (e.g. `1-1.3`). Wins over VID auto-detection — use when multiple devices share a VID. |
| `MESHTASTIC_UHUBCTL_PORT_<ROLE>` | Pin a role to a specific hub port number. Required alongside `LOCATION_<ROLE>`. |
| `MESHTASTIC_UI_CAMERA_BACKEND` | Camera backend for UI tier + `capture_screen` tool: `opencv` / `ffmpeg` / `null` / `auto` (default). |
| `MESHTASTIC_UI_CAMERA_DEVICE` | Generic camera device (index or path). Used by the UI tier when no per-role var is set. |
| `MESHTASTIC_UI_CAMERA_DEVICE_<ROLE>` | Per-role camera pinning (e.g. `MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0` for the OLED-bearing heltec-v3). |
| `MESHTASTIC_UI_OCR_BACKEND` | OCR engine selection: `easyocr` / `pytesseract` / `null` / `auto` (default). |
| `MESHTASTIC_UI_TUI_CAMERA` | Set to `1` to mount the live camera-feed panel in `meshtastic-mcp-test-tui`. |
+3 -1
View File
@@ -3,15 +3,17 @@
# trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
FROM python:3.14-slim-trixie AS builder
FROM debian:trixie AS builder
ARG PIO_ENV=native
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Etc/UTC
# Install Dependencies
ENV PIP_ROOT_USER_ACTION=ignore
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN apt-get update && apt-get install --no-install-recommends -y \
curl wget g++ zip git ca-certificates pkg-config \
python3-pip python3-grpc-tools \
libgpiod-dev libyaml-cpp-dev libbluetooth-dev libi2c-dev libuv1-dev \
libusb-1.0-0-dev libulfius-dev liborcania-dev libssl-dev \
libx11-dev libinput-dev libxkbcommon-x11-dev libsqlite3-dev libsdl2-dev \
+10 -3
View File
@@ -3,12 +3,19 @@
# 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 alpine:3.23 AS builder
ARG PIO_ENV=native
ENV PIP_ROOT_USER_ACTION=ignore
# Enable Alpine community repository (for 'py3-grpcio-tools')
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v$(cut -d. -f1,2 /etc/alpine-release)/community" >> /etc/apk/repositories
# Install Dependencies
ENV PIP_ROOT_USER_ACTION=ignore
ENV PIP_BREAK_SYSTEM_PACKAGES=1
RUN apk --no-cache add \
bash g++ libstdc++-dev linux-headers zip git ca-certificates libbsd-dev \
py3-pip py3-grpcio-tools \
libgpiod-dev yaml-cpp-dev bluez-dev \
libusb-dev i2c-tools-dev libuv-dev openssl-dev pkgconf argp-standalone \
libx11-dev libinput-dev libxkbcommon-dev sqlite-dev sdl2-dev \
@@ -60,4 +67,4 @@ EXPOSE 4403
CMD [ "sh", "-cx", "meshtasticd --fsdir=/var/lib/meshtasticd" ]
HEALTHCHECK NONE
HEALTHCHECK NONE
+2 -1
View File
@@ -31,5 +31,6 @@ basename=meshtasticd-$1-$VERSION
pio pkg install --environment "$PIO_ENV" || platformioFailed
pio run --environment "$PIO_ENV" || platformioFailed
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_${os_name}_$(uname -m)"
cp bin/native-install.* $OUTDIR/
+43
View File
@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"partitions": "default_16MB.csv",
"memory_type": "qio_opi"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "heltec_v4_r8"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "heltec_wifi_lora_32 v4 r8 (16 MB FLASH, 8 MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 327680,
"maximum_size": 16777216,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 921600
},
"url": "https://heltec.org/",
"vendor": "heltec"
}
+1
View File
@@ -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
+67 -9
View File
@@ -166,15 +166,73 @@ rather than auto-`sudo`'ing mid-run.
## Environment variables
| Var | Default | Purpose |
| -------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- |
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
| Var | Default | Purpose |
| -------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `MESHTASTIC_FIRMWARE_ROOT` | walks up from cwd for `platformio.ini` | Pin the firmware repo |
| `MESHTASTIC_PIO_BIN` | `~/.platformio/penv/bin/pio` → `$PATH` `pio` → `platformio` | Override `pio` location |
| `MESHTASTIC_ESPTOOL_BIN` | `<firmware>/.venv/bin/esptool` → `$PATH` | Override esptool |
| `MESHTASTIC_NRFUTIL_BIN` | `$PATH` | Override nrfutil |
| `MESHTASTIC_PICOTOOL_BIN` | `$PATH` | Override picotool |
| `MESHTASTIC_MCP_SEED` | `mcp-<user>-<host>` | PSK seed for test-harness session (CI override) |
| `MESHTASTIC_MCP_FLASH_LOG` | `<mcp-server>/tests/flash.log` | Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
| `MESHTASTIC_MCP_TCP_HOST` | unset | `host` or `host:port` of a `meshtasticd` daemon to surface as a TCP device (see "TCP / native-host nodes" below) |
## TCP / native-host nodes
The `native-macos` and `native` PlatformIO envs build a headless `meshtasticd`
binary that runs on the host (Apple Silicon / Intel macOS, or Linux Portduino).
The daemon exposes the meshtastic TCP API on port `4403` rather than a USB
serial endpoint — point the MCP server at it via `MESHTASTIC_MCP_TCP_HOST`:
```bash
# 1. Build + run a daemon on this host (see variants/native/portduino/platformio.ini
# for full Homebrew prereqs and CH341 LoRa-adapter setup).
pio run -e native-macos
~/.meshtasticd/meshtasticd
# 2. Point the MCP server at it.
export MESHTASTIC_MCP_TCP_HOST=localhost # or host:port, default port 4403
```
**First-run gotcha — MAC address.** `meshtasticd` derives its MAC from the
USB adapter's serial-number / product strings. Many cheap CH341 dongles
(MeshStick included — VID 0x1A86 / PID 0x5512) ship with `iSerialNumber=0`
and `iProduct=0`, so the daemon aborts on boot with `*** Blank MAC Address
not allowed!`. Set the MAC explicitly in `config.yaml`:
```yaml
# Under General:
MACAddress: 02:CA:FE:BA:BE:01
```
Use a locally-administered address (first byte's second-LSB set, e.g.
`02:*` / `06:*` / `0A:*` / `0E:*`) to avoid colliding with a real OUI.
There is also a `--hwid AA:BB:CC:DD:EE:FF` CLI flag visible in
`meshtasticd --help`, but it is **currently broken** in
`MAC_from_string()` (`src/platform/portduino/PortduinoGlue.cpp`): the
function strips colons from its parameter but then reads bytes from the
global `portduino_config.mac_address`, so `--hwid` is silently overridden
when `MACAddress:` is also set, and crashes the daemon (uncaught
`std::invalid_argument: stoi: no conversion`) when it isn't. Use the YAML
form until that's fixed upstream.
`list_devices` will surface the daemon as `tcp://localhost:4403` with
`likely_meshtastic=True`, so `device_info`, `list_nodes`, `get_config`,
`set_config`, `set_owner`, `send_text`, `userprefs_*`, and the admin RPCs
auto-select it when no `port` is passed. Pass `port="tcp://other-host:9999"`
explicitly to target a different daemon.
**Tools that don't apply to a TCP/native node** (no USB hardware to operate
on) raise a clear `ConnectionError` rather than failing mysteriously:
`pio_flash`, `erase_and_flash`, `update_flash`, `touch_1200bps`,
`serial_open` (use info/admin tools directly), and the vendor escape hatches
`esptool_*`, `nrfutil_*`, `picotool_*`. `pio_flash` against a `native*` env
similarly raises — there's no upload step; use `build` and run the binary
directly.
The pytest harness in `tests/` still assumes USB-attached devices per role —
TCP-aware fixtures are not part of this surface yet.
## Hardware Test Suite
+157 -12
View File
@@ -1,4 +1,4 @@
"""Context manager for meshtastic.SerialInterface connections.
"""Context manager for meshtastic interface connections (serial + TCP).
Every info/admin tool goes through `connect(port)` so we have a single place
that:
@@ -6,8 +6,16 @@ that:
- fails fast if a serial_session is already holding the port,
- guarantees `.close()` is called, even on exception.
The `SerialInterface` blocks on construction waiting for the node database;
that's fine for v1 since every tool is a short-lived request.
Two transports:
- Serial: USB-attached firmware on `/dev/cu.*` / `/dev/ttyUSB*` / `COM*`.
- TCP: a `meshtasticd` daemon (e.g. the native macOS / Linux Portduino
headless build) addressed as `tcp://host[:port]` (default port 4403).
Surfaced by `devices.list_devices()` when `MESHTASTIC_MCP_TCP_HOST` is
set, so `resolve_port(None)` auto-selects it like a USB candidate.
Both `SerialInterface` and `TCPInterface` block on construction waiting for
the node database; that's fine for v1 since every tool is a short-lived
request.
"""
from __future__ import annotations
@@ -17,20 +25,107 @@ from typing import Iterator
from . import devices, registry
DEFAULT_TCP_PORT = 4403
TCP_SCHEME = "tcp://"
TCP_HOST_ENV = "MESHTASTIC_MCP_TCP_HOST"
class ConnectionError(RuntimeError):
pass
def is_tcp_port(port: str | None) -> bool:
return bool(port) and port.startswith(TCP_SCHEME)
def parse_tcp_port(port: str) -> tuple[str, int]:
"""Parse `tcp://host[:port]` → (host, port). Defaults to 4403.
Validates host shape (non-empty, no path separators) and port range
(1..65535). Raises `ConnectionError` on malformed input — never lets
a raw `ValueError` bubble up to a tool surface.
"""
if not port.startswith(TCP_SCHEME):
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: expected '{TCP_SCHEME}host[:port]'."
)
rest = port[len(TCP_SCHEME) :]
if ":" in rest:
host, port_str = rest.rsplit(":", 1)
try:
tcp_port = int(port_str)
except ValueError as e:
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: port {port_str!r} is not an integer."
) from e
else:
host, tcp_port = rest, DEFAULT_TCP_PORT
if not host:
raise ConnectionError(f"Invalid TCP endpoint {port!r}: empty host.")
if any(c in host for c in ("/", "\\")):
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: host {host!r} contains a path "
"separator. TCP hostnames cannot contain '/' or '\\' — did you "
"pass a serial port path or a Windows drive path by mistake?"
)
if not (1 <= tcp_port <= 65535):
raise ConnectionError(
f"Invalid TCP endpoint {port!r}: port {tcp_port} out of range "
"(must be 1..65535)."
)
return host, tcp_port
def normalize_tcp_endpoint(endpoint: str) -> str:
r"""Normalize `host`, `host:port`, or `tcp://host[:port]` → canonical
`tcp://host:port` form. One place that owns the lock-key shape.
Defers all validation to `parse_tcp_port`, so path-like inputs
(`/dev/cu.foo`, `C:\Windows\…`), empty hosts, non-integer ports,
and out-of-range ports raise `ConnectionError` here too.
"""
if endpoint.startswith(TCP_SCHEME):
canonical = endpoint
elif ":" in endpoint:
canonical = f"{TCP_SCHEME}{endpoint}"
else:
canonical = f"{TCP_SCHEME}{endpoint}:{DEFAULT_TCP_PORT}"
host, port = parse_tcp_port(canonical)
return f"{TCP_SCHEME}{host}:{port}"
def reject_if_tcp(port: str | None, tool_name: str) -> None:
"""Raise if `port` is a TCP endpoint — for tools that need real USB
hardware (flash, bootloader, vendor escape hatches, serial monitor).
Only checks the explicit arg; auto-selection via env var is the caller's
responsibility to handle if it matters.
"""
if is_tcp_port(port):
raise ConnectionError(
f"{tool_name} is not applicable to TCP/native nodes ({port}). "
"This tool requires USB-attached hardware."
)
def resolve_port(port: str | None) -> str:
"""Pick a port: explicit > sole likely_meshtastic candidate > error."""
"""Pick a port: explicit > sole likely_meshtastic candidate > error.
A `tcp://` string passes through (after canonicalization). When `port`
is None and no USB candidates are present, `MESHTASTIC_MCP_TCP_HOST`
is consulted via `devices.list_devices()`.
"""
if port:
if is_tcp_port(port):
return normalize_tcp_endpoint(port)
return port
candidates = [d for d in devices.list_devices() if d["likely_meshtastic"]]
if not candidates:
raise ConnectionError(
"No Meshtastic devices detected. Plug one in or pass `port` explicitly. "
"Run `list_devices` with include_unknown=True to see all serial ports."
"No Meshtastic devices detected. Plug one in, set "
f"{TCP_HOST_ENV}=<host[:port]> for a meshtasticd daemon, "
"or pass `port` explicitly. Run `list_devices` with "
"include_unknown=True to see all serial ports."
)
if len(candidates) > 1:
ports = ", ".join(c["port"] for c in candidates)
@@ -43,17 +138,62 @@ def resolve_port(port: str | None) -> str:
@contextmanager
def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
"""Open a `meshtastic.SerialInterface` and always close it.
"""Open a meshtastic interface (serial or TCP) and always close it.
Raises `ConnectionError` immediately if another serial session holds the
port (a `pio device monitor` in `serial_sessions/`, for instance).
For serial: raises `ConnectionError` immediately if another serial
session holds the port (a `pio device monitor` in `serial_sessions/`).
For TCP: no exclusive-access requirement, so the serial-session check
is skipped — but the `port_lock` still serializes parallel `connect()`
calls to the same daemon endpoint.
`timeout_s` is plumbed through to both `SerialInterface(timeout=...)`
and `TCPInterface(timeout=...)`. The meshtastic library uses the value
as the reply-wait deadline for `localNode.waitForConfig()` during
construction and for any subsequent admin RPC. `int()`-converted at
the boundary because the upstream API expects whole seconds.
"""
resolved = resolve_port(port)
timeout = int(timeout_s)
if is_tcp_port(resolved):
from meshtastic.tcp_interface import (
TCPInterface, # type: ignore[import-untyped]
)
host, tcp_port = parse_tcp_port(resolved)
lock = registry.port_lock(resolved)
if not lock.acquire(blocking=False):
raise ConnectionError(
f"TCP endpoint {resolved} is busy — another device operation "
"is in flight. Retry shortly."
)
iface = None
try:
iface = TCPInterface(
hostname=host,
portNumber=tcp_port,
connectNow=True,
noProto=False,
timeout=timeout,
)
yield iface
finally:
if iface is not None:
try:
iface.close()
except Exception:
pass
try:
lock.release()
except RuntimeError:
pass
return
from meshtastic.serial_interface import (
SerialInterface, # type: ignore[import-untyped]
)
resolved = resolve_port(port)
active = registry.active_session_for_port(resolved)
if active is not None:
raise ConnectionError(
@@ -70,7 +210,12 @@ def connect(port: str | None = None, timeout_s: float = 8.0) -> Iterator:
iface = None
try:
iface = SerialInterface(devPath=resolved, connectNow=True, noProto=False)
iface = SerialInterface(
devPath=resolved,
connectNow=True,
noProto=False,
timeout=timeout,
)
yield iface
finally:
if iface is not None:
+63 -3
View File
@@ -1,13 +1,18 @@
"""USB/serial device discovery.
"""USB/serial + TCP device discovery.
Combines the canonical `meshtastic.util.findPorts()` allowlist/blocklist with
the richer metadata (`serial.tools.list_ports.comports()`) so callers see
VID/PID, descriptions, and manufacturer strings alongside the "is this likely
a Meshtastic device" signal.
If `MESHTASTIC_MCP_TCP_HOST=<host[:port]>` is set, a synthetic entry for the
`meshtasticd` daemon at that endpoint is prepended to the result, so
`resolve_port(None)` auto-selects it like a USB candidate.
"""
from __future__ import annotations
import os
from typing import Any
from serial.tools import list_ports
@@ -19,6 +24,45 @@ def _to_hex(value: int | None) -> str | None:
return f"0x{value:04x}"
def _tcp_endpoint_from_env() -> dict[str, Any] | None:
"""Synthesize a TCP device entry from MESHTASTIC_MCP_TCP_HOST, if set.
If the env var is malformed (non-integer port, path-like host, etc.),
return an entry with `likely_meshtastic=False` and the parser error in
the description, rather than raising — `list_devices` is the diagnostic
tool a user reaches for when their env var isn't working, so it must
not crash on misconfiguration.
"""
host = os.environ.get("MESHTASTIC_MCP_TCP_HOST")
if not host:
return None
# Lazy import to avoid a circular dependency (connection imports devices).
from . import connection
try:
port = connection.normalize_tcp_endpoint(host)
description = "meshtasticd (TCP)"
likely = True
except connection.ConnectionError as e:
# Surface the raw env-var value plus the parser's reason so the
# user can see exactly what they set and why it was rejected.
# Don't double the scheme if the user already prefixed `tcp://`.
port = host if host.startswith(connection.TCP_SCHEME) else f"tcp://{host}"
description = f"meshtasticd (TCP) — invalid MESHTASTIC_MCP_TCP_HOST: {e}"
likely = False
return {
"port": port,
"vid": None,
"pid": None,
"description": description,
"manufacturer": None,
"product": None,
"serial_number": None,
"likely_meshtastic": likely,
"blacklisted": False,
}
def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
"""Return enriched info for serial ports, flagging Meshtastic candidates.
@@ -70,6 +114,22 @@ def list_devices(include_unknown: bool = False) -> list[dict[str, Any]]:
}
)
# Stable ordering: likely_meshtastic first, then by port path
results.sort(key=lambda r: (not r["likely_meshtastic"], r["port"]))
# Append the TCP endpoint (if env var set) and sort everything together.
tcp_entry = _tcp_endpoint_from_env()
if tcp_entry is not None:
results.append(tcp_entry)
# Stable ordering: likely_meshtastic first; within rank, TCP wins over
# USB (explicit env-var configuration takes precedence over USB
# enumeration); then by port path. A misconfigured TCP entry has
# likely_meshtastic=False and lands among the other ignored entries —
# it does NOT pre-empt real USB devices at the top of the list.
results.sort(
key=lambda r: (
not r["likely_meshtastic"],
not r["port"].startswith("tcp://"),
r["port"],
)
)
return results
+18 -1
View File
@@ -17,7 +17,7 @@ from typing import Any
import serial
from . import boards, config, devices, pio, userprefs
from . import boards, config, connection, devices, pio, userprefs
# Meshtastic variants use both `esp32s3` and `esp32-s3` style names across
# variants/*/platformio.ini (no consistency enforced). Accept both spellings.
@@ -46,6 +46,18 @@ def _require_confirm(confirm: bool, operation: str) -> None:
)
def _reject_native_env(env: str, operation: str) -> None:
"""`native*` envs build a host executable, not firmware — there's no
upload step. The user wants `build` (or just runs the binary directly).
"""
if env.startswith("native"):
raise FlashError(
f"{operation} is not applicable for env {env!r}: native envs "
"produce a host executable, not flashable firmware. Use `build` "
"instead, then run the resulting binary directly."
)
def _artifacts_for(env: str) -> list[Path]:
build_dir = config.firmware_root() / ".pio" / "build" / env
if not build_dir.is_dir():
@@ -141,6 +153,8 @@ def flash(
that pio performs will pick up the injected values.
"""
_require_confirm(confirm, "flash")
_reject_native_env(env, "flash")
connection.reject_if_tcp(port, "flash")
with userprefs.temporary_overrides(userprefs_overrides) as effective:
result = pio.run(
["run", "-e", env, "-t", "upload", "--upload-port", port],
@@ -200,6 +214,7 @@ def erase_and_flash(
in that case) since a cached factory.bin would not reflect the new prefs.
"""
_require_confirm(confirm, "erase_and_flash")
connection.reject_if_tcp(port, "erase_and_flash")
_check_esp32_env(env)
if userprefs_overrides and skip_build:
@@ -257,6 +272,7 @@ def update_flash(
overrides are provided we always force a rebuild.
"""
_require_confirm(confirm, "update_flash")
connection.reject_if_tcp(port, "update_flash")
_check_esp32_env(env)
if userprefs_overrides and skip_build:
@@ -391,6 +407,7 @@ def touch_1200bps(
Returns `{ok, former_port, new_port, new_port_vid_pid, attempts}`.
"""
connection.reject_if_tcp(port, "touch_1200bps")
before_list = devices.list_devices(include_unknown=True)
before_ports = {d["port"] for d in before_list}
+6 -1
View File
@@ -16,7 +16,7 @@ import subprocess
from pathlib import Path
from typing import Any, Sequence
from . import config, pio
from . import config, connection, pio
_TIMEOUT_SHORT = 30
_TIMEOUT_LONG = 600
@@ -102,6 +102,7 @@ def _parse_esptool_chip_info(stdout: str) -> dict[str, Any]:
def esptool_chip_info(port: str) -> dict[str, Any]:
connection.reject_if_tcp(port, "esptool_chip_info")
binary = config.esptool_bin()
# `chip_id` prints chip + mac + crystal + features. `flash_id` adds flash.
combined = _run(binary, ["--port", port, "flash_id"], timeout=_TIMEOUT_SHORT)
@@ -116,6 +117,7 @@ def esptool_chip_info(port: str) -> dict[str, Any]:
def esptool_erase_flash(port: str, confirm: bool = False) -> dict[str, Any]:
"""Full-chip erase. Leaves the device unbootable until reflashed."""
_require_confirm(confirm, "esptool_erase_flash")
connection.reject_if_tcp(port, "esptool_erase_flash")
binary = config.esptool_bin()
# esptool v5 uses `erase-flash`, older uses `erase_flash`. Try the new name
# first; if it fails with unknown command, retry old.
@@ -134,6 +136,7 @@ def esptool_raw(
"""Raw esptool passthrough. Destructive subcommands require confirm=True."""
if not args:
raise ToolError("args must not be empty")
connection.reject_if_tcp(port, "esptool_raw")
# Find the first non-flag arg (the subcommand).
subcommand = next((a for a in args if not a.startswith("-")), None)
if subcommand and subcommand.replace("-", "_") in {
@@ -156,6 +159,7 @@ NRFUTIL_DESTRUCTIVE = {"dfu", "settings"}
def nrfutil_dfu(port: str, package_path: str, confirm: bool = False) -> dict[str, Any]:
_require_confirm(confirm, "nrfutil_dfu")
connection.reject_if_tcp(port, "nrfutil_dfu")
pkg = Path(package_path).expanduser()
if not pkg.is_file():
raise ToolError(f"Package not found: {pkg}")
@@ -213,6 +217,7 @@ def _parse_picotool_info(stdout: str) -> dict[str, Any]:
def picotool_info(port: str | None = None) -> dict[str, Any]:
"""Read device info from a Pico in BOOTSEL mode. `port` is informational
only — picotool auto-detects."""
connection.reject_if_tcp(port, "picotool_info")
binary = config.picotool_bin()
res = _run(binary, ["info", "-a"], timeout=_TIMEOUT_SHORT)
if res["exit_code"] != 0:
@@ -71,6 +71,10 @@ def open_session(
If `env` is supplied, pio resolves baud and filters from platformio.ini.
Otherwise uses the supplied `baud` and `filters` (default `['direct']`).
"""
# Lazy import to avoid circular: registry imports serial_session.
from . import connection
connection.reject_if_tcp(port, "serial_open")
args = ["device", "monitor", "--port", port, "--no-reconnect"]
effective_filters: list[str]
effective_baud: int = baud
@@ -0,0 +1,383 @@
"""TCP transport plumbing in connection.py + devices.py.
Pure-Python tests — no real device or daemon required. Mocks `TCPInterface`
when exercising `connect()`.
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from meshtastic_mcp import connection, devices
# ---------- helpers --------------------------------------------------------
class TestIsTcpPort:
def test_tcp_scheme(self) -> None:
assert connection.is_tcp_port("tcp://localhost") is True
assert connection.is_tcp_port("tcp://localhost:4403") is True
assert connection.is_tcp_port("tcp://192.168.1.50:9999") is True
def test_serial_paths(self) -> None:
assert connection.is_tcp_port("/dev/cu.usbmodem1234") is False
assert connection.is_tcp_port("/dev/ttyUSB0") is False
assert connection.is_tcp_port("COM3") is False
def test_empty_or_none(self) -> None:
assert connection.is_tcp_port(None) is False
assert connection.is_tcp_port("") is False
class TestParseTcpPort:
def test_default_port(self) -> None:
assert connection.parse_tcp_port("tcp://localhost") == ("localhost", 4403)
def test_explicit_port(self) -> None:
assert connection.parse_tcp_port("tcp://localhost:9999") == (
"localhost",
9999,
)
def test_ip_with_port(self) -> None:
assert connection.parse_tcp_port("tcp://192.168.1.50:4403") == (
"192.168.1.50",
4403,
)
class TestNormalizeTcpEndpoint:
def test_bare_host(self) -> None:
assert connection.normalize_tcp_endpoint("localhost") == "tcp://localhost:4403"
def test_host_port(self) -> None:
assert (
connection.normalize_tcp_endpoint("localhost:5000")
== "tcp://localhost:5000"
)
def test_full_url(self) -> None:
assert (
connection.normalize_tcp_endpoint("tcp://1.2.3.4") == "tcp://1.2.3.4:4403"
)
assert (
connection.normalize_tcp_endpoint("tcp://1.2.3.4:9999")
== "tcp://1.2.3.4:9999"
)
def test_idempotent(self) -> None:
once = connection.normalize_tcp_endpoint("localhost:4403")
twice = connection.normalize_tcp_endpoint(once)
assert once == twice == "tcp://localhost:4403"
def test_path_like_endpoint_rejected(self) -> None:
# Serial port paths and Windows drive paths are common config typos
# (someone passes a serial path to MESHTASTIC_MCP_TCP_HOST). Reject
# rather than producing a nonsense `tcp:///dev/cu.foo:4403` URL.
with pytest.raises(connection.ConnectionError, match="path separator"):
connection.normalize_tcp_endpoint("/dev/cu.foo")
with pytest.raises(connection.ConnectionError):
connection.normalize_tcp_endpoint("tcp:///dev/cu.foo:4403")
with pytest.raises(connection.ConnectionError):
connection.normalize_tcp_endpoint(r"C:\Windows\System32")
def test_non_integer_port_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="not an integer"):
connection.normalize_tcp_endpoint("tcp://host:notaport")
with pytest.raises(connection.ConnectionError, match="not an integer"):
connection.normalize_tcp_endpoint("host:notaport")
def test_empty_host_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="empty host"):
connection.normalize_tcp_endpoint("tcp://:4403")
def test_port_out_of_range_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.normalize_tcp_endpoint("tcp://host:0")
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.normalize_tcp_endpoint("tcp://host:65536")
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.normalize_tcp_endpoint("host:99999")
class TestParseTcpPortValidation:
def test_missing_scheme_rejected(self) -> None:
# parse_tcp_port is a low-level helper that requires the scheme.
# Misuse should fail loudly rather than silently mis-parsing.
with pytest.raises(connection.ConnectionError, match="expected"):
connection.parse_tcp_port("localhost:4403")
def test_negative_port_rejected(self) -> None:
with pytest.raises(connection.ConnectionError, match="out of range"):
connection.parse_tcp_port("tcp://host:-1")
# ---------- reject_if_tcp --------------------------------------------------
class TestRejectIfTcp:
def test_rejects_tcp(self) -> None:
with pytest.raises(connection.ConnectionError, match="not applicable"):
connection.reject_if_tcp("tcp://localhost", "esptool_chip_info")
def test_passes_through_serial(self) -> None:
connection.reject_if_tcp("/dev/cu.usbmodem1", "esptool_chip_info") # no raise
def test_passes_through_none(self) -> None:
# None means "auto-detect"; not the explicit-arg case we guard.
connection.reject_if_tcp(None, "esptool_chip_info") # no raise
# ---------- resolve_port ---------------------------------------------------
class TestResolvePort:
def test_explicit_serial_passthrough(self) -> None:
assert connection.resolve_port("/dev/cu.usbmodem999") == "/dev/cu.usbmodem999"
def test_explicit_tcp_normalized(self) -> None:
assert connection.resolve_port("tcp://localhost") == "tcp://localhost:4403"
def test_no_port_no_devices_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch.object(devices, "list_devices", return_value=[]):
with pytest.raises(
connection.ConnectionError, match="No Meshtastic devices"
):
connection.resolve_port(None)
def test_no_port_one_candidate_selected(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
fake = [{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True}]
with patch.object(devices, "list_devices", return_value=fake):
assert connection.resolve_port(None) == "/dev/cu.usbmodem1"
def test_no_port_multiple_candidates_errors(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
fake = [
{"port": "/dev/cu.usbmodem1", "likely_meshtastic": True},
{"port": "/dev/cu.usbmodem2", "likely_meshtastic": True},
]
with patch.object(devices, "list_devices", return_value=fake):
with pytest.raises(connection.ConnectionError, match="Multiple"):
connection.resolve_port(None)
def test_env_var_surfaces_tcp_via_devices(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
# Don't patch list_devices — let the real env-var path run, but stub
# the USB enumeration to keep the test hermetic.
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
assert connection.resolve_port(None) == "tcp://localhost:4403"
# ---------- devices.list_devices TCP entry --------------------------------
class TestDevicesTcpEntry:
def test_no_env_var_no_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices()
assert all(not d["port"].startswith("tcp://") for d in ds)
def test_env_var_adds_tcp_entry(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "myhost:9999")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices()
tcp = [d for d in ds if d["port"].startswith("tcp://")]
assert len(tcp) == 1
assert tcp[0]["port"] == "tcp://myhost:9999"
assert tcp[0]["likely_meshtastic"] is True
assert tcp[0]["description"] == "meshtasticd (TCP)"
def test_tcp_entry_first_in_results(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices()
assert ds, "expected at least the TCP entry"
assert ds[0]["port"].startswith("tcp://")
def test_invalid_env_var_does_not_break_list_devices(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# `list_devices` is the diagnostic tool reached for when an env var
# isn't working — it must not throw on misconfiguration.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices(include_unknown=True)
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
assert len(tcp) == 1
assert tcp[0]["likely_meshtastic"] is False
assert "invalid MESHTASTIC_MCP_TCP_HOST" in tcp[0]["description"]
assert "not an integer" in tcp[0]["description"]
def test_invalid_env_var_excluded_from_resolve_port_autodetect(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# `likely_meshtastic=False` keeps the bad TCP entry out of the
# auto-select path — `resolve_port(None)` should still report
# "no Meshtastic devices" rather than picking a broken endpoint.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
with pytest.raises(connection.ConnectionError, match="No Meshtastic"):
connection.resolve_port(None)
def test_invalid_env_var_does_not_double_tcp_scheme(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# If a user mistakenly sets `MESHTASTIC_MCP_TCP_HOST=tcp://host:bad`,
# the diagnostic entry must surface the raw value as-is rather than
# producing `tcp://tcp://host:bad`.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "tcp://host:notaport")
with patch("meshtastic_mcp.devices.list_ports.comports", return_value=[]):
ds = devices.list_devices(include_unknown=True)
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
assert len(tcp) == 1
assert tcp[0]["port"] == "tcp://host:notaport"
assert "tcp://tcp://" not in tcp[0]["port"]
def test_invalid_env_var_does_not_pre_empt_real_usb_devices(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# Sort ordering: a misconfigured TCP env var must NOT take position 0
# ahead of real USB candidates. Position 0 is reserved for the highest
# rank (likely_meshtastic=True), with TCP-before-USB as a tiebreaker
# within rank.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "host:notaport")
# Stub a USB Meshtastic candidate (Espressif VID, port present in
# findPorts).
class FakeInfo:
def __init__(self, device: str, vid: int, pid: int) -> None:
self.device = device
self.vid = vid
self.pid = pid
self.description = "Heltec V3"
self.manufacturer = "Espressif"
self.product = "USB JTAG/serial"
self.serial_number = "abc"
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
with patch(
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
), patch(
"meshtastic.util.findPorts",
return_value=["/dev/cu.usbmodem4201"],
):
ds = devices.list_devices(include_unknown=True)
assert ds, "expected at least the USB + TCP entries"
# Real USB candidate must be at position 0 — it's likely_meshtastic.
assert ds[0]["port"] == "/dev/cu.usbmodem4201"
assert ds[0]["likely_meshtastic"] is True
# The malformed TCP entry exists but lands among the unlikely entries.
tcp = [d for d in ds if "TCP" in (d["description"] or "")]
assert len(tcp) == 1
assert tcp[0]["likely_meshtastic"] is False
assert ds.index(tcp[0]) > 0
def test_likely_tcp_entry_wins_tiebreak_over_usb(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# Conversely, a *valid* TCP env var should sort ahead of USB
# candidates of equal likely_meshtastic rank — explicit env-var
# configuration is a precedence signal.
monkeypatch.setenv("MESHTASTIC_MCP_TCP_HOST", "localhost:4403")
class FakeInfo:
def __init__(self, device: str, vid: int, pid: int) -> None:
self.device = device
self.vid = vid
self.pid = pid
self.description = "Heltec V3"
self.manufacturer = "Espressif"
self.product = "USB JTAG/serial"
self.serial_number = "abc"
fake_port = FakeInfo("/dev/cu.usbmodem4201", 0x303A, 0x1001)
with patch(
"meshtastic_mcp.devices.list_ports.comports", return_value=[fake_port]
), patch(
"meshtastic.util.findPorts",
return_value=["/dev/cu.usbmodem4201"],
):
ds = devices.list_devices()
assert ds[0]["port"] == "tcp://localhost:4403"
assert ds[0]["likely_meshtastic"] is True
# ---------- connect() routing ---------------------------------------------
class TestConnectRoutesTcp:
def test_connect_uses_tcp_interface_for_tcp_port(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Verify the TCP branch instantiates `TCPInterface(hostname, portNumber)`
and never touches `SerialInterface`."""
# Make sure the env var doesn't leak in and confuse resolve_port.
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp, patch(
"meshtastic.serial_interface.SerialInterface"
) as mock_serial:
mock_tcp.return_value.close.return_value = None
with connection.connect(port="tcp://example.com:1234", timeout_s=12.0):
pass
mock_tcp.assert_called_once_with(
hostname="example.com",
portNumber=1234,
connectNow=True,
noProto=False,
timeout=12,
)
mock_serial.assert_not_called()
def test_connect_plumbs_timeout_to_serial_interface(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Verify the serial branch also propagates `timeout_s` so callers
passing a custom timeout to `device_info` / `list_nodes` / etc. don't
silently get the library default."""
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic.serial_interface.SerialInterface") as mock_serial, patch(
"meshtastic.tcp_interface.TCPInterface"
) as mock_tcp:
mock_serial.return_value.close.return_value = None
with connection.connect(port="/dev/cu.fake", timeout_s=20.0):
pass
mock_serial.assert_called_once_with(
devPath="/dev/cu.fake",
connectNow=True,
noProto=False,
timeout=20,
)
mock_tcp.assert_not_called()
def test_connect_releases_lock_on_tcp_failure(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("MESHTASTIC_MCP_TCP_HOST", raising=False)
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
mock_tcp.side_effect = RuntimeError("boom")
with pytest.raises(RuntimeError, match="boom"):
with connection.connect(port="tcp://locktest:4403"):
pass
# Lock should be released — a second connect attempt must not fail
# with "busy".
with patch("meshtastic.tcp_interface.TCPInterface") as mock_tcp:
mock_tcp.return_value.close.return_value = None
with connection.connect(port="tcp://locktest:4403"):
pass
+1 -1
View File
@@ -126,7 +126,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/56e1da4e7d30abcd746a2092a30e422f8cf5fc2b.zip
https://github.com/meshtastic/device-ui/archive/4bf593a82100b911ff816dddf7158ffdee2114cd.zip
; Common libs for environmental measurements in telemetry module
[environmental_base]
+15 -18
View File
@@ -26,6 +26,8 @@ SOFTWARE.*/
#include "DebugConfiguration.h"
#include <memory>
#ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
#endif
@@ -119,27 +121,22 @@ bool Syslog::vlogf(uint16_t pri, const char *fmt, va_list args)
bool Syslog::vlogf(uint16_t pri, const char *appName, const char *fmt, va_list args)
{
char *message;
size_t initialLen;
size_t len;
bool result;
// First measure the formatted length using a copy of args; passing args directly
// to vsnprintf consumes it, and reusing a consumed va_list is undefined behavior.
va_list args_measure;
va_copy(args_measure, args);
int needed = vsnprintf(nullptr, 0, fmt, args_measure);
va_end(args_measure);
initialLen = strlen(fmt);
if (needed < 0)
return false; // encoding error
message = new char[initialLen + 1];
auto message = std::unique_ptr<char[]>(new char[static_cast<size_t>(needed) + 1]);
int written = vsnprintf(message.get(), static_cast<size_t>(needed) + 1, fmt, args);
if (written < 0)
return false;
len = vsnprintf(message, initialLen + 1, fmt, args);
if (len > initialLen) {
delete[] message;
message = new char[len + 1];
vsnprintf(message, len + 1, fmt, args);
}
result = this->_sendLog(pri, appName, message);
delete[] message;
return result;
return this->_sendLog(pri, appName, message.get());
}
inline bool Syslog::_sendLog(uint16_t pri, const char *appName, const char *message)
+2
View File
@@ -755,8 +755,10 @@ void Power::reboot()
rp2040.reboot();
#elif defined(ARCH_PORTDUINO)
deInitApiServer();
#ifdef __linux__
if (aLinuxInputImpl)
aLinuxInputImpl->deInit();
#endif
SPI.end();
Wire.end();
Serial1.end();
+1 -1
View File
@@ -1,8 +1,8 @@
#pragma once
#include "../freertosinc.h"
#include "Print.h"
#include "mesh/generated/meshtastic/mesh.pb.h"
#include <Print.h>
#include <stdarg.h>
#include <string>
+7
View File
@@ -14,6 +14,11 @@ Lock::Lock() : handle(xSemaphoreCreateBinary())
}
}
Lock::~Lock()
{
vSemaphoreDelete(handle);
}
void Lock::lock()
{
if (xSemaphoreTake(handle, portMAX_DELAY) == false) {
@@ -30,6 +35,8 @@ void Lock::unlock()
#else
Lock::Lock() {}
Lock::~Lock() {}
void Lock::lock() {}
void Lock::unlock() {}
+1
View File
@@ -12,6 +12,7 @@ class Lock
{
public:
Lock();
~Lock();
Lock(const Lock &) = delete;
Lock &operator=(const Lock &) = delete;
+3
View File
@@ -157,6 +157,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#elif defined(HELTEC_MESH_NODE_T096)
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 12, 11, 10, 9, 8, 7
#elif defined(HELTEC_V4_R8)
#define NUM_PA_POINTS 22
#define TX_GAIN_LORA 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 11, 11, 10, 9, 8, 7
#else
// If a board enables USE_KCT8103L_PA but does not match a known variant and has
// not already provided a PA curve, fail at compile time to avoid unsafe defaults.
+11 -3
View File
@@ -1017,10 +1017,13 @@ void GPS::up()
setPowerState(GPS_ACTIVE);
}
// We've got a GPS lock. Enter a low power state, potentially.
// We've finished a GPS search cycle (lock or timeout). Enter a low power state, potentially.
void GPS::down()
{
scheduling.informGotLock();
if (hasValidLocation)
scheduling.informGotLock();
else
scheduling.informSearchFailed();
uint32_t predictedSearchDuration = scheduling.predictedSearchDurationMs();
uint32_t sleepTime = scheduling.msUntilNextSearch();
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval);
@@ -1545,7 +1548,12 @@ std::unique_ptr<GPS> GPS::createGps()
_en_gpio = PIN_GPS_EN;
#endif
#ifdef ARCH_PORTDUINO
if (!portduino_config.has_gps)
if (portduino_config.has_gps) {
// These need to set as flags so later checks will pass on native and GPS will work.
// They are not used for any hardware access.
_rx_gpio = 1;
_tx_gpio = 1;
} else
return nullptr;
#endif
if (!_rx_gpio || !_serial_gps) // Configured to have no GPS at all
+43 -6
View File
@@ -15,6 +15,19 @@ void GPSUpdateScheduling::informGotLock()
searchEndedMs = millis();
LOG_DEBUG("Took %us to get lock", (searchEndedMs - searchStartedMs) / 1000);
updateLockTimePrediction();
consecutiveFailures = 0; // Drop back to fast cadence as soon as we acquire any fix
}
// Search finished without obtaining a fix. We still need to mark the end time so
// the next sleep is timed correctly, but we must not feed the timeout duration
// into predictedMsToGetLock — doing so poisons msUntilNextSearch() and causes
// down() to fall into GPS_IDLE, leaving the chip awake on subsequent indoor cycles.
void GPSUpdateScheduling::informSearchFailed()
{
searchEndedMs = millis();
consecutiveFailures++;
LOG_DEBUG("GPS search ended without fix after %us (consecutive failures: %u)", (searchEndedMs - searchStartedMs) / 1000,
consecutiveFailures);
}
// Clear old lock-time prediction data.
@@ -25,6 +38,7 @@ void GPSUpdateScheduling::reset()
searchEndedMs = 0;
searchCount = 0;
predictedMsToGetLock = 0;
consecutiveFailures = 0;
}
// How many milliseconds before we should next search for GPS position
@@ -36,6 +50,20 @@ uint32_t GPSUpdateScheduling::msUntilNextSearch()
// Target interval (seconds), between GPS updates
uint32_t updateInterval = Default::getConfiguredOrDefaultMs(config.position.gps_update_interval, default_gps_update_interval);
// After a failed search, back off: indoors / no-sky environments will keep failing,
// so wake at most once per broadcast interval rather than once per gps_update_interval.
// Capped at 1 hour so a user-configured very-long broadcast interval still retries
// periodically (in case conditions change). Reset on any successful lock.
if (consecutiveFailures > 0) {
constexpr uint32_t failureRetryCapMs = 60UL * 60UL * 1000UL; // 1 hour cap
uint32_t failureSleepMs =
Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs);
if (failureSleepMs > failureRetryCapMs)
failureSleepMs = failureRetryCapMs;
if (updateInterval < failureSleepMs)
updateInterval = failureSleepMs;
}
// Check how long until we should start searching, to hopefully hit our target interval
uint32_t dueAtMs = searchEndedMs + updateInterval;
uint32_t compensatedStart = dueAtMs - predictedMsToGetLock;
@@ -70,20 +98,29 @@ bool GPSUpdateScheduling::isUpdateDue()
// Have we been searching for a GPS position for too long?
bool GPSUpdateScheduling::searchedTooLong()
{
constexpr uint32_t oneMinuteMs = 60UL * 1000UL;
constexpr uint32_t maxSearchClampMs = 15UL * oneMinuteMs; // Hard cap: 15 minutes is always too long
constexpr uint32_t postFailureSearchMs = 5UL * oneMinuteMs; // Tighter dwell once we know the environment is hostile
uint32_t elapsed = elapsedSearchMs();
// Anything over 15 minutes is too long, regardless of the broadcast interval.
if (elapsed > maxSearchClampMs)
return true;
// After a prior failed search, shorten the dwell
if (consecutiveFailures > 0 && elapsed > postFailureSearchMs)
return true;
uint32_t minimumOrConfiguredSecs =
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, default_broadcast_interval_secs);
uint32_t maxSearchMs = Default::getConfiguredOrDefaultMs(minimumOrConfiguredSecs, default_broadcast_interval_secs);
// If broadcast interval set to max, no such thing as "too long"
if (maxSearchMs == UINT32_MAX)
return false;
// If we've been searching longer than our position broadcast interval: that's too long
else if (elapsedSearchMs() > maxSearchMs)
if (elapsed > maxSearchMs)
return true;
// Otherwise, not too long yet!
else
return false;
return false;
}
// Updates the predicted time-to-get-lock, by exponentially smoothing the latest observation
+3 -1
View File
@@ -8,7 +8,8 @@ class GPSUpdateScheduling
public:
// Marks the time of these events, for calculation use
void informSearching();
void informGotLock(); // Predicted lock-time is recalculated here
void informGotLock(); // Predicted lock-time is recalculated here
void informSearchFailed(); // Search ended without a fix; prediction is left untouched
void reset(); // Reset the prediction - after GPS::disable() / GPS::enable()
bool isUpdateDue(); // Is it time to begin searching for a GPS position?
@@ -24,6 +25,7 @@ class GPSUpdateScheduling
uint32_t searchEndedMs = 0;
uint32_t searchCount = 0;
uint32_t predictedMsToGetLock = 0;
uint32_t consecutiveFailures = 0; // Count of search cycles that ended without a fix; reset on lock
const float weighting = 0.2; // Controls exponential smoothing of lock-times prediction. 20% weighting of "latest lock-time".
};
+15 -4
View File
@@ -428,7 +428,7 @@ static LGFX *tft = nullptr;
#elif defined(ST7789_CS)
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
#ifdef HELTEC_V4_TFT
#if defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)
#include "chsc6x.h"
#include "lgfx/v1/Touch.hpp"
namespace lgfx
@@ -450,7 +450,11 @@ class TOUCH_CHSC6X : public ITouch
bool init(void) override
{
if (chsc6xTouch == nullptr) {
#if (TOUCH_I2C_PORT == 1)
chsc6xTouch = new chsc6x(&Wire1, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
#else
chsc6xTouch = new chsc6x(&Wire, TOUCH_SDA_PIN, TOUCH_SCL_PIN, TOUCH_INT_PIN, TOUCH_RST_PIN);
#endif
}
chsc6xTouch->chsc6x_init();
return true;
@@ -487,7 +491,7 @@ class LGFX : public lgfx::LGFX_Device
#if HAS_TOUCHSCREEN
#if defined(T_WATCH_S3) || defined(ELECROW)
lgfx::Touch_FT5x06 _touch_instance;
#elif defined(HELTEC_V4_TFT)
#elif defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)
lgfx::TOUCH_CHSC6X _touch_instance;
#else
lgfx::Touch_GT911 _touch_instance;
@@ -506,7 +510,11 @@ class LGFX : public lgfx::LGFX_Device
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
// 80MHz by an integer)
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
cfg.spi_3wire = false;
#ifdef SPI_3_WIRE
cfg.spi_3wire = SPI_3_WIRE;
#else
cfg.spi_3wire = true; // Set to true if reception is done on the MOSI pin
#endif
cfg.use_lock = true; // Set to true to use transaction locking
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
// SPI_DMA_CH_AUTO=auto setting)
@@ -556,8 +564,11 @@ class LGFX : public lgfx::LGFX_Device
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
cfg.dlen_16bit =
false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
#if defined(HAS_SDCARD)
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
#else
cfg.bus_shared = false;
#endif
// Set the following only when the display is shifted with a driver with a variable number of pixels, such as the
// ST7735 or ILI9163.
// cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
+3
View File
@@ -390,8 +390,11 @@ void InputBroker::Init()
seesawRotary = nullptr;
}
}
#ifdef __linux__
// Linux evdev keyboard input only — macOS has no <linux/input.h>.
aLinuxInputImpl = new LinuxInputImpl();
aLinuxInputImpl->init();
#endif
}
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
+4 -1
View File
@@ -1,5 +1,8 @@
#pragma once
#if ARCH_PORTDUINO
// Linux evdev keyboard input. Only compiled on Linux portduino targets;
// macOS / non-Linux builds have no <linux/input.h> or epoll, and the
// headless build doesn't need real keyboards anyway.
#if ARCH_PORTDUINO && defined(__linux__)
#include "InputBroker.h"
#include "concurrency/OSThread.h"
#include <assert.h>
+2 -1
View File
@@ -1,4 +1,5 @@
#ifdef ARCH_PORTDUINO
// Linux evdev impl. Same Linux-only gating as LinuxInput.h.
#if defined(ARCH_PORTDUINO) && defined(__linux__)
#pragma once
#include "LinuxInput.h"
#include "main.h"
+3 -8
View File
@@ -13,7 +13,6 @@ RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl()
{
rotary = nullptr;
#ifdef ARCH_ESP32
isFirstInit = true;
#endif
@@ -23,11 +22,6 @@ RotaryEncoderImpl::~RotaryEncoderImpl()
{
LOG_DEBUG("RotaryEncoderImpl destructor");
detachRotaryEncoderInterrupts();
if (rotary != nullptr) {
delete rotary;
rotary = nullptr;
}
}
bool RotaryEncoderImpl::init()
@@ -43,8 +37,9 @@ bool RotaryEncoderImpl::init()
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
if (rotary == nullptr) {
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
moduleConfig.canned_message.inputbroker_pin_press);
rotary.reset(new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a,
moduleConfig.canned_message.inputbroker_pin_b,
moduleConfig.canned_message.inputbroker_pin_press));
}
attachRotaryEncoderInterrupts();
+2 -1
View File
@@ -5,6 +5,7 @@
#include "InputBroker.h"
#include "concurrency/OSThread.h"
#include "mesh/NodeDB.h"
#include <memory>
class RotaryEncoder;
@@ -28,7 +29,7 @@ class RotaryEncoderImpl final : public InputPollable
input_broker_event eventCcw = INPUT_BROKER_NONE;
input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary;
std::unique_ptr<RotaryEncoder> rotary;
private:
#ifdef ARCH_ESP32
+18 -8
View File
@@ -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
}
}
+11 -1
View File
@@ -19,8 +19,12 @@ extern Adafruit_nRFCrypto nRFCrypto;
#include <Arduino.h>
#elif defined(ARCH_PORTDUINO)
#include <random>
#include <sys/random.h>
#include <unistd.h>
#ifdef __linux__
#include <sys/random.h> // getrandom()
#else
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
#endif
#endif
namespace HardwareRNG
@@ -119,10 +123,16 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
filled = true;
#elif defined(ARCH_PORTDUINO)
// Prefer the host OS RNG first when running under Portduino.
#ifdef __linux__
ssize_t generated = ::getrandom(buffer, length, 0);
if (generated == static_cast<ssize_t>(length)) {
filled = true;
}
#else
// arc4random_buf is available on Darwin/BSD and cannot fail.
::arc4random_buf(buffer, length);
filled = true;
#endif
if (!filled) {
fillWithRandomDevice(buffer, length);
+27
View File
@@ -6,6 +6,33 @@
#include "configuration.h"
#include "detect/LoRaRadioType.h"
// Sentinel marking the end of a modem preset array. Declared `const` rather
// than `constexpr` because the cast from 0xFF to the enum is out-of-range and
// therefore not a valid constant expression on Clang 16+ (Apple Clang on
// macOS). The value is only ever compared at runtime, so static-init is fine.
static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
// Region profile: bundles the preset list with regulatory parameters shared across regions
struct RegionProfile {
const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated; first entry is the default
float spacing; // gaps between radio channels
float padding; // padding at each side of the "operating channel"
bool audioPermitted;
bool licensedOnly; // a region profile for licensed operators only
int8_t textThrottle; // throttle for text - future expansion
int8_t positionThrottle; // throttle for location data - future expansion
int8_t telemetryThrottle; // throttle for telemetry - future expansion
uint8_t overrideSlot; // a per-region override slot for if we need to fix it in place
};
extern const RegionProfile PROFILE_STD;
extern const RegionProfile PROFILE_EU868;
extern const RegionProfile PROFILE_UNDEF;
// extern const RegionProfile PROFILE_LITE;
// extern const RegionProfile PROFILE_NARROW;
// extern const RegionProfile PROFILE_HAM;
// Map from old region names to new region enums
struct RegionInfo {
meshtastic_Config_LoRaConfig_RegionCode code;
+3 -3
View File
@@ -688,7 +688,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT)) && \
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT)) && \
HAS_TFT
// switch BT off by default; use TFT programming mode or hotkey to enable
config.bluetooth.enabled = false;
@@ -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) {
+23 -32
View File
@@ -16,7 +16,7 @@
#define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging
#define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots
PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members
PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initialize members
{
if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense
LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX);
@@ -25,7 +25,7 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
// Allocate memory for the recent packets array
recentPacketsCapacity = size;
recentPackets = new PacketRecord[recentPacketsCapacity];
recentPackets.reset(new PacketRecord[recentPacketsCapacity]);
if (!recentPackets) { // No logging here, console/log probably uninitialized yet.
LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size,
sizeof(PacketRecord) * recentPacketsCapacity);
@@ -34,14 +34,7 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
}
// Initialize the recent packets array to zero
memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity);
}
PacketHistory::~PacketHistory()
{
recentPacketsCapacity = 0;
delete[] recentPackets;
recentPackets = NULL;
memset(recentPackets.get(), 0, sizeof(PacketRecord) * recentPacketsCapacity);
}
/** Update recentPackets and return true if we have already seen this packet */
@@ -205,13 +198,14 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
return NULL;
}
PacketRecord *base = recentPackets.get();
PacketRecord *it = NULL;
for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
for (it = base; it < (base + recentPacketsCapacity); ++it) {
if (it->id == id && it->sender == sender) {
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender,
it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec),
it - recentPackets, recentPacketsCapacity);
it - base, recentPacketsCapacity);
#endif
// only the first match is returned, so be careful not to create duplicate entries
return it; // Return pointer to the found record
@@ -229,39 +223,38 @@ void PacketHistory::insert(const PacketRecord &r)
{
uint32_t now_millis = millis(); // Should not jump with time changes
uint32_t OldtrxTimeMsec = 0;
PacketRecord *base = recentPackets.get();
PacketRecord *tu = NULL; // Will insert here.
PacketRecord *it = NULL;
// Find a free, matching or oldest used slot in the recentPackets array
for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
for (it = base; it < (base + recentPacketsCapacity); ++it) {
if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty
tu = it; // Remember the free slot
#if VERBOSE_PACKET_HISTORY >= 2
LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity);
LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - base, recentPacketsCapacity);
#endif
// We have that, Exit the loop
it = (recentPackets + recentPacketsCapacity);
it = (base + recentPacketsCapacity);
} else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert
tu = it; // Remember the matching slot
OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age
#if VERBOSE_PACKET_HISTORY >= 2
LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity,
OldtrxTimeMsec);
LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
#endif
// We have that, Exit the loop
it = (recentPackets + recentPacketsCapacity);
it = (base + recentPacketsCapacity);
} else {
if (it->rxTimeMsec == 0) {
LOG_WARN(
"Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!",
it->sender, it->id, it - recentPackets, recentPacketsCapacity);
it->sender, it->id, it - base, recentPacketsCapacity);
}
if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly
OldtrxTimeMsec = now_millis - it->rxTimeMsec;
tu = it; // remember the oldest packet
#if VERBOSE_PACKET_HISTORY >= 2
LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity,
OldtrxTimeMsec);
LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
#endif
}
// keep looking for oldest till entire array is checked
@@ -276,13 +269,11 @@ void PacketHistory::insert(const PacketRecord &r)
#if VERBOSE_PACKET_HISTORY
if (tu->id == 0 && tu->sender == 0) {
LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity);
LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - base, recentPacketsCapacity);
} else if (tu->id == r.id && tu->sender == r.sender) {
LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity,
OldtrxTimeMsec);
LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
} else {
LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity,
OldtrxTimeMsec);
LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - base, recentPacketsCapacity, OldtrxTimeMsec);
}
#endif
@@ -315,9 +306,9 @@ void PacketHistory::insert(const PacketRecord &r)
#endif
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE",
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
tu->relayed_by[2], tu->rxTimeMsec);
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", tu - base,
recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
tu->rxTimeMsec);
#endif
if (r.rxTimeMsec == 0) {
@@ -330,9 +321,9 @@ void PacketHistory::insert(const PacketRecord &r)
*tu = r; // store the packet
#if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER",
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
tu->relayed_by[2], tu->rxTimeMsec);
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", tu - base,
recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], tu->relayed_by[2],
tu->rxTimeMsec);
#endif
}
+3 -5
View File
@@ -1,6 +1,7 @@
#pragma once
#include "NodeDB.h"
#include <memory>
// Number of relayers we keep track of. Use 6 to be efficient with memory alignment of PacketRecord to 20 bytes
#define NUM_RELAYERS 6
@@ -26,7 +27,7 @@ class PacketHistory
uint32_t recentPacketsCapacity =
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat.
std::unique_ptr<PacketRecord[]> recentPackets; // Simple and fixed in size. Debloat.
/** Find a packet record in history.
* @param sender NodeNum
@@ -48,11 +49,8 @@ class PacketHistory
uint8_t getOurTxHopLimit(const PacketRecord &r);
void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);
PacketHistory(const PacketHistory &); // non construction-copyable
PacketHistory &operator=(const PacketHistory &); // non copyable
public:
explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX
~PacketHistory();
/**
* Update recentBroadcasts and return true if we have already seen this packet
@@ -74,5 +72,5 @@ class PacketHistory
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
// To check if the PacketHistory was initialized correctly by constructor
bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; }
bool initOk(void) { return recentPackets != nullptr && recentPacketsCapacity != 0; }
};
+7 -6
View File
@@ -512,11 +512,6 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
// LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard,
// nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name);
// Occasional progress logging. (readIndex==2 will be true for the first non-us node)
if (readIndex == 2 || readIndex % 20 == 0) {
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
}
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag;
fromRadioScratch.node_info = infoToSend;
prefetchNodeInfos();
@@ -647,9 +642,11 @@ void PhoneAPI::releaseQueueStatusPhonePacket()
void PhoneAPI::prefetchNodeInfos()
{
bool added = false;
bool wasEmpty = false;
// Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment.
{
concurrency::LockGuard guard(&nodeInfoMutex);
wasEmpty = nodeInfoQueue.empty();
while (nodeInfoQueue.size() < kNodePrefetchDepth) {
auto nextNode = nodeDB->readNextMeshNode(readIndex);
if (!nextNode)
@@ -663,11 +660,15 @@ void PhoneAPI::prefetchNodeInfos()
info.via_mqtt = isUs ? false : info.via_mqtt;
info.is_favorite = info.is_favorite || isUs;
nodeInfoQueue.push_back(info);
// Log progress here (at fetch time) so readIndex is accurate and each value logs only once.
if (readIndex == 2 || readIndex % 20 == 0) {
LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes());
}
added = true;
}
}
if (added)
if (added && wasEmpty)
onNowHasData(0);
}
+1 -1
View File
@@ -109,7 +109,7 @@ bool RadioLibInterface::canSendImmediately()
return true;
}
bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag)
bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag)
{
bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag));
// Handle false detections
+1 -1
View File
@@ -213,7 +213,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
protected:
uint32_t activeReceiveStart = 0;
bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag);
bool receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag);
/** Do any hardware setup needed on entry into send configuration for the radio.
* Subclasses can customize, but must also call this base method */
+7 -4
View File
@@ -719,9 +719,13 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
// Also, we should set the time from the ISR and it should have msec level resolution
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
// Store a copy of encrypted packet for MQTT
// Store a copy of the encrypted packet for MQTT.
// Local, not a class member: handleReceived re-enters itself when a module
// reply broadcast goes through MeshService::sendToMesh -> Router::sendLocal,
// and a member would be silently overwritten without release on the inner
// call. Each invocation now owns its own copy (issue #9632, #10101, #8729).
DEBUG_HEAP_BEFORE;
p_encrypted = packetPool.allocCopy(*p);
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
@@ -815,8 +819,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
#endif
}
packetPool.release(p_encrypted); // Release the encrypted packet
p_encrypted = nullptr;
packetPool.release(p_encrypted); // Release the encrypted packet (release() handles nullptr)
}
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
-3
View File
@@ -92,9 +92,6 @@ class Router : protected concurrency::OSThread, protected PacketHistory
before us */
uint32_t rxDupe = 0, txRelayCanceled = 0;
// pointer to the encrypted packet
meshtastic_MeshPacket *p_encrypted = nullptr;
protected:
friend class RoutingModule;
+1 -1
View File
@@ -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" */
+8 -4
View File
@@ -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
+1 -1
View File
@@ -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
+25 -10
View File
@@ -1,22 +1,34 @@
/*
Adds a WebServer and WebService callbacks to meshtastic as Linux Version. The WebServer & Webservices
runs in a real linux thread beside the portdunio threading emulation. It replaces the complete ESP32
Webserver libs including generation of SSL certifcicates, because the use ESP specific details in
the lib that can't be emulated.
Adds a WebServer and WebService callbacks to meshtastic via the Portduino/native target (Linux and
macOS). The WebServer & Webservices run in a real host thread beside the Portduino threading
emulation. It replaces the complete ESP32 Webserver libs including generation of SSL certificates,
because those libs use ESP-specific details that can't be emulated.
The WebServices adapt to the two major phoneapi functions "handleAPIv1FromRadio,handleAPIv1ToRadio"
The WebServer just adds basaic support to deliver WebContent, so it can be used to
deliver the WebGui definded by the WebClient Project.
The WebServer just adds basic support to deliver WebContent, so it can be used to
deliver the WebGui defined by the WebClient Project.
Steps to get it running:
1.) Add these Linux Libs to the compile and target machine:
Linux (apt):
1.) Add these libs to the compile and target machine:
sudo apt update && \
apt -y install openssl libssl-dev libopenssl libsdl2-dev \
apt -y install openssl libssl-dev libsdl2-dev \
libulfius-dev liborcania-dev
macOS (Homebrew):
1.) Install prerequisites via Homebrew:
brew install ulfius openssl@3
The PlatformIO env (native-macos) picks up compiler/linker flags via
`pkg-config`. In particular, OpenSSL needs `pkg-config --cflags --libs openssl@3`
so both the Homebrew include path and linker flags are provided; ulfius and its
dependencies (liborcania, libyder) are also resolved via `pkg-config`.
2.) Configure the root directory of the web Content in the config.yaml file.
The followinng tags should be included and set at your needs
The following tags should be included and set at your needs
Example entry in the config.yaml
Webserver:
@@ -34,7 +46,10 @@ Author: Marc Philipp Hammermann
mail: marchammermann@googlemail.com
*/
#ifdef PORTDUINO_LINUX_HARDWARE
// Mirrors the guard in PiWebServer.h — see comment there. macOS Homebrew
// provides ulfius + deps; Linux pulls them via apt. Either way, this
// translation unit only compiles when the headers are present.
#ifdef ARCH_PORTDUINO
#if __has_include(<ulfius.h>)
#include "PiWebServer.h"
#include "NodeDB.h"
+5 -1
View File
@@ -1,5 +1,9 @@
#pragma once
#ifdef PORTDUINO_LINUX_HARDWARE
// Portduino webserver is built whenever the ulfius headers are reachable,
// not only on Linux. macOS users can `brew install ulfius` to enable it;
// without ulfius the entire body is skipped and main.cpp's matching
// __has_include guard avoids referencing the type.
#ifdef ARCH_PORTDUINO
#if __has_include(<ulfius.h>)
#include "PhoneAPI.h"
#include "ulfius-cfg.h"
+2 -2
View File
@@ -80,9 +80,9 @@ void StoreForwardModule::populatePSRAM()
(this->records ? this->records : (((memGet.getFreePsram() / 4) * 3) / sizeof(PacketHistoryStruct)));
this->records = numberOfPackets;
#if defined(ARCH_ESP32)
this->packetHistory = static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct)));
this->packetHistory.reset(static_cast<PacketHistoryStruct *>(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))));
#elif defined(ARCH_PORTDUINO)
this->packetHistory = static_cast<PacketHistoryStruct *>(calloc(numberOfPackets, sizeof(PacketHistoryStruct)));
this->packetHistory.reset(static_cast<PacketHistoryStruct *>(calloc(numberOfPackets, sizeof(PacketHistoryStruct))));
#endif
+8 -1
View File
@@ -7,6 +7,7 @@
#include "configuration.h"
#include <Arduino.h>
#include <functional>
#include <memory>
#include <unordered_map>
struct PacketHistoryStruct {
@@ -29,11 +30,17 @@ struct PacketHistoryStruct {
class StoreForwardModule : private concurrency::OSThread, public ProtobufModule<meshtastic_StoreAndForward>
{
// packetHistory is allocated with ps_calloc / calloc, so it must be released with free(),
// not delete[].
struct CFreeDeleter {
void operator()(PacketHistoryStruct *p) const noexcept { free(p); }
};
bool busy = 0;
uint32_t busyTo = 0;
char routerMessage[meshtastic_Constants_DATA_PAYLOAD_LEN] = {0};
PacketHistoryStruct *packetHistory = 0;
std::unique_ptr<PacketHistoryStruct[], CFreeDeleter> packetHistory;
uint32_t packetHistoryTotalCount = 0;
uint32_t last_time = 0;
uint32_t requestCount = 0;
+1 -1
View File
@@ -32,7 +32,7 @@
#include <assert.h>
#include <utility>
#include <IPAddress.h>
#include "IPAddress.h"
#if defined(ARCH_PORTDUINO)
#include <netinet/in.h>
#elif !defined(ntohl)
+86 -20
View File
@@ -9,26 +9,41 @@
#include "PortduinoGlue.h"
#include "SHA256.h"
#include "api/ServerAPI.h"
#include "linux/gpio/LinuxGPIOPin.h"
#include "meshUtils.h"
#include <ErriezCRC32.h>
#include <Utility.h>
#include <assert.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <stdexcept>
#include <sys/ioctl.h>
#include <unistd.h>
#ifdef PORTDUINO_LINUX_HARDWARE
#include "linux/gpio/LinuxGPIOPin.h"
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#endif
#ifdef PORTDUINO_LINUX_HARDWARE
#include <cxxabi.h>
#endif
#ifdef __APPLE__
// Used by getMacAddr()'s macOS fallback to read the en0 link-layer address.
// `getifaddrs()` is the BSD-portable way; `<net/if_dl.h>` provides the
// `sockaddr_dl` cast and the `LLADDR()` macro that points at the 6-byte MAC.
#include <cstring> // strcmp, memcpy
#include <ifaddrs.h>
#include <net/if.h>
#include <net/if_dl.h>
#endif
#include "platform/portduino/USBHal.h"
portduino_config_struct portduino_config;
@@ -130,9 +145,9 @@ void getMacAddr(uint8_t *dmac)
}
} else if (portduino_config.mac_address.length() > 11) {
MAC_from_string(portduino_config.mac_address, dmac);
exit;
return;
} else {
#ifdef PORTDUINO_LINUX_HARDWARE
struct hci_dev_info di = {0};
di.dev_id = 0;
bdaddr_t bdaddr;
@@ -152,6 +167,37 @@ void getMacAddr(uint8_t *dmac)
dmac[3] = di.bdaddr.b[2];
dmac[4] = di.bdaddr.b[1];
dmac[5] = di.bdaddr.b[0];
#elif defined(__APPLE__)
// No BlueZ on macOS, but we can fall back to the host's primary
// network interface MAC. `en0` is Wi-Fi on every shipping Mac
// (Ethernet, when present, is en1 or higher), which gives the user
// the same kind of stable, host-derived identifier that the BlueZ
// path provides on Linux. If en0 isn't found or has no MAC, dmac is
// left untouched and the caller's "Blank MAC Address not allowed!"
// check will still fire — preserving existing behavior for users
// who deliberately rely on --hwid or YAML override.
struct ifaddrs *ifap = nullptr;
if (getifaddrs(&ifap) == 0) {
for (struct ifaddrs *p = ifap; p != nullptr; p = p->ifa_next) {
if (p->ifa_addr == nullptr || p->ifa_addr->sa_family != AF_LINK) {
continue;
}
if (strcmp(p->ifa_name, "en0") != 0) {
continue;
}
auto *sdl = reinterpret_cast<struct sockaddr_dl *>(p->ifa_addr);
if (sdl->sdl_alen == 6) {
memcpy(dmac, LLADDR(sdl), 6);
break;
}
}
freeifaddrs(ifap);
}
#else
// No platform-specific MAC source; leave dmac at its default. Caller
// can override via the --hwid CLI flag or the YAML config.
(void)dmac;
#endif
}
}
@@ -252,10 +298,9 @@ void portduinoSetup()
// Try CH341
try {
std::cout << "autoconf: Looking for CH341 device..." << std::endl;
ch341Hal = new Ch341Hal(0, portduino_config.lora_usb_serial_num, portduino_config.lora_usb_vid,
portduino_config.lora_usb_pid);
ch341Hal->getProductString(autoconf_product, 95);
delete ch341Hal;
auto probe = std::unique_ptr<Ch341Hal>(new Ch341Hal(0, portduino_config.lora_usb_serial_num,
portduino_config.lora_usb_vid, portduino_config.lora_usb_pid));
probe->getProductString(autoconf_product, 95);
std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl;
found_ch341 = true;
@@ -482,10 +527,17 @@ void portduinoSetup()
exit(EXIT_FAILURE);
}
char serial[9] = {0};
ch341Hal->getSerialString(serial, 8);
// Pass the full buffer size (9 = 8 chars + null) to getSerialString,
// not 8. The function treats `len` as buffer size and reserves one
// slot for the null terminator, so passing 8 produced a 7-char serial
// and broke the `strlen(serial) == 8` check below — masked on Linux
// by the BlueZ HCI MAC fallback in getMacAddr(), but on macOS (where
// the BlueZ path is __linux__-guarded) it left mac_address empty and
// meshtasticd refused to start.
ch341Hal->getSerialString(serial, sizeof(serial));
std::cout << "CH341 Serial " << serial << std::endl;
char product_string[96] = {0};
ch341Hal->getProductString(product_string, 95);
ch341Hal->getProductString(product_string, sizeof(product_string));
std::cout << "CH341 Product " << product_string << std::endl;
if (strlen(serial) == 8 && portduino_config.mac_address.length() < 12) {
std::cout << "Deriving MAC address from Serial and Product String" << std::endl;
@@ -1037,17 +1089,31 @@ static bool ends_with(std::string_view str, std::string_view suffix)
bool MAC_from_string(std::string mac_str, uint8_t *dmac)
{
mac_str.erase(std::remove(mac_str.begin(), mac_str.end(), ':'), mac_str.end());
if (mac_str.length() == 12) {
dmac[0] = std::stoi(portduino_config.mac_address.substr(0, 2), nullptr, 16);
dmac[1] = std::stoi(portduino_config.mac_address.substr(2, 2), nullptr, 16);
dmac[2] = std::stoi(portduino_config.mac_address.substr(4, 2), nullptr, 16);
dmac[3] = std::stoi(portduino_config.mac_address.substr(6, 2), nullptr, 16);
dmac[4] = std::stoi(portduino_config.mac_address.substr(8, 2), nullptr, 16);
dmac[5] = std::stoi(portduino_config.mac_address.substr(10, 2), nullptr, 16);
return true;
} else {
if (mac_str.length() != 12) {
return false;
}
// Validate every character is a hex digit before parsing. std::stoi
// would otherwise skip leading whitespace and silently truncate at the
// first non-digit, which is too lenient for a MAC address.
for (char c : mac_str) {
if (!isxdigit(static_cast<unsigned char>(c))) {
return false;
}
}
// Parse into a temporary so dmac is not partially modified if a later
// byte fails. At least one caller in getMacAddr() ignores the bool
// return, so leaving stale bytes in dmac on failure would silently
// produce a wrong MAC.
uint8_t tmp[6];
try {
for (int i = 0; i < 6; i++) {
tmp[i] = static_cast<uint8_t>(std::stoi(mac_str.substr(i * 2, 2), nullptr, 16));
}
} catch (const std::exception &) {
return false;
}
memcpy(dmac, tmp, 6);
return true;
}
std::string exec(const char *cmd)
+7 -3
View File
@@ -5,6 +5,7 @@
#include "platform/portduino/PortduinoGlue.h"
#include <RadioLib.h>
#include <csignal>
#include <cstring>
#include <iostream>
#include <libpinedio-usb.h>
#include <unistd.h>
@@ -34,7 +35,7 @@ class Ch341Hal : public RadioLibHal
: RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING)
{
if (serial != "") {
strncpy(pinedio.serial_number, serial.c_str(), 8);
std::strncpy(pinedio.serial_number, serial.c_str(), 8);
pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1);
}
// LOG_INFO("USB Serial: %s", pinedio.serial_number);
@@ -59,8 +60,11 @@ class Ch341Hal : public RadioLibHal
void getSerialString(char *_serial, size_t len)
{
len = len > 8 ? 8 : len;
strncpy(_serial, pinedio.serial_number, len);
if (len == 0)
return;
size_t bytesCopied = (len - 1) < 8 ? (len - 1) : 8;
std::strncpy(_serial, pinedio.serial_number, bytesCopied);
_serial[bytesCopied] = '\0';
}
void getProductString(char *_product_string, size_t len)
+61
View File
@@ -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());
}
+195
View File
@@ -0,0 +1,195 @@
// Unit tests for MAC_from_string in src/platform/portduino/PortduinoGlue.cpp.
//
// Regression coverage for when the function stripped colons from
// its mac_str parameter but then read bytes from the global
// portduino_config.mac_address. Symptoms: --hwid silently ignored when
// MACAddress: was also set, and SIGABRT (stoi: no conversion) when --hwid
// was used without MACAddress: in config.yaml.
#include "Arduino.h"
#include "TestUtil.h"
#include <cstdint>
#include <cstring>
#include <string>
#include <unity.h>
// Forward-declare instead of including PortduinoGlue.h to avoid pulling in
// LR11x0Interface, USBHal, mesh.pb.h, yaml-cpp, and the full portduino_config
// struct just to test a self-contained string parser. The symbol is defined
// in PortduinoGlue.cpp and resolved at link time.
bool MAC_from_string(std::string mac_str, uint8_t *dmac);
void setUp(void) {}
void tearDown(void) {}
// --- Happy-path parsing ---
void test_colon_separated_uppercase()
{
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AA:BB:CC:DD:EE:FF", dmac));
TEST_ASSERT_EQUAL_HEX8(0xAA, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xBB, dmac[1]);
TEST_ASSERT_EQUAL_HEX8(0xCC, dmac[2]);
TEST_ASSERT_EQUAL_HEX8(0xDD, dmac[3]);
TEST_ASSERT_EQUAL_HEX8(0xEE, dmac[4]);
TEST_ASSERT_EQUAL_HEX8(0xFF, dmac[5]);
}
void test_colon_separated_lowercase()
{
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("02:ca:fe:ba:be:01", dmac));
TEST_ASSERT_EQUAL_HEX8(0x02, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xCA, dmac[1]);
TEST_ASSERT_EQUAL_HEX8(0xFE, dmac[2]);
TEST_ASSERT_EQUAL_HEX8(0xBA, dmac[3]);
TEST_ASSERT_EQUAL_HEX8(0xBE, dmac[4]);
TEST_ASSERT_EQUAL_HEX8(0x01, dmac[5]);
}
void test_no_colons_packed_hex()
{
// The CLI form produced by some tools — 12 hex chars, no separators.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AABBCCDDEEFF", dmac));
TEST_ASSERT_EQUAL_HEX8(0xAA, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xFF, dmac[5]);
}
void test_two_distinct_inputs_yield_distinct_outputs()
{
// Direct regression for the original bug: parsing two different MAC
// strings in succession must produce two different byte sequences.
// Pre-fix, both calls would have produced identical bytes derived from
// the (untouched) global portduino_config.mac_address.
uint8_t a[6] = {0};
uint8_t b[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AA:BB:CC:DD:EE:FF", a));
TEST_ASSERT_TRUE(MAC_from_string("02:CA:FE:BA:BE:01", b));
TEST_ASSERT_NOT_EQUAL(0, std::memcmp(a, b, 6));
TEST_ASSERT_EQUAL_HEX8(0xAA, a[0]);
TEST_ASSERT_EQUAL_HEX8(0x02, b[0]);
}
void test_does_not_read_external_state()
{
// The function must derive every byte from its parameter, not from any
// global. Provide a unique MAC and verify all six bytes match the input
// exactly — leaves no room for the function to be smuggling bytes from
// elsewhere.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("12:34:56:78:9A:BC", dmac));
const uint8_t expected[6] = {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC};
TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, dmac, 6);
}
// --- Rejected inputs ---
// Pre-fix, the empty/short cases either crashed (stoi exception on substr("")
// of the empty global) or silently filled dmac with stale bytes. Post-fix,
// the length guard rejects them cleanly with `false` and dmac is unchanged.
void test_empty_string_returns_false()
{
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("", dmac));
// dmac must be untouched on failure.
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_too_short_returns_false()
{
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_too_long_returns_false()
{
uint8_t dmac[6] = {0};
// 14 hex chars after colon-strip > 12.
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC:DD:EE:FF:00", dmac));
}
void test_only_colons_returns_false()
{
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string(":::::", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_extra_colons_still_parses()
{
// Colon stripping happens before length check, so an unconventional
// grouping that totals 12 hex chars after stripping is still accepted.
uint8_t dmac[6] = {0};
TEST_ASSERT_TRUE(MAC_from_string("AABB:CCDD:EEFF", dmac));
TEST_ASSERT_EQUAL_HEX8(0xAA, dmac[0]);
TEST_ASSERT_EQUAL_HEX8(0xFF, dmac[5]);
}
void test_non_hex_input_returns_false()
{
// 12 chars of non-hex would have made std::stoi throw before the
// try/catch wrapper was added, killing the daemon. Now must return false
// and leave dmac untouched.
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("ZZ:ZZ:ZZ:ZZ:ZZ:ZZ", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_partial_hex_failure_preserves_dmac()
{
// First five bytes are valid hex; the sixth ("ZZ") is not. Without the
// temp-buffer staging, dmac would be partially overwritten with the five
// good bytes plus stale data in slot 5 — silently producing a wrong MAC
// since the only caller that uses this in getMacAddr() ignores the bool
// return value.
uint8_t dmac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11};
uint8_t before[6];
std::memcpy(before, dmac, 6);
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC:DD:EE:ZZ", dmac));
TEST_ASSERT_EQUAL_HEX8_ARRAY(before, dmac, 6);
}
void test_embedded_non_hex_returns_false()
{
// std::stoi tolerates leading whitespace and a "0x" prefix, so a stray
// space inside a 2-char window like " F" would silently parse as 0xF.
// The per-character isxdigit() pre-check rejects these. The 14-char
// "0xAABBCCDDEEFF" is also rejected by the length check.
uint8_t dmac[6] = {0};
TEST_ASSERT_FALSE(MAC_from_string("AA:BB:CC:DD:EE: F", dmac));
TEST_ASSERT_FALSE(MAC_from_string("0xAABBCCDDEEFF", dmac));
}
// --- Unity lifecycle ---
void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_colon_separated_uppercase);
RUN_TEST(test_colon_separated_lowercase);
RUN_TEST(test_no_colons_packed_hex);
RUN_TEST(test_two_distinct_inputs_yield_distinct_outputs);
RUN_TEST(test_does_not_read_external_state);
RUN_TEST(test_empty_string_returns_false);
RUN_TEST(test_too_short_returns_false);
RUN_TEST(test_too_long_returns_false);
RUN_TEST(test_only_colons_returns_false);
RUN_TEST(test_extra_colons_still_parses);
RUN_TEST(test_non_hex_input_returns_false);
RUN_TEST(test_partial_hex_failure_preserves_dmac);
RUN_TEST(test_embedded_non_hex_returns_false);
exit(UNITY_END());
}
void loop() {}
+1 -1
View File
@@ -12,4 +12,4 @@ build_flags =
lib_deps =
${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
+1 -1
View File
@@ -35,4 +35,4 @@ lib_ignore =
lib_deps =
${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
+1 -1
View File
@@ -11,7 +11,7 @@ build_flags =
lib_deps =
${esp32_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
# renovate: datasource=custom.pio depName=SX1509 IO Expander packageName=sparkfun/library/SX1509 IO Expander
sparkfun/SX1509 IO Expander@3.0.6
# renovate: datasource=custom.pio depName=APA102 packageName=pololu/library/APA102
+4 -2
View File
@@ -89,8 +89,10 @@ build_flags =
-D VIEW_240x320
-D DISPLAY_SET_RESOLUTION
-D DISPLAY_SIZE=240x320 ; portrait mode
-D LGFX_SPI_3WIRE=true
-D LGFX_PIN_SCK=17
-D LGFX_PIN_MOSI=33
-D LGFX_PIN_MISO=-1
-D LGFX_PIN_DC=16
-D LGFX_PIN_CS=15
-D LGFX_PIN_BL=21
@@ -123,7 +125,7 @@ build_flags =
-D SCREEN_TRANSITION_FRAMERATE=5
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
-D HAS_TOUCHSCREEN=1
-D TOUCH_I2C_PORT=0
-D TOUCH_I2C_PORT=1
-D TOUCH_SLAVE_ADDRESS=0x2E
-D SCREEN_TOUCH_INT=TOUCH_INT_PIN
-D SCREEN_TOUCH_RST=TOUCH_RST_PIN
@@ -131,6 +133,6 @@ build_flags =
lib_deps = ${heltec_v4_base.lib_deps}
${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
@@ -0,0 +1,56 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
#define USB_VID 0x303a
#define USB_PID 0x1001
static const uint8_t TX = 43;
static const uint8_t RX = 44;
static const uint8_t SDA = 17;
static const uint8_t SCL = 18;
static const uint8_t SS = 8;
static const uint8_t MOSI = 10;
static const uint8_t MISO = 11;
static const uint8_t SCK = 9;
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
static const uint8_t A6 = 7;
static const uint8_t A7 = 8;
static const uint8_t A8 = 9;
static const uint8_t A9 = 10;
static const uint8_t A10 = 11;
static const uint8_t A11 = 12;
static const uint8_t A12 = 13;
static const uint8_t A13 = 14;
static const uint8_t A14 = 15;
static const uint8_t A15 = 16;
static const uint8_t A16 = 17;
static const uint8_t A17 = 18;
static const uint8_t A18 = 19;
static const uint8_t A19 = 20;
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
static const uint8_t T7 = 7;
static const uint8_t T8 = 8;
static const uint8_t T9 = 9;
static const uint8_t T10 = 10;
static const uint8_t T11 = 11;
static const uint8_t T12 = 12;
static const uint8_t T13 = 13;
static const uint8_t T14 = 14;
#endif /* Pins_Arduino_h */
@@ -0,0 +1,145 @@
[heltec_v4_r8_base]
extends = esp32s3_base
board = heltec_v4_r8
board_check = true
board_build.partitions = default_16MB.csv
build_flags =
${esp32s3_base.build_flags}
-D HELTEC_V4_R8
-D HAS_LORA_FEM=1
-D BOARD_HAS_PSRAM
-I variants/esp32s3/heltec_v4_r8
-ULED_BUILTIN
[env:heltec-v4-r8-oled]
custom_meshtastic_hw_model = 132
custom_meshtastic_hw_model_slug = HELTEC_V4_R8
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = Heltec V4 R8
custom_meshtastic_images = heltec_v4_r8.svg
custom_meshtastic_tags = Heltec
custom_meshtastic_requires_dfu = true
custom_meshtastic_partition_scheme = 16MB
extends = heltec_v4_r8_base
build_flags =
${heltec_v4_r8_base.build_flags}
-D HELTEC_V4_R8_OLED
-D USE_SSD1306 ; Heltec_v4_R8 has an SSD1315 display (compatible with SSD1306 driver)
-D LED_POWER=46
-D RESET_OLED=21
-D I2C_SDA=17
-D I2C_SCL=18
[env:heltec-v4-r8-tft]
custom_meshtastic_hw_model = 132
custom_meshtastic_hw_model_slug = HELTEC_V4_R8
custom_meshtastic_architecture = esp32-s3
custom_meshtastic_actively_supported = true
custom_meshtastic_support_level = 1
custom_meshtastic_display_name = Heltec V4 R8 TFT
custom_meshtastic_images = heltec_v4_r8_tft.svg
custom_meshtastic_tags = Heltec
custom_meshtastic_requires_dfu = true
custom_meshtastic_partition_scheme = 16MB
extends = heltec_v4_r8_base
build_flags =
${heltec_v4_r8_base.build_flags} ;-Os
-D HELTEC_V4_R8_TFT
-D I2C_SDA=17
-D I2C_SCL=18
-D PIN_BUTTON2=46
-D ALT_BUTTON_PIN=PIN_BUTTON2
-D ALT_BUTTON_ACTIVE_LOW=false
-D PIN_BUZZER=4
-D USE_PIN_BUZZER=PIN_BUZZER
-D CONFIG_ARDUHAL_LOG_COLORS
-D RADIOLIB_DEBUG_SPI=0
-D RADIOLIB_DEBUG_PROTOCOL=0
-D RADIOLIB_DEBUG_BASIC=0
-D RADIOLIB_VERBOSE_ASSERT=0
-D RADIOLIB_SPI_PARANOID=0
-D CONFIG_DISABLE_HAL_LOCKS=1
-D INPUTDRIVER_BUTTON_TYPE=0
-D HAS_SCREEN=1
-D HAS_TFT=1
-D RAM_SIZE=5120
-D LV_LVGL_H_INCLUDE_SIMPLE
-D LV_CONF_INCLUDE_SIMPLE
-D LV_COMP_CONF_INCLUDE_SIMPLE
-D LV_USE_SYSMON=0
-D LV_USE_PROFILER=0
-D LV_USE_PERF_MONITOR=0
-D LV_USE_MEM_MONITOR=0
-D LV_USE_LOG=0
-D LV_BUILD_TEST=0
-D USE_LOG_DEBUG
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
-D USE_PACKET_API
-D LGFX_DRIVER=LGFX_HELTEC_V4_TFT
-D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_HELTEC_V4_TFT.h\"
-D VIEW_240x320
-D DISPLAY_SET_RESOLUTION
-D DISPLAY_SIZE=240x320 ; portrait mode
-D LGFX_SPI_3WIRE=false
-D LGFX_PIN_SCK=16
-D LGFX_PIN_MOSI=15
-D LGFX_PIN_MISO=45
-D LGFX_PIN_DC=48
-D LGFX_PIN_CS=47
-D LGFX_PIN_BL=44
-D LGFX_PIN_RST=21
-D CUSTOM_TOUCH_DRIVER
-D TOUCH_SDA_PIN=I2C_SDA
-D TOUCH_SCL_PIN=I2C_SCL
-D TOUCH_INT_PIN=-1
-D TOUCH_RST_PIN=-1
;base UI
-D TFT_CS=LGFX_PIN_CS
-D ST7789_CS=TFT_CS
-D ST7789_RS=LGFX_PIN_DC
-D ST7789_SDA=LGFX_PIN_MOSI
-D ST7789_SCK=LGFX_PIN_SCK
-D ST7789_RESET=LGFX_PIN_RST
-D ST7789_MISO=LGFX_PIN_MISO
-D ST7789_BUSY=-1
-D ST7789_BL=LGFX_PIN_BL
-D ST7789_SPI_HOST=SPI3_HOST
-D TFT_BL=ST7789_BL
-D SPI_FREQUENCY=75000000
-D SPI_READ_FREQUENCY=SPI_FREQUENCY
-D SPI_3_WIRE=false
-D TFT_HEIGHT=320
-D TFT_WIDTH=240
-D TFT_OFFSET_X=0
-D TFT_OFFSET_Y=0
-D TFT_OFFSET_ROTATION=0
-D SCREEN_ROTATE
-D SCREEN_TRANSITION_FRAMERATE=5
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
-D HAS_TOUCHSCREEN=1
-D TOUCH_I2C_PORT=0
-D TOUCH_SLAVE_ADDRESS=0x2E
-D SCREEN_TOUCH_INT=TOUCH_INT_PIN
-D SCREEN_TOUCH_RST=TOUCH_RST_PIN
; Have SPI interface SD card slot
-D HAS_SDCARD
-D SDCARD_USE_SPI1
-D SDCARD_USER_SPI_BEGIN
-D SPI_MOSI=LGFX_PIN_MOSI
-D SPI_SCK=LGFX_PIN_SCK
-D SPI_MISO=LGFX_PIN_MISO
-D SPI_CS=3
-D SDCARD_CS=SPI_CS
-D SD_SPI_FREQUENCY=SPI_FREQUENCY
lib_deps = ${heltec_v4_r8_base.lib_deps}
${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.21
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
https://github.com/Quency-D/chsc6x/archive/3b2b6cebf3177b3e2c33d06e07909b0b10159516.zip
+72
View File
@@ -0,0 +1,72 @@
#define VEXT_ENABLE 40 // active low, powers the oled display and the lora antenna boost
#define VEXT_ON_VALUE LOW
#define BUTTON_PIN 0
#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
#define ADC_CHANNEL ADC1_GPIO1_CHANNEL
#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider
#define ADC_MULTIPLIER 4.9 * 1.035
#define USE_SX1262
#define LORA_DIO0 -1 // a No connect on the SX1262 module
#define LORA_RESET 12
#define LORA_DIO1 14 // SX1262 IRQ
#define LORA_DIO2 13 // SX1262 BUSY
#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TCXO is enabled
#define LORA_SCK 9
#define LORA_MISO 11
#define LORA_MOSI 10
#define LORA_CS 8
#define SX126X_CS LORA_CS
#define SX126X_DIO1 LORA_DIO1
#define SX126X_BUSY LORA_DIO2
#define SX126X_RESET LORA_RESET
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
// Enable Traffic Management Module for Heltec V4
#ifndef HAS_TRAFFIC_MANAGEMENT
#define HAS_TRAFFIC_MANAGEMENT 1
#endif
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 2048
#endif
// ---- KCT8103L RF FRONT END CONFIGURATION ----
// The Heltec V4.3 uses a KCT8103L FEM chip with integrated PA and LNA
// RF path: SX1262 -> Pi attenuator -> KCT8103L PA -> Antenna
// Control logic (from KCT8103L datasheet):
// Transmit PA: CSD=1, CTX=1, CPS=1
// Receive LNA: CSD=1, CTX=0, CPS=X (21dB gain, 1.9dB NF)
// Receive bypass: CSD=1, CTX=1, CPS=0
// Shutdown: CSD=0, CTX=X, CPS=X
// Pin mapping:
// CPS (pin 5) -> SX1262 DIO2: TX/RX path select (automatic via SX126X_DIO2_AS_RF_SWITCH)
// CSD (pin 4) -> GPIO2: Chip enable (HIGH=on, LOW=shutdown)
// CTX (pin 6) -> GPIO5: Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA)
// VCC0/VCC1 -> Vfem via U3 LDO, controlled by GPIO7
// KCT8103L FEM: TX/RX path switching is handled by DIO2 -> CPS pin (via SX126X_DIO2_AS_RF_SWITCH)
#define USE_KCT8103L_PA
#define LORA_PA_POWER 7 // VFEM_Ctrl - KCT8103L LDO power enable
#define LORA_KCT8103L_PA_CSD 2 // CSD - KCT8103L chip enable (HIGH=on)
#define LORA_KCT8103L_PA_CTX 5 // CTX - Switch between Receive LNA Mode and Receive Bypass Mode. (HIGH=RX bypass, LOW=RX LNA)
#if HAS_TFT
#define USE_TFTDISPLAY 1
#endif
/*
* GPS pins
*/
#define GPS_L76K
#define PIN_GPS_EN (42)
#define GPS_EN_ACTIVE LOW
#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing
#define PIN_GPS_PPS (41)
// Seems to be missing on this new board
#define GPS_TX_PIN (38) // This is for bits going TOWARDS the CPU
#define GPS_RX_PIN (39) // This is for bits going TOWARDS the GPS
#define GPS_THREAD_INTERVAL 50
@@ -24,4 +24,4 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
@@ -22,4 +22,4 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
@@ -21,4 +21,4 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
+1 -1
View File
@@ -55,7 +55,7 @@ lib_deps =
${esp32s3_base.lib_deps}
${device-ui_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
[mesh_tab_xpt2046]
extends = mesh_tab_base
@@ -25,7 +25,7 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
build_src_filter =
${esp32s3_base.build_src_filter}
@@ -37,7 +37,7 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
[env:rak_wismesh_tap_v2-tft]
extends = env:rak_wismesh_tap_v2
+1 -1
View File
@@ -29,7 +29,7 @@ build_flags = ${esp32s3_base.build_flags}
lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
# renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix
https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
+1 -1
View File
@@ -22,7 +22,7 @@ build_flags = ${esp32s3_base.build_flags}
lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4
# renovate: datasource=custom.pio depName=Adafruit DRV2605 packageName=adafruit/library/Adafruit DRV2605 Library
+1 -1
View File
@@ -33,7 +33,7 @@ build_flags = ${esp32s3_base.build_flags}
lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
# renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix
https://github.com/meshtastic/ESP8266Audio/archive/343024632ee78d6216907b2353fc943a62422d80.zip
# renovate: datasource=custom.pio depName=ESP8266SAM packageName=earlephilhower/library/ESP8266SAM
+2 -2
View File
@@ -22,7 +22,7 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
[env:tracksenger-lcd]
custom_meshtastic_hw_model = 48
@@ -48,7 +48,7 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
[env:tracksenger-oled]
custom_meshtastic_hw_model = 48
+1 -1
View File
@@ -37,7 +37,7 @@ build_src_filter =
lib_deps = ${esp32s3_base.lib_deps}
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
# TODO renovate https://gitlab.com/hamishcunningham/unphonelibrary#meshtastic@9.0.0
https://gitlab.com/hamishcunningham/unphonelibrary/-/archive/meshtastic/unphonelibrary-meshtastic.zip
+21 -11
View File
@@ -2,7 +2,7 @@
[portduino_base]
platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
https://github.com/meshtastic/platform-native/archive/135b91e953db0b5f44d278f8ebd5b8d985fc03d8.zip
https://github.com/meshtastic/platform-native/archive/cab4b21d902973e43c938dab3cf4844ba02547ec.zip
framework = arduino
build_src_filter =
@@ -27,7 +27,7 @@ lib_deps =
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
rweather/Crypto@0.4.0
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
lovyan03/LovyanGFX@1.2.19
lovyan03/LovyanGFX@1.2.21
; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main
https://github.com/pine64/libch341-spi-userspace/archive/23c42319a69cffcb65868e3c72e6bed83974a393.zip
# renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
@@ -37,25 +37,35 @@ lib_deps =
# renovate: datasource=github-tags depName=Adafruit_BME680 packageName=adafruit/Adafruit_BME680
https://github.com/adafruit/Adafruit_BME680/archive/refs/tags/2.0.6.zip
build_flags =
; Cross-platform build flags shared between native (Linux) and native-macos builds.
; Anything Linux-only (libbluetooth, libgpiod, libi2c, /sys/class/gpio,
; PORTDUINO_LINUX_HARDWARE, glibc-style _FORTIFY_SOURCE) goes in `build_flags`
; instead. UDP multicast also lives there because the firmware's
; UdpMulticastHandler.h unconditionally `#include <WiFi.h>` on non-NRF52
; targets, which requires the framework's bundled WiFi shim that we
; lib_ignore on macOS.
build_flags_common =
${arduino_base.build_flags}
-D ARCH_PORTDUINO
-fPIC
-D_FORTIFY_SOURCE=2
-fstack-protector-all -Wstack-protector --param ssp-buffer-size=4
-Isrc/platform/portduino
-DRADIOLIB_EEPROM_UNSUPPORTED
-DPORTDUINO_LINUX_HARDWARE
-DHAS_UDP_MULTICAST=1
-lpthread
-lstdc++fs
-lbluetooth
-lgpiod
-lyaml-cpp
-li2c
-luv
-std=gnu17
-std=gnu++17
build_flags =
${portduino_base.build_flags_common}
-DHAS_UDP_MULTICAST=1
-D_FORTIFY_SOURCE=2
-fstack-protector-all -Wstack-protector --param ssp-buffer-size=4
-DPORTDUINO_LINUX_HARDWARE
-lstdc++fs
-lbluetooth
-lgpiod
-li2c
lib_ignore =
Adafruit NeoPixel
Adafruit ST7735 and ST7789 Library
+125
View File
@@ -117,3 +117,128 @@ build_flags = -lgcov --coverage -fprofile-abs-path -fsanitize=address ${env:nati
test_testing_command =
${platformio.build_dir}/${this.__env__}/meshtasticd
-s
; ---------------------------------------------------------------------------
; Native build for macOS (Darwin / arm64 + x86_64). Headless meshtasticd that
; runs in SimRadio mode (`-s`) or against real LoRa hardware via a CH341
; USB-SPI bridge. No BlueZ, libgpiod, or Linux I2C — those require Linux.
;
; Prerequisites (Homebrew):
; brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
; # Optional: enable the HTTP API (PiWebServer) on macOS:
; brew install ulfius
;
; The macOS-side patches now live upstream:
; * meshtastic/platform-native — `String.h`-shadow shim, `-Wno-enum-constexpr-conversion`,
; empty-variant-dir guard. Pulled via `portduino_base.platform` zip pin.
; * meshtastic/framework-portduino — LinuxHardwareI2C macOS stubs, AsyncUDP
; SOCK_NONBLOCK fallback, Common.h __APPLE__ guard, WiFiServer.cpp extern-C
; fix, package.json URL refresh. Pulled by platform-native at its pinned commit.
; This env therefore only carries the firmware-side build flags and src filter.
;
; Real LoRa hardware on macOS:
; The same lib_dep `pine64/libch341-spi-userspace` used on Linux works on
; macOS as-is — its `libusb_detach_kernel_driver()` call is `__linux__`-
; guarded, but on macOS the kernel doesn't bind a driver to a CH341A SPI
; bridge (PID 0x5512; bDeviceClass=0xff vendor-specific) by default, so
; no detach is needed. Apple's bundled CH34x driver targets the CH340
; *UART* variant (PID 0x7523) — different product. libusb opens the device
; and claims interface 0 directly via IOUSBHostInterface.
;
; To use, point `meshtasticd` at any of the existing `bin/config.d/lora-*.yaml`
; files that specify `spidev: ch341` — they're platform-agnostic. Example:
; pio run -e native-macos
; mkdir -p ~/.meshtasticd && cp bin/config-dist.yaml ~/.meshtasticd/config.yaml
; # Edit ~/.meshtasticd/config.yaml: ConfigDirectory: ./config.d/
; mkdir ~/.meshtasticd/config.d && cp bin/config.d/lora-meshstick-1262.yaml ~/.meshtasticd/config.d/
; cd ~/.meshtasticd && /path/to/firmware/.pio/build/native-macos/meshtasticd
;
; The MAC address auto-derives from the CH341's USB serial + product string
; (PortduinoGlue.cpp ~497-518); on Linux a BlueZ HCI socket is the fallback
; when that path isn't taken, but BlueZ is `__linux__`-guarded so the
; serial-derivation path is mandatory on macOS. Override with
; `MACAddress: AA:BB:CC:DD:EE:FF` in config.yaml's `General:` section if
; the device's serial isn't 8 hex chars.
;
; Diagnosing CH341 issues on macOS:
; ioreg -p IOUSB -l -w 0 | grep -B2 -A30 0x5512
; Children should be `IOUSBHostInterface`. If a vendor driver class
; (e.g. `com.wch.CH34xVCPDriver` from a third-party WCH installer)
; claims interface 0, libusb will fail with LIBUSB_ERROR_BUSY.
; Workaround: `sudo kmutil unload -b <bundleID>`.
; LIBUSB_DEBUG=4 .pio/build/native-macos/meshtasticd
; Verbose libusb trace — useful when claim_interface fails.
; ---------------------------------------------------------------------------
[env:native-macos]
extends = native_base
; Apple's ld doesn't accept GNU ld's `-Wl,-Map,<file>` syntax (inherited from
; the top-level platformio.ini). Strip it; the linker map isn't useful for
; the macOS dev loop anyway, and Apple ld's equivalent (`-Wl,-map,<file>`)
; uses different argument shape.
build_unflags = -Wl,-Map,"${platformio.build_dir}"/output.map
build_flags = ${portduino_base.build_flags_common}
-I variants/native/portduino
-I/opt/homebrew/include
-I/opt/homebrew/opt/argp-standalone/include
-I/opt/homebrew/opt/yaml-cpp/include
-L/opt/homebrew/lib
-L/opt/homebrew/opt/argp-standalone/lib
-L/opt/homebrew/opt/yaml-cpp/lib
-largp
-DPORTDUINO_DARWIN
; Headless build — variants/native/portduino/variant.h would otherwise
; default HAS_SCREEN to 1 and pull in screen-renderer source that uses
; VLA-with-initializer (a GNU/GCC extension Apple Clang rejects).
; MESHTASTIC_EXCLUDE_SCREEN gates the optional `screen->setHeading(...)`-
; style screen-driver hooks scattered through sensor sources.
-DHAS_SCREEN=0
-DMESHTASTIC_EXCLUDE_SCREEN=1
; openssl@3 is the keg-only Homebrew formula; --cflags is required so the
; compiler finds <openssl/*.h> in the Homebrew prefix (not just the linker).
!pkg-config --cflags --libs openssl --silence-errors || :
; PiWebServer (src/mesh/raspihttp/PiWebServer.cpp) auto-engages when ulfius
; headers are reachable via `#if __has_include(<ulfius.h>)`. The `|| :`
; tail keeps the build green when the user hasn't run `brew install ulfius`
; — they just don't get the HTTP API in that case.
!pkg-config --cflags --libs liborcania --silence-errors || :
!pkg-config --cflags --libs libyder --silence-errors || :
!pkg-config --cflags --libs libulfius --silence-errors || :
; src/input/Linux*.{cpp,h} drive evdev (`<linux/input.h>`) which doesn't exist
; on macOS. graphics/Panel_sdl.* and graphics/TFTDisplay.cpp pull LovyanGFX
; (which we lib_ignore on macOS for the <malloc.h> issue). Neither is needed
; for the headless build.
build_src_filter = ${native_base.build_src_filter}
-<input/LinuxInput.cpp>
-<input/LinuxInputImpl.cpp>
-<graphics/Panel_sdl.cpp>
-<graphics/TFTDisplay.cpp>
; LovyanGFX includes <malloc.h> (Linux-only) and is only needed by TFT
; variants — not relevant for the headless macOS build.
lib_ignore =
${portduino_base.lib_ignore}
LovyanGFX
; ---------------------------------------------------------------------------
; Same as [env:native-macos] but built with AddressSanitizer for catching
; use-after-free, leaks, and OOB access during local development. Headless
; (no SDL/X11/libinput) so it stays cheap to build. Mirrors the shape of
; [env:native-tft-debug] but without the TFT/X11 dependencies.
;
; pio run -e native-macos-debug
; .pio/build/native-macos-debug/meshtasticd -s
;
; ASan runtime tuning (set in the shell before launching):
; ASAN_OPTIONS=detect_leaks=1:halt_on_error=0:abort_on_error=1
; MallocStackLogging=1 # macOS: nicer stack traces in malloc reports
; ---------------------------------------------------------------------------
[env:native-macos-debug]
extends = native_base
build_type = debug
build_unflags = ${env:native-macos.build_unflags}
build_flags = ${env:native-macos.build_flags}
-O0
-g
-fsanitize=address
-fno-omit-frame-pointer
build_src_filter = ${env:native-macos.build_src_filter}
lib_ignore = ${env:native-macos.lib_ignore}