mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-04 03:18:10 +10:00
Compare commits
111
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b8d8938b9 | ||
|
|
0bb89644f9 | ||
|
|
fcef46f4b0 | ||
|
|
bb86cf4e81 | ||
|
|
6ea0d5ebba | ||
|
|
b2bda3b07a | ||
|
|
41f53177a1 | ||
|
|
2d761f6453 | ||
|
|
7cb071c780 | ||
|
|
b7a9555905 | ||
|
|
0240a00d09 | ||
|
|
c0fcf807c0 | ||
|
|
55f40ecdfd | ||
|
|
90744ee0b7 | ||
|
|
1eb860a3fc | ||
|
|
4ee9598107 | ||
|
|
7066abbb86 | ||
|
|
21cef8c2e5 | ||
|
|
aef13bef89 | ||
|
|
6d31915e57 | ||
|
|
aa89faa619 | ||
|
|
ee796c75c9 | ||
|
|
918577b5b5 | ||
|
|
c5a477f4c1 | ||
|
|
fc79cf3971 | ||
|
|
0004616878 | ||
|
|
a0a7209229 | ||
|
|
16f2ff8c9e | ||
|
|
894a2f43bd | ||
|
|
e470723811 | ||
|
|
13ee648e3d | ||
|
|
bec00b0a57 | ||
|
|
025630937c | ||
|
|
527e2dc7f7 | ||
|
|
095838f375 | ||
|
|
e66fcde7e3 | ||
|
|
dc478d2163 | ||
|
|
f85ed3e59e | ||
|
|
acb7ac6e3d | ||
|
|
91314c40f5 | ||
|
|
c6282d3a3c | ||
|
|
3f35f2c6d7 | ||
|
|
407ab7abda | ||
|
|
6847f232a7 | ||
|
|
e9cd77e758 | ||
|
|
f586824435 | ||
|
|
b0d7440bad | ||
|
|
8dc82c45b8 | ||
|
|
96538b492a | ||
|
|
023461cd92 | ||
|
|
41df7ef0bf | ||
|
|
e49dab1f08 | ||
|
|
74f7d9ce3c | ||
|
|
6c74052ec3 | ||
|
|
fb7d34afb9 | ||
|
|
814773f50e | ||
|
|
621aeb29b5 | ||
|
|
6f1d611c34 | ||
|
|
fc871f42e4 | ||
|
|
9da0ee9c51 | ||
|
|
76734f0067 | ||
|
|
fd65de142b | ||
|
|
5ae50b607d | ||
|
|
20fb1b2fc2 | ||
|
|
a30ee4748a | ||
|
|
7726863043 | ||
|
|
61bab08d9e | ||
|
|
da2cd98d98 | ||
|
|
979a1a1554 | ||
|
|
a9a655c8f3 | ||
|
|
e85ebc6859 | ||
|
|
c9fc1edb4f | ||
|
|
483bb33749 | ||
|
|
62f62b4e8c | ||
|
|
ad568021ba | ||
|
|
ff8102e753 | ||
|
|
3c7d5435fe | ||
|
|
dd1184f7af | ||
|
|
46c1230c39 | ||
|
|
fa8f0f0dd8 | ||
|
|
e22a22b325 | ||
|
|
35e4a877ee | ||
|
|
a6f6175535 | ||
|
|
3e5f95f8ed | ||
|
|
ca15aa86f2 | ||
|
|
a3dcad08d4 | ||
|
|
70caa76fa0 | ||
|
|
edb975d886 | ||
|
|
8d649e9a0b | ||
|
|
5c1e5f3dd6 | ||
|
|
f1ffd05a55 | ||
|
|
6bde1d5fbb | ||
|
|
bc311616a3 | ||
|
|
9fdeec173e | ||
|
|
b24a6676a6 | ||
|
|
a3f39de4b9 | ||
|
|
d235d3f933 | ||
|
|
67726f9e3a | ||
|
|
3cbc9c91cb | ||
|
|
5c2afbf8ce | ||
|
|
f1ca363efe | ||
|
|
814dc2db1b | ||
|
|
fbeabe29ed | ||
|
|
7235afec2f | ||
|
|
ef7036e9ed | ||
|
|
c997e3bb65 | ||
|
|
028f781ea5 | ||
|
|
86cdff463b | ||
|
|
778090a269 | ||
|
|
14e9cb0fc3 | ||
|
|
aa506ce4ab |
@@ -8,7 +8,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"installTools": true,
|
||||
"version": "3.14"
|
||||
"version": "3.13"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
|
||||
@@ -575,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
brew update
|
||||
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config
|
||||
brew install platformio yaml-cpp libuv openssl@3 libusb argp-standalone pkg-config ulfius
|
||||
|
||||
- name: Get release version string
|
||||
run: |
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.525
|
||||
- checkov@3.2.526
|
||||
- renovate@43.150.0
|
||||
- prettier@3.8.3
|
||||
- trufflehog@3.95.2
|
||||
|
||||
Vendored
+4
-4
@@ -1,10 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
"Jason2866.esp-decoder",
|
||||
"pioarduino.pioarduino-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
"platformio.platformio-ide"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -126,16 +126,17 @@ Sequence these; don't parallelize on the same port.
|
||||
|
||||
## 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
@@ -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 \
|
||||
|
||||
+8
-2
@@ -4,12 +4,18 @@
|
||||
# trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions
|
||||
|
||||
# Ensure the Alpine version is updated in both stages of the container!
|
||||
FROM python:3.14-alpine3.23 AS builder
|
||||
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 \
|
||||
|
||||
+1
-1
@@ -38,4 +38,4 @@ cp bin/device-install.* $OUTDIR/
|
||||
cp bin/device-update.* $OUTDIR/
|
||||
|
||||
echo "Copying manifest"
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true
|
||||
cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json
|
||||
|
||||
@@ -293,9 +293,12 @@ if ("HAS_TFT", 1) in env.get("CPPDEFINES", []):
|
||||
board_arch = infer_architecture(env.BoardConfig())
|
||||
should_skip_manifest = board_arch is None
|
||||
|
||||
# For host/native envs, avoid depending on 'buildprog' (some targets don't define it)
|
||||
mtjson_deps = [] if should_skip_manifest else ["buildprog"]
|
||||
if not should_skip_manifest and platform.name == "espressif32":
|
||||
# Most platforms can generate the manifest as part of the default 'buildprog' target.
|
||||
# Typically this passes success/failure properly.
|
||||
mtjson_deps = ["buildprog"]
|
||||
if platform.name == "espressif32":
|
||||
# On ESP32, we need to explicitly depend upon the binary to prevent fake-success upon failure.
|
||||
mtjson_deps = ["$BUILD_DIR/${PROGNAME}.bin"]
|
||||
# Build littlefs image as part of mtjson target
|
||||
# Equivalent to `pio run -t buildfs`
|
||||
target_lfs = env.DataToBin(
|
||||
@@ -309,7 +312,8 @@ if should_skip_manifest:
|
||||
|
||||
env.AddCustomTarget(
|
||||
name="mtjson",
|
||||
dependencies=mtjson_deps,
|
||||
# For host/native envs, avoid depending on 'buildprog' (some targets don't define it)
|
||||
dependencies=[],
|
||||
actions=[skip_manifest],
|
||||
title="Meshtastic Manifest (skipped)",
|
||||
description="mtjson generation is skipped for native environments",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"extra_flags": [
|
||||
"-D CDEBYTE_EORA_S3",
|
||||
"-D ARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-D ARDUINO_USB_MODE=0",
|
||||
"-D ARDUINO_USB_MODE=1",
|
||||
"-D ARDUINO_RUNNING_CORE=1",
|
||||
"-D ARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-D BOARD_HAS_PSRAM"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"build": {
|
||||
"core": "esp32",
|
||||
"extra_flags": ["-DBOARD_HAS_PSRAM"],
|
||||
"f_cpu": "360000000L",
|
||||
"f_flash": "80000000L",
|
||||
"f_psram": "200000000L",
|
||||
"flash_mode": "qio",
|
||||
"hwids": [["0x1A86", "0x7522"]],
|
||||
"mcu": "esp32p4",
|
||||
"chip_variant": "esp32p4",
|
||||
"variant": "esp32p4"
|
||||
},
|
||||
"arduino": {
|
||||
"partitions": "default_16MB.csv"
|
||||
},
|
||||
"connectivity": ["bluetooth", "openthread"],
|
||||
"debug": {
|
||||
"openocd_target": "esp32p4.cfg"
|
||||
},
|
||||
"frameworks": ["arduino", "espidf"],
|
||||
"name": "CrowPanel Advanced ESP32-P4 HMI AI Display",
|
||||
"upload": {
|
||||
"flash_size": "16MB",
|
||||
"maximum_ram_size": 512000,
|
||||
"maximum_size": 16777216,
|
||||
"require_upload_port": true,
|
||||
"speed": 1500000
|
||||
},
|
||||
"url": "https://www.elecrow.com/crowpanel-advanced-5inch-esp32-p4-hmi-ai-display-800x480-ips-touch-screen-with-wifi-6.html",
|
||||
"vendor": "Elecrow"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DHELTEC_WIRELESS_TRACKER",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"core": "esp32",
|
||||
"extra_flags": [
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=0"
|
||||
],
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DLILYGO_TBEAM_1W",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"extra_flags": [
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DLILYGO_TBEAM_S3_CORE",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"extra_flags": [
|
||||
"-DLILYGO_T3S3_V1",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1",
|
||||
"-DBOARD_HAS_PSRAM"
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"-DBOARD_HAS_PSRAM",
|
||||
"-DUNPHONE_SPIN=9",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||
"-DARDUINO_USB_MODE=0",
|
||||
"-DARDUINO_USB_MODE=1",
|
||||
"-DARDUINO_RUNNING_CORE=1",
|
||||
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x640000,
|
||||
app1, app, ota_1, 0x650000,0x640000,
|
||||
spiffs, data, spiffs, 0xc90000,0x360000,
|
||||
coredump, data, coredump,0xFF0000,0x10000,
|
||||
|
@@ -0,0 +1,7 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x330000,
|
||||
app1, app, ota_1, 0x340000,0x330000,
|
||||
spiffs, data, spiffs, 0x670000,0x180000,
|
||||
coredump, data, coredump,0x7F0000,0x10000,
|
||||
|
@@ -70,17 +70,6 @@ def esp32_create_combined_bin(source, target, env):
|
||||
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
|
||||
|
||||
esp32_kind = env.GetProjectOption("custom_esp32_kind")
|
||||
if esp32_kind == "esp32":
|
||||
# Free up some IRAM by removing auxiliary SPI flash chip drivers.
|
||||
# Wrapped stub symbols are defined in src/platform/esp32/iram-quirk.c.
|
||||
env.Append(
|
||||
LINKFLAGS=[
|
||||
"-Wl,--wrap=esp_flash_chip_gd",
|
||||
"-Wl,--wrap=esp_flash_chip_issi",
|
||||
"-Wl,--wrap=esp_flash_chip_winbond",
|
||||
]
|
||||
)
|
||||
else:
|
||||
# For newer ESP32 targets, using newlib nano works better.
|
||||
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
|
||||
# Enable Newlib Nano formatting to save space
|
||||
# ...but allow printf float support (compromise)
|
||||
env.Append(LINKFLAGS=["--specs=nano.specs", "-u", "_printf_float"])
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# trunk-ignore-all(ruff/F821)
|
||||
# trunk-ignore-all(flake8/F821): For SConstruct imports
|
||||
|
||||
# force linker response file instead of command line arguments
|
||||
|
||||
Import("env")
|
||||
|
||||
|
||||
def wrap_with_tempfile(command_key):
|
||||
command = env.get(command_key)
|
||||
if not command or not isinstance(command, str):
|
||||
return
|
||||
if "TEMPFILE(" in command:
|
||||
return
|
||||
env.Replace(**{command_key: "${TEMPFILE('%s')}" % command})
|
||||
|
||||
|
||||
# Force SCons to spill long commands into response files on this target.
|
||||
env.Replace(MAXLINELENGTH=8192)
|
||||
|
||||
for key in ("LINKCOM", "CXXLINKCOM", "SHLINKCOM", "SHCXXLINKCOM"):
|
||||
wrap_with_tempfile(key)
|
||||
+67
-9
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
+4
-4
@@ -2,7 +2,7 @@
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = tbeam
|
||||
default_envs = heltec-v3
|
||||
|
||||
extra_configs =
|
||||
variants/*/*.ini
|
||||
@@ -120,7 +120,7 @@ lib_deps =
|
||||
[radiolib_base]
|
||||
lib_deps =
|
||||
# renovate: datasource=github-tags depName=RadioLib packageName=jgromes/RadioLib
|
||||
https://github.com/jgromes/RadioLib/archive/refs/tags/7.6.0.zip
|
||||
https://github.com/jgromes/RadioLib/archive/afe72ae46a343e15e3cac7f26ac585c7f98bffe5.zip
|
||||
|
||||
[device-ui_base]
|
||||
lib_deps =
|
||||
@@ -170,8 +170,8 @@ lib_deps =
|
||||
https://github.com/EmotiBit/EmotiBit_MLX90632/archive/refs/tags/v1.0.8.zip
|
||||
# renovate: datasource=github-tags depName=Adafruit MLX90614 packageName=adafruit/Adafruit_MLX90614
|
||||
https://github.com/adafruit/Adafruit-MLX90614-Library/archive/refs/tags/2.1.6.zip
|
||||
# renovate: datasource=git-refs depName=INA3221 packageName=https://github.com/sgtwilko/INA3221 gitBranch=FixOverflow
|
||||
https://github.com/sgtwilko/INA3221/archive/bb03d7e9bfcc74fc798838a54f4f99738f29fc6a.zip
|
||||
# renovate: datasource=github-tags depName=INA3221_RT packageName=RobTillaart/INA3221_RT
|
||||
https://github.com/RobTillaart/INA3221_RT/archive/refs/tags/0.4.2.zip
|
||||
# renovate: datasource=github-tags depName=QMC5883L Compass packageName=mprograms/QMC5883LCompass
|
||||
https://github.com/mprograms/QMC5883LCompass/archive/refs/tags/v1.2.3.zip
|
||||
# renovate: datasource=github-tags depName=DFRobot_RTU packageName=dfrobot/DFRobot_RTU
|
||||
|
||||
+25
-1
@@ -14,4 +14,28 @@ const uint8_t FROMNUM_UUID_16[16u] = {0x53, 0x44, 0xe3, 0x47, 0x75, 0xaa, 0x70,
|
||||
const uint8_t LEGACY_LOGRADIO_UUID_16[16u] = {0xe2, 0xf2, 0x1e, 0xbe, 0xc5, 0x15, 0xcf, 0xaa,
|
||||
0x6b, 0x43, 0xfa, 0x78, 0x38, 0xd2, 0x6f, 0x6c};
|
||||
const uint8_t LOGRADIO_UUID_16[16u] = {0x47, 0x95, 0xDF, 0x8C, 0xDE, 0xE9, 0x44, 0x99,
|
||||
0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A};
|
||||
0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A};
|
||||
|
||||
void BluetoothApi::setup() {}
|
||||
void BluetoothApi::shutdown() {}
|
||||
void BluetoothApi::deinit() {}
|
||||
void BluetoothApi::clearBonds() {}
|
||||
bool BluetoothApi::isActive()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool BluetoothApi::isConnected()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void BluetoothApi::sendLog(const uint8_t *logMessage, size_t length)
|
||||
{
|
||||
(void)logMessage;
|
||||
(void)length;
|
||||
}
|
||||
|
||||
void updateBatteryLevel(uint8_t level) __attribute__((weak));
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
(void)level;
|
||||
}
|
||||
@@ -24,9 +24,14 @@ void updateBatteryLevel(uint8_t level);
|
||||
class BluetoothApi
|
||||
{
|
||||
public:
|
||||
virtual ~BluetoothApi() = default;
|
||||
|
||||
virtual void setup();
|
||||
virtual void shutdown();
|
||||
virtual void deinit();
|
||||
virtual void clearBonds();
|
||||
virtual bool isActive();
|
||||
virtual bool isConnected();
|
||||
virtual int getRssi() = 0;
|
||||
virtual void sendLog(const uint8_t *logMessage, size_t length);
|
||||
};
|
||||
@@ -151,9 +151,8 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
||||
#include <RAK13800_W5100S.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#if HAS_WIFI
|
||||
|
||||
+65
-95
@@ -79,28 +79,46 @@ bool copyFile(const char *from, const char *to)
|
||||
bool renameFile(const char *pathFrom, const char *pathTo)
|
||||
{
|
||||
#ifdef FSCom
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// take SPI Lock
|
||||
spiLock->lock();
|
||||
// rename was fixed for ESP32 IDF LittleFS in April
|
||||
bool result = FSCom.rename(pathFrom, pathTo);
|
||||
spiLock->unlock();
|
||||
return result;
|
||||
#else
|
||||
// copyFile does its own locking.
|
||||
if (copyFile(pathFrom, pathTo) && FSCom.remove(pathFrom)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @brief Platform-agnostic filesystem format / wipe.
|
||||
*
|
||||
* On embedded targets (ESP32, NRF52, STM32WL, RP2040) this calls the
|
||||
* native FSCom.format() which erases and reinitialises the LittleFS
|
||||
* partition.
|
||||
*
|
||||
* On Portduino the fs::FS backend has no format() method. We instead
|
||||
* delete /prefs (the only meshtastic data directory written at runtime)
|
||||
* and return. rmDir("/prefs") is already called unconditionally by
|
||||
* factoryReset() so this is a proven primitive on Portduino.
|
||||
* FSBegin() is a no-op (#define FSBegin() true) on Portduino.
|
||||
*
|
||||
* @return true on success, false on failure or if no filesystem is configured.
|
||||
*/
|
||||
bool fsFormat()
|
||||
{
|
||||
#ifdef FSCom
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
rmDir("/prefs");
|
||||
return FSBegin();
|
||||
#else
|
||||
return FSCom.format();
|
||||
#endif
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the list of files in a directory.
|
||||
*
|
||||
@@ -123,23 +141,21 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
||||
|
||||
File file = root.openNextFile();
|
||||
while (file) {
|
||||
#ifdef ARCH_ESP32
|
||||
const char *filepath = file.path();
|
||||
#else
|
||||
const char *filepath = file.name();
|
||||
#endif
|
||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||
if (levels) {
|
||||
#ifdef ARCH_ESP32
|
||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(file.path(), levels - 1);
|
||||
#else
|
||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(file.name(), levels - 1);
|
||||
#endif
|
||||
std::vector<meshtastic_FileInfo> subDirFilenames = getFiles(filepath, levels - 1);
|
||||
filenames.insert(filenames.end(), subDirFilenames.begin(), subDirFilenames.end());
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
meshtastic_FileInfo fileInfo = {"", static_cast<uint32_t>(file.size())};
|
||||
#ifdef ARCH_ESP32
|
||||
strcpy(fileInfo.file_name, file.path());
|
||||
#else
|
||||
strcpy(fileInfo.file_name, file.name());
|
||||
#endif
|
||||
strncpy(fileInfo.file_name, filepath, sizeof(fileInfo.file_name) - 1);
|
||||
fileInfo.file_name[sizeof(fileInfo.file_name) - 1] = '\0';
|
||||
if (!String(fileInfo.file_name).endsWith(".")) {
|
||||
filenames.push_back(fileInfo);
|
||||
}
|
||||
@@ -163,98 +179,59 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels)
|
||||
void listDir(const char *dirname, uint8_t levels, bool del)
|
||||
{
|
||||
#ifdef FSCom
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
char buffer[255];
|
||||
#endif
|
||||
File root = FSCom.open(dirname, FILE_O_READ);
|
||||
if (!root) {
|
||||
if (!root || !root.isDirectory())
|
||||
return;
|
||||
}
|
||||
if (!root.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File file = root.openNextFile();
|
||||
while (
|
||||
file &&
|
||||
file.name()[0]) { // This file.name() check is a workaround for a bug in the Adafruit LittleFS nrf52 glue (see issue 4395)
|
||||
while (file && file.name()[0]) { // file.name()[0] check: workaround for Adafruit LittleFS nRF52 bug #4395
|
||||
#ifdef ARCH_ESP32
|
||||
const char *filepath = file.path();
|
||||
#else
|
||||
const char *filepath = file.name();
|
||||
#endif
|
||||
if (file.isDirectory() && !String(file.name()).endsWith(".")) {
|
||||
if (levels) {
|
||||
#ifdef ARCH_ESP32
|
||||
listDir(file.path(), levels - 1, del);
|
||||
listDir(filepath, levels - 1, del);
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", file.path());
|
||||
strncpy(buffer, file.path(), sizeof(buffer));
|
||||
LOG_DEBUG("Remove %s", filepath);
|
||||
strncpy(buffer, filepath, sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
file.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
file.close();
|
||||
}
|
||||
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
listDir(file.name(), levels - 1, del);
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", file.name());
|
||||
strncpy(buffer, file.name(), sizeof(buffer));
|
||||
file.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
file.close();
|
||||
}
|
||||
#else
|
||||
LOG_DEBUG(" %s (directory)", file.name());
|
||||
listDir(file.name(), levels - 1, del);
|
||||
file.close();
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#ifdef ARCH_ESP32
|
||||
if (del) {
|
||||
LOG_DEBUG("Delete %s", file.path());
|
||||
strncpy(buffer, file.path(), sizeof(buffer));
|
||||
LOG_DEBUG("Delete %s", filepath);
|
||||
strncpy(buffer, filepath, sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
file.close();
|
||||
FSCom.remove(buffer);
|
||||
} else {
|
||||
LOG_DEBUG(" %s (%i Bytes)", file.path(), file.size());
|
||||
LOG_DEBUG(" %s (%i Bytes)", filepath, file.size());
|
||||
file.close();
|
||||
}
|
||||
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
if (del) {
|
||||
LOG_DEBUG("Delete %s", file.name());
|
||||
strncpy(buffer, file.name(), sizeof(buffer));
|
||||
file.close();
|
||||
FSCom.remove(buffer);
|
||||
} else {
|
||||
LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size());
|
||||
file.close();
|
||||
}
|
||||
#else
|
||||
LOG_DEBUG(" %s (%i Bytes)", file.name(), file.size());
|
||||
file.close();
|
||||
#endif
|
||||
}
|
||||
file = root.openNextFile();
|
||||
}
|
||||
#ifdef ARCH_ESP32
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", root.path());
|
||||
strncpy(buffer, root.path(), sizeof(buffer));
|
||||
root.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
root.close();
|
||||
}
|
||||
#elif (defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", root.name());
|
||||
strncpy(buffer, root.name(), sizeof(buffer));
|
||||
root.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
root.close();
|
||||
}
|
||||
const char *rootpath = root.path();
|
||||
#else
|
||||
root.close();
|
||||
const char *rootpath = root.name();
|
||||
#endif
|
||||
if (del) {
|
||||
LOG_DEBUG("Remove %s", rootpath);
|
||||
strncpy(buffer, rootpath, sizeof(buffer) - 1);
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
root.close();
|
||||
FSCom.rmdir(buffer);
|
||||
} else {
|
||||
root.close();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -268,14 +245,7 @@ void listDir(const char *dirname, uint8_t levels, bool del)
|
||||
void rmDir(const char *dirname)
|
||||
{
|
||||
#ifdef FSCom
|
||||
|
||||
#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
|
||||
listDir(dirname, 10, true);
|
||||
#elif defined(ARCH_NRF52)
|
||||
// nRF52 implementation of LittleFS has a recursive delete function
|
||||
FSCom.rmdir_r(dirname);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -307,7 +277,7 @@ void fsInit()
|
||||
*/
|
||||
void setupSDCard()
|
||||
{
|
||||
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI)
|
||||
#if defined(HAS_SDCARD) && !defined(SDCARD_USE_SOFT_SPI) && !defined(HAS_SD_MMC)
|
||||
concurrency::LockGuard g(spiLock);
|
||||
SDHandler.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
|
||||
if (!SD.begin(SDCARD_CS, SDHandler, SD_SPI_FREQUENCY)) {
|
||||
|
||||
@@ -52,6 +52,7 @@ void fsInit();
|
||||
void fsListFiles();
|
||||
bool copyFile(const char *from, const char *to);
|
||||
bool renameFile(const char *pathFrom, const char *pathTo);
|
||||
bool fsFormat();
|
||||
std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels);
|
||||
void listDir(const char *dirname, uint8_t levels, bool del = false);
|
||||
void rmDir(const char *dirname);
|
||||
|
||||
+126
-83
@@ -24,6 +24,13 @@
|
||||
#include "meshUtils.h"
|
||||
#include "power/PowerHAL.h"
|
||||
#include "sleep.h"
|
||||
#ifdef ARCH_ESP32
|
||||
// #include <driver/adc.h>
|
||||
#include <esp_adc/adc_cali.h>
|
||||
#include <esp_adc/adc_cali_scheme.h>
|
||||
#include <esp_adc/adc_oneshot.h>
|
||||
#include <esp_err.h>
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
#include "api/WiFiServerAPI.h"
|
||||
@@ -63,9 +70,8 @@
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#endif
|
||||
@@ -77,21 +83,86 @@
|
||||
#if defined(BATTERY_PIN) && defined(ARCH_ESP32)
|
||||
|
||||
#ifndef BAT_MEASURE_ADC_UNIT // ADC1 is default
|
||||
static const adc1_channel_t adc_channel = ADC_CHANNEL;
|
||||
static const adc_channel_t adc_channel = ADC_CHANNEL;
|
||||
static const adc_unit_t unit = ADC_UNIT_1;
|
||||
#else // ADC2
|
||||
static const adc2_channel_t adc_channel = ADC_CHANNEL;
|
||||
#else // ADC2
|
||||
static const adc_channel_t adc_channel = ADC_CHANNEL;
|
||||
static const adc_unit_t unit = ADC_UNIT_2;
|
||||
RTC_NOINIT_ATTR uint64_t RTC_reg_b;
|
||||
|
||||
#endif // BAT_MEASURE_ADC_UNIT
|
||||
|
||||
esp_adc_cal_characteristics_t *adc_characs = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t));
|
||||
static adc_oneshot_unit_handle_t adc_handle = nullptr;
|
||||
static adc_cali_handle_t adc_cali_handle = nullptr;
|
||||
static bool adc_calibrated = false;
|
||||
#ifndef ADC_ATTENUATION
|
||||
static const adc_atten_t atten = ADC_ATTEN_DB_12;
|
||||
#else
|
||||
static const adc_atten_t atten = ADC_ATTENUATION;
|
||||
#endif
|
||||
#ifdef ADC_BITWIDTH
|
||||
static const adc_bitwidth_t adc_width = ADC_BITWIDTH;
|
||||
#else
|
||||
static const adc_bitwidth_t adc_width = ADC_BITWIDTH_DEFAULT;
|
||||
#endif
|
||||
|
||||
static int adcBitWidthToBits(adc_bitwidth_t width)
|
||||
{
|
||||
switch (width) {
|
||||
case ADC_BITWIDTH_9:
|
||||
return 9;
|
||||
case ADC_BITWIDTH_10:
|
||||
return 10;
|
||||
case ADC_BITWIDTH_11:
|
||||
return 11;
|
||||
case ADC_BITWIDTH_12:
|
||||
return 12;
|
||||
#ifdef ADC_BITWIDTH_13
|
||||
case ADC_BITWIDTH_13:
|
||||
return 13;
|
||||
#endif
|
||||
default:
|
||||
return 12;
|
||||
}
|
||||
}
|
||||
|
||||
static bool initAdcCalibration()
|
||||
{
|
||||
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
|
||||
adc_cali_curve_fitting_config_t cali_config = {
|
||||
.unit_id = unit,
|
||||
.atten = atten,
|
||||
.bitwidth = adc_width,
|
||||
};
|
||||
esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle);
|
||||
if (ret == ESP_OK) {
|
||||
LOG_INFO("ADC calibration: curve fitting enabled");
|
||||
return true;
|
||||
}
|
||||
if (ret != ESP_ERR_NOT_SUPPORTED) {
|
||||
LOG_WARN("ADC calibration: curve fitting failed: %s", esp_err_to_name(ret));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
|
||||
adc_cali_line_fitting_config_t cali_config = {
|
||||
.unit_id = unit,
|
||||
.atten = atten,
|
||||
.bitwidth = adc_width,
|
||||
.default_vref = DEFAULT_VREF,
|
||||
};
|
||||
esp_err_t ret = adc_cali_create_scheme_line_fitting(&cali_config, &adc_cali_handle);
|
||||
if (ret == ESP_OK) {
|
||||
LOG_INFO("ADC calibration: line fitting enabled");
|
||||
return true;
|
||||
}
|
||||
if (ret != ESP_ERR_NOT_SUPPORTED) {
|
||||
LOG_WARN("ADC calibration: line fitting failed: %s", esp_err_to_name(ret));
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INFO("ADC calibration not supported; using approximate scaling");
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif // BATTERY_PIN && ARCH_ESP32
|
||||
|
||||
#ifdef EXT_PWR_DETECT
|
||||
@@ -367,8 +438,20 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
scaled *= operativeAdcMultiplier;
|
||||
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
|
||||
raw = espAdcRead();
|
||||
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
|
||||
scaled *= operativeAdcMultiplier;
|
||||
int voltage_mv = 0;
|
||||
if (adc_calibrated && adc_cali_handle) {
|
||||
if (adc_cali_raw_to_voltage(adc_cali_handle, raw, &voltage_mv) != ESP_OK) {
|
||||
LOG_WARN("ADC calibration read failed; using raw value");
|
||||
voltage_mv = 0;
|
||||
}
|
||||
}
|
||||
if (voltage_mv == 0) {
|
||||
// Fallback approximate conversion without calibration
|
||||
const int bits = adcBitWidthToBits(adc_width);
|
||||
const float max_code = powf(2.0f, bits) - 1.0f;
|
||||
voltage_mv = (int)((raw / max_code) * DEFAULT_VREF);
|
||||
}
|
||||
scaled = voltage_mv * operativeAdcMultiplier;
|
||||
#else // block for all other platforms
|
||||
#ifdef ARCH_NRF52
|
||||
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
|
||||
@@ -410,51 +493,22 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
uint32_t raw = 0;
|
||||
uint8_t raw_c = 0; // raw reading counter
|
||||
|
||||
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
|
||||
if (!adc_handle) {
|
||||
LOG_ERROR("ADC oneshot handle not initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
|
||||
int val_ = adc1_get_raw(adc_channel);
|
||||
if (val_ >= 0) { // save only valid readings
|
||||
raw += val_;
|
||||
int val = 0;
|
||||
esp_err_t err = adc_oneshot_read(adc_handle, adc_channel, &val);
|
||||
if (err == ESP_OK) {
|
||||
raw += val;
|
||||
raw_c++;
|
||||
}
|
||||
// delayMicroseconds(100);
|
||||
}
|
||||
#else // ADC2
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3 // ESP32S3
|
||||
// ADC2 wifi bug workaround not required, breaks compile
|
||||
// On ESP32S3, ADC2 can take turns with Wifi (?)
|
||||
|
||||
int32_t adc_buf;
|
||||
esp_err_t read_result;
|
||||
|
||||
// Multiple samples
|
||||
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
|
||||
adc_buf = 0;
|
||||
read_result = -1;
|
||||
|
||||
read_result = adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf);
|
||||
if (read_result == ESP_OK) {
|
||||
raw += adc_buf;
|
||||
raw_c++; // Count valid samples
|
||||
} else {
|
||||
LOG_DEBUG("An attempt to sample ADC2 failed");
|
||||
LOG_DEBUG("ADC read failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
#else // Other ESP32
|
||||
int32_t adc_buf = 0;
|
||||
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
|
||||
// ADC2 wifi bug workaround, see
|
||||
// https://github.com/espressif/arduino-esp32/issues/102
|
||||
WRITE_PERI_REG(SENS_SAR_READ_CTRL2_REG, RTC_reg_b);
|
||||
SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV);
|
||||
adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf);
|
||||
raw += adc_buf;
|
||||
raw_c++;
|
||||
}
|
||||
#endif // BAT_MEASURE_ADC_UNIT
|
||||
|
||||
#endif // End BAT_MEASURE_ADC_UNIT
|
||||
return (raw / (raw_c < 1 ? 1 : raw_c));
|
||||
}
|
||||
#endif
|
||||
@@ -666,42 +720,31 @@ bool Power::analogInit()
|
||||
#ifdef ARCH_STM32WL
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
|
||||
adc_oneshot_unit_init_cfg_t init_config = {
|
||||
.unit_id = unit,
|
||||
};
|
||||
|
||||
#ifndef ADC_WIDTH // max resolution by default
|
||||
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
|
||||
#else
|
||||
static const adc_bits_width_t width = ADC_WIDTH;
|
||||
#endif
|
||||
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
|
||||
adc1_config_width(width);
|
||||
adc1_config_channel_atten(adc_channel, atten);
|
||||
#else // ADC2
|
||||
adc2_config_channel_atten(adc_channel, atten);
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32S3
|
||||
// ADC2 wifi bug workaround
|
||||
// Not required with ESP32S3, breaks compile
|
||||
RTC_reg_b = READ_PERI_REG(SENS_SAR_READ_CTRL2_REG);
|
||||
#endif
|
||||
#endif
|
||||
// calibrate ADC
|
||||
esp_adc_cal_value_t val_type = esp_adc_cal_characterize(unit, atten, width, DEFAULT_VREF, adc_characs);
|
||||
// show ADC characterization base
|
||||
if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) {
|
||||
LOG_INFO("ADC config based on Two Point values stored in eFuse");
|
||||
} else if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
|
||||
LOG_INFO("ADC config based on reference voltage stored in eFuse");
|
||||
if (!adc_handle) {
|
||||
esp_err_t err = adc_oneshot_new_unit(&init_config, &adc_handle);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ADC oneshot init failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
// ESP32S3
|
||||
else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP_FIT) {
|
||||
LOG_INFO("ADC config based on Two Point values and fitting curve "
|
||||
"coefficients stored in eFuse");
|
||||
|
||||
adc_oneshot_chan_cfg_t chan_cfg = {
|
||||
.atten = atten,
|
||||
.bitwidth = adc_width,
|
||||
};
|
||||
|
||||
esp_err_t err = adc_oneshot_config_channel(adc_handle, adc_channel, &chan_cfg);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ADC channel config failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
LOG_INFO("ADC config based on default reference voltage");
|
||||
}
|
||||
#endif // ARCH_ESP32
|
||||
|
||||
adc_calibrated = initAdcCalibration();
|
||||
#endif // ARCH_ESP32
|
||||
|
||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) {
|
||||
bool isBleConnected = false;
|
||||
#ifdef ARCH_ESP32
|
||||
isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected();
|
||||
isBleConnected = bluetoothApi && bluetoothApi->isActive() && bluetoothApi->isConnected();
|
||||
#elif defined(ARCH_NRF52)
|
||||
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
|
||||
#endif
|
||||
@@ -238,7 +238,7 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
|
||||
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
|
||||
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
|
||||
#ifdef ARCH_ESP32
|
||||
nimbleBluetooth->sendLog(buffer.get(), size);
|
||||
bluetoothApi->sendLog(buffer.get(), size);
|
||||
#elif defined(ARCH_NRF52)
|
||||
nrf52Bluetooth->sendLog(buffer.get(), size);
|
||||
#endif
|
||||
|
||||
@@ -7,10 +7,6 @@ static File openFile(const char *filename, bool fullAtomic)
|
||||
{
|
||||
concurrency::LockGuard g(spiLock);
|
||||
LOG_DEBUG("Opening %s, fullAtomic=%d", filename, fullAtomic);
|
||||
#ifdef ARCH_NRF52
|
||||
FSCom.remove(filename);
|
||||
return FSCom.open(filename, FILE_O_WRITE);
|
||||
#endif
|
||||
if (!fullAtomic) {
|
||||
FSCom.remove(filename); // Nuke the old file to make space (ignore if it !exists)
|
||||
}
|
||||
@@ -67,9 +63,6 @@ bool SafeFile::close()
|
||||
f.close();
|
||||
spiLock->unlock();
|
||||
|
||||
#ifdef ARCH_NRF52
|
||||
return true;
|
||||
#endif
|
||||
if (!testReadback())
|
||||
return false;
|
||||
|
||||
|
||||
@@ -0,0 +1,762 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4) && defined(CONFIG_ESP_HOSTED_ENABLED) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#include "BluetoothStatus.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "bluetooth/HostedBluetooth.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "esp32-hal-hosted.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_hosted.h"
|
||||
#include "esp_hosted_event.h"
|
||||
#include "main.h"
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include <BLEAdvertising.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLESecurity.h>
|
||||
#include <BLEServer.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
#include <driver/gpio.h>
|
||||
#include <mutex>
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "host/ble_store.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/*
|
||||
* Maintainer note: HostedBluetooth intentionally stays close to NimbleBluetooth
|
||||
* but is not a strict drop-in equivalent.
|
||||
*
|
||||
* Intentional differences from NimbleBluetooth include:
|
||||
* - ESP-Hosted transport lifecycle handling (event callbacks + CP reset GPIO control).
|
||||
* - Data-length update behavior (Hosted currently requests ble_gap_set_data_len on connect).
|
||||
* - No Battery Service exposure here.
|
||||
*
|
||||
* If you modify common BLE flow in one class, review and likely mirror in both:
|
||||
* - PhoneAPI queueing/synchronization between BLE callbacks and runOnce().
|
||||
* - Security/pairing config and passkey UX/status updates.
|
||||
* - Mesh GATT characteristics, permissions, and advertising setup.
|
||||
* - Connection parameter tuning and reconnect/disconnect cleanup paths.
|
||||
*/
|
||||
constexpr uint16_t kPreferredBleMtu = 517;
|
||||
constexpr uint16_t kPreferredBleTxOctets = 251;
|
||||
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
constexpr size_t kBluetoothToPhoneQueueSize = 3;
|
||||
constexpr size_t kBluetoothFromPhoneQueueSize = 3;
|
||||
|
||||
BLECharacteristic *fromNumCharacteristic = nullptr;
|
||||
BLECharacteristic *logRadioCharacteristic = nullptr;
|
||||
BLEServer *bleServer = nullptr;
|
||||
|
||||
static bool passkeyShowing = false;
|
||||
std::atomic<uint16_t> hostedConnHandle{BLE_HS_CONN_HANDLE_NONE};
|
||||
|
||||
void clearPairingDisplay()
|
||||
{
|
||||
if (!passkeyShowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
passkeyShowing = false;
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->endAlert();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class HostedBluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
HostedBluetoothPhoneAPI() : concurrency::OSThread("HostedBluetooth") { api_type = TYPE_BLE; }
|
||||
|
||||
std::mutex fromPhoneMutex;
|
||||
std::atomic<size_t> fromPhoneQueueSize{0};
|
||||
std::array<BLEValue, kBluetoothFromPhoneQueueSize> fromPhoneQueue{};
|
||||
|
||||
std::mutex toPhoneMutex;
|
||||
std::atomic<size_t> toPhoneQueueSize{0};
|
||||
std::array<std::array<uint8_t, meshtastic_FromRadio_size>, kBluetoothToPhoneQueueSize> toPhoneQueue{};
|
||||
std::array<size_t, kBluetoothToPhoneQueueSize> toPhoneQueueByteSizes{};
|
||||
std::atomic<bool> onReadCallbackIsWaitingForData{false};
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override
|
||||
{
|
||||
while (fromPhoneQueueSize > 0 || onReadCallbackIsWaitingForData) {
|
||||
runOnceHandleFromPhoneQueue();
|
||||
runOnceHandleToPhoneQueue();
|
||||
}
|
||||
return INT32_MAX;
|
||||
}
|
||||
|
||||
void onNowHasData(uint32_t fromRadioNum) override
|
||||
{
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
|
||||
if (!fromNumCharacteristic || !bleServer || bleServer->getConnectedCount() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t val[4];
|
||||
put_le32(val, fromRadioNum);
|
||||
fromNumCharacteristic->setValue(val, sizeof(val));
|
||||
fromNumCharacteristic->notify();
|
||||
}
|
||||
|
||||
bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||
|
||||
private:
|
||||
void runOnceHandleToPhoneQueue()
|
||||
{
|
||||
if (!onReadCallbackIsWaitingForData) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
|
||||
size_t numBytes = getFromRadio(fromRadioBytes);
|
||||
|
||||
if (numBytes > 0 && toPhoneQueueSize < kBluetoothToPhoneQueueSize) {
|
||||
std::lock_guard<std::mutex> guard(toPhoneMutex);
|
||||
const size_t storeAtIndex = toPhoneQueueSize.load();
|
||||
memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes);
|
||||
toPhoneQueueByteSizes[storeAtIndex] = numBytes;
|
||||
toPhoneQueueSize++;
|
||||
}
|
||||
|
||||
onReadCallbackIsWaitingForData = false;
|
||||
}
|
||||
|
||||
void runOnceHandleFromPhoneQueue()
|
||||
{
|
||||
if (fromPhoneQueueSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
BLEValue val;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(fromPhoneMutex);
|
||||
val = fromPhoneQueue[0];
|
||||
for (size_t i = 1; i < fromPhoneQueueSize; ++i) {
|
||||
fromPhoneQueue[i - 1] = fromPhoneQueue[i];
|
||||
}
|
||||
fromPhoneQueueSize--;
|
||||
}
|
||||
|
||||
handleToRadio(val.getData(), val.getLength());
|
||||
}
|
||||
};
|
||||
|
||||
static HostedBluetoothPhoneAPI *bluetoothPhoneAPI = nullptr;
|
||||
uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE] = {0};
|
||||
|
||||
class HostedBluetoothToRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
public:
|
||||
void onWrite(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
if (!bluetoothPhoneAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
BLEValue val;
|
||||
val.setValue(pCharacteristic->getData(), pCharacteristic->getLength());
|
||||
|
||||
if (memcmp(lastToRadio, val.getData(), val.getLength()) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothPhoneAPI->fromPhoneQueueSize >= kBluetoothFromPhoneQueueSize) {
|
||||
LOG_WARN("Hosted BLE onWrite drop: queue full (%u bytes)", val.getLength());
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(lastToRadio, val.getData(), val.getLength());
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
|
||||
bluetoothPhoneAPI->fromPhoneQueueSize++;
|
||||
}
|
||||
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
concurrency::mainDelay.interrupt();
|
||||
}
|
||||
};
|
||||
|
||||
class HostedBluetoothFromRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
public:
|
||||
void onRead(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
if (!bluetoothPhoneAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothPhoneAPI->toPhoneQueueSize == 0) {
|
||||
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true;
|
||||
bluetoothPhoneAPI->setIntervalFromNow(0);
|
||||
concurrency::mainDelay.interrupt();
|
||||
|
||||
int tries = 0;
|
||||
while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) {
|
||||
delay(tries < 20 ? 1 : 5);
|
||||
tries++;
|
||||
if (tries == 4000) {
|
||||
LOG_WARN("BLE onRead: timeout waiting for data after %d tries, giving up and returning 0-size response",
|
||||
tries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
|
||||
size_t numBytes = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
|
||||
size_t pending = bluetoothPhoneAPI->toPhoneQueueSize.load();
|
||||
if (pending > 0) {
|
||||
numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0];
|
||||
memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), numBytes);
|
||||
|
||||
for (size_t i = 1; i < pending; ++i) {
|
||||
memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(),
|
||||
bluetoothPhoneAPI->toPhoneQueueByteSizes[i]);
|
||||
bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i];
|
||||
}
|
||||
|
||||
bluetoothPhoneAPI->toPhoneQueueSize--;
|
||||
}
|
||||
}
|
||||
|
||||
pCharacteristic->setValue(fromRadioBytes, numBytes);
|
||||
}
|
||||
};
|
||||
|
||||
class HostedBluetoothServerCallback : public BLEServerCallbacks
|
||||
{
|
||||
public:
|
||||
explicit HostedBluetoothServerCallback(HostedBluetooth *owner) : owner(owner) {}
|
||||
|
||||
private:
|
||||
HostedBluetooth *owner;
|
||||
|
||||
void onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) override
|
||||
{
|
||||
BLEAddress peerAddr(desc->peer_id_addr);
|
||||
LOG_INFO("Hosted BLE incoming connection %s", peerAddr.toString().c_str());
|
||||
owner->setConnected(true);
|
||||
hostedConnHandle = desc->conn_handle;
|
||||
|
||||
const int dataLenResult = ble_gap_set_data_len(desc->conn_handle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
|
||||
if (dataLenResult != 0) {
|
||||
LOG_WARN("Hosted BLE failed to raise data length rc=%d", dataLenResult);
|
||||
}
|
||||
|
||||
pServer->updateConnParams(desc->conn_handle, 6, 12, 0, 200);
|
||||
}
|
||||
|
||||
void onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) override
|
||||
{
|
||||
(void)pServer;
|
||||
(void)desc;
|
||||
LOG_INFO("Hosted BLE disconnected");
|
||||
owner->setConnected(false);
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
clearPairingDisplay();
|
||||
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
|
||||
if (bluetoothPhoneAPI) {
|
||||
bluetoothPhoneAPI->close();
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueueSize = 0;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
|
||||
bluetoothPhoneAPI->toPhoneQueueSize = 0;
|
||||
}
|
||||
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false;
|
||||
}
|
||||
|
||||
owner->startAdvertising();
|
||||
}
|
||||
};
|
||||
|
||||
class HostedBluetoothSecurityCallback : public BLESecurityCallbacks
|
||||
{
|
||||
public:
|
||||
void onPassKeyNotify(uint32_t passkey) override
|
||||
{
|
||||
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey);
|
||||
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
snprintf(btPIN, sizeof(btPIN), "%06u", passkey);
|
||||
int x_offset = display->width() / 2;
|
||||
int y_offset = display->height() <= 80 ? 0 : 12;
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
display->setFont(FONT_SMALL);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, "Enter this code");
|
||||
#endif
|
||||
display->setFont(FONT_LARGE);
|
||||
char pin[8];
|
||||
snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3);
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, pin);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
char deviceName[64];
|
||||
snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName());
|
||||
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
|
||||
display->drawString(x_offset + x, y_offset + y, deviceName);
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
passkeyShowing = true;
|
||||
}
|
||||
|
||||
void onAuthenticationComplete(ble_gap_conn_desc *desc) override
|
||||
{
|
||||
(void)desc;
|
||||
LOG_INFO("Hosted BLE authentication complete");
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
clearPairingDisplay();
|
||||
}
|
||||
};
|
||||
|
||||
HostedBluetoothToRadioCallback *toRadioCallbacks = nullptr;
|
||||
HostedBluetoothFromRadioCallback *fromRadioCallbacks = nullptr;
|
||||
HostedBluetoothSecurityCallback *securityCallbacks = nullptr;
|
||||
|
||||
gpio_num_t getSlaveResetGpio()
|
||||
{
|
||||
#if defined(CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE)
|
||||
return static_cast<gpio_num_t>(CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE);
|
||||
#elif defined(CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE)
|
||||
return static_cast<gpio_num_t>(CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE);
|
||||
#else
|
||||
return GPIO_NUM_NC;
|
||||
#endif
|
||||
}
|
||||
|
||||
void setSlaveResetLine(bool assertReset)
|
||||
{
|
||||
const gpio_num_t gpioNum = getSlaveResetGpio();
|
||||
if (gpioNum == GPIO_NUM_NC || gpioNum < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gpio_reset_pin(gpioNum) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
if (gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT) != ESP_OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int activeLevel =
|
||||
#if defined(CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH)
|
||||
1;
|
||||
#else
|
||||
0;
|
||||
#endif
|
||||
const int inactiveLevel = activeLevel ? 0 : 1;
|
||||
|
||||
gpio_set_level(gpioNum, assertReset ? activeLevel : inactiveLevel);
|
||||
LOG_DEBUG("[HostedBluetooth] setSlaveResetLine: GPIO[%d] -> %d (assertReset=%d, activeLevel=%d)", gpioNum,
|
||||
assertReset ? activeLevel : inactiveLevel, assertReset, activeLevel);
|
||||
}
|
||||
|
||||
void hostedEventHandler(void *arg, esp_event_base_t eventBase, int32_t eventId, void *eventData)
|
||||
{
|
||||
(void)eventData;
|
||||
|
||||
auto *self = static_cast<HostedBluetooth *>(arg);
|
||||
if (!self || eventBase != ESP_HOSTED_EVENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventId) {
|
||||
case ESP_HOSTED_EVENT_TRANSPORT_UP:
|
||||
LOG_INFO("ESP-Hosted transport is up");
|
||||
self->setConnected(true);
|
||||
break;
|
||||
case ESP_HOSTED_EVENT_TRANSPORT_DOWN:
|
||||
LOG_WARN("ESP-Hosted transport is down");
|
||||
[[fallthrough]];
|
||||
case ESP_HOSTED_EVENT_TRANSPORT_FAILURE:
|
||||
if (eventId == ESP_HOSTED_EVENT_TRANSPORT_FAILURE) {
|
||||
LOG_ERROR("ESP-Hosted transport failure");
|
||||
}
|
||||
self->setConnected(false);
|
||||
break;
|
||||
case ESP_HOSTED_EVENT_CP_INIT:
|
||||
case ESP_HOSTED_EVENT_CP_HEARTBEAT:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
HostedBluetooth::HostedBluetooth() {}
|
||||
|
||||
HostedBluetooth::~HostedBluetooth()
|
||||
{
|
||||
deinit();
|
||||
}
|
||||
|
||||
bool HostedBluetooth::registerCallbacks()
|
||||
{
|
||||
if (callbacksRegistered) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Defensive cleanup in case setup() is called again after an incomplete/early previous init.
|
||||
// esp_event_handler_unregister can safely fail if the handler wasn't present.
|
||||
esp_event_handler_unregister(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler);
|
||||
|
||||
esp_err_t err = esp_event_handler_register(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler, this);
|
||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||
LOG_ERROR("Failed to register hosted event handler: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
LOG_WARN("Hosted event handler already registered, continuing");
|
||||
}
|
||||
|
||||
callbacksRegistered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostedBluetooth::unregisterCallbacks()
|
||||
{
|
||||
if (!callbacksRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
esp_event_handler_unregister(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler);
|
||||
|
||||
callbacksRegistered = false;
|
||||
}
|
||||
|
||||
void HostedBluetooth::setup()
|
||||
{
|
||||
if (active) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure co-processor is released from reset before bringing transport back up.
|
||||
setSlaveResetLine(false);
|
||||
LOG_DEBUG("[HostedBluetooth] setup(): Released co-processor from reset");
|
||||
|
||||
if (!registerCallbacks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
active = setupGatt();
|
||||
firstRssiLogged.store(false);
|
||||
if (active) {
|
||||
LOG_INFO("ESP-Hosted Bluetooth ready");
|
||||
} else {
|
||||
LOG_ERROR("Hosted BLE setup failed in hosted mode");
|
||||
deinit();
|
||||
}
|
||||
}
|
||||
|
||||
void HostedBluetooth::shutdown()
|
||||
{
|
||||
deinit();
|
||||
}
|
||||
|
||||
void HostedBluetooth::deinit()
|
||||
{
|
||||
if (!active && !callbacksRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
shutdownGatt();
|
||||
if (BLEDevice::getInitialized()) {
|
||||
BLEDevice::deinit(false);
|
||||
} else {
|
||||
hostedDeinitBLE();
|
||||
}
|
||||
|
||||
// Hold co-processor in reset when hosted transport is disabled to reduce idle draw.
|
||||
setSlaveResetLine(true);
|
||||
|
||||
unregisterCallbacks();
|
||||
|
||||
connected.store(false);
|
||||
active = false;
|
||||
rssi.store(0);
|
||||
firstRssiLogged.store(false);
|
||||
}
|
||||
|
||||
void HostedBluetooth::clearBonds()
|
||||
{
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
|
||||
}
|
||||
|
||||
bool HostedBluetooth::isActive()
|
||||
{
|
||||
return active;
|
||||
}
|
||||
|
||||
bool HostedBluetooth::isConnected()
|
||||
{
|
||||
if (!connected.load()) {
|
||||
return false;
|
||||
}
|
||||
return bleServer && bleServer->getConnectedCount() > 0;
|
||||
}
|
||||
|
||||
int HostedBluetooth::getRssi()
|
||||
{
|
||||
if (!bleServer || !isConnected()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t connHandle = hostedConnHandle.load();
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
const auto peers = bleServer->getPeerDevices(true);
|
||||
if (!peers.empty()) {
|
||||
connHandle = peers.begin()->first;
|
||||
hostedConnHandle = connHandle;
|
||||
}
|
||||
}
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t currentRssi = 0;
|
||||
const int rc = ble_gap_conn_rssi(connHandle, ¤tRssi);
|
||||
if (rc == 0) {
|
||||
setRssi(currentRssi);
|
||||
return currentRssi;
|
||||
}
|
||||
|
||||
return rssi.load();
|
||||
}
|
||||
|
||||
void HostedBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
{
|
||||
if (!logRadioCharacteristic || !isConnected() || length > 512) {
|
||||
return;
|
||||
}
|
||||
|
||||
logRadioCharacteristic->setValue(logMessage, length);
|
||||
logRadioCharacteristic->notify();
|
||||
}
|
||||
|
||||
void HostedBluetooth::setConnected(bool value)
|
||||
{
|
||||
connected.store(value);
|
||||
}
|
||||
|
||||
void HostedBluetooth::setRssi(int value)
|
||||
{
|
||||
rssi.store(value);
|
||||
maybeLogFirstRssi(value);
|
||||
}
|
||||
|
||||
void HostedBluetooth::maybeLogFirstRssi(int value)
|
||||
{
|
||||
bool expected = false;
|
||||
if (firstRssiLogged.compare_exchange_strong(expected, true)) {
|
||||
LOG_INFO("ESP-Hosted first RSSI update: %d dBm", value);
|
||||
}
|
||||
}
|
||||
|
||||
bool HostedBluetooth::setupGatt()
|
||||
{
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
|
||||
if (!BLEDevice::init(getDeviceName())) {
|
||||
LOG_ERROR("Hosted BLE init failed");
|
||||
return false;
|
||||
}
|
||||
#ifdef ESP_PWR_LVL_P9
|
||||
BLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
#endif
|
||||
|
||||
BLESecurity *security = new BLESecurity();
|
||||
security->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
security->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
security->setCapability(ESP_IO_CAP_OUT);
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Hosted BLE using random passkey");
|
||||
security->setPassKey(false);
|
||||
} else {
|
||||
LOG_INFO("Hosted BLE using fixed passkey");
|
||||
security->setPassKey(true, config.bluetooth.fixed_pin);
|
||||
}
|
||||
// Enable authorization requirements:
|
||||
// - bonding: true (for persistent storage of the keys)
|
||||
// - MITM: true (enables Man-In-The-Middle protection for password prompts)
|
||||
// - secure connection: true (enables secure connection for encryption)
|
||||
security->setAuthenticationMode(true, true, true);
|
||||
} else {
|
||||
security->setCapability(ESP_IO_CAP_NONE);
|
||||
security->setAuthenticationMode(true, false, false);
|
||||
}
|
||||
|
||||
if (!securityCallbacks) {
|
||||
securityCallbacks = new HostedBluetoothSecurityCallback();
|
||||
}
|
||||
BLEDevice::setSecurityCallbacks(securityCallbacks);
|
||||
|
||||
const int mtuResult = BLEDevice::setMTU(kPreferredBleMtu);
|
||||
if (mtuResult == 0) {
|
||||
LOG_INFO("Hosted BLE MTU request set to %u", kPreferredBleMtu);
|
||||
} else {
|
||||
LOG_WARN("Hosted BLE unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult);
|
||||
}
|
||||
|
||||
bleServer = BLEDevice::createServer();
|
||||
if (!bleServer) {
|
||||
LOG_ERROR("Hosted BLE createServer failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
const int nameRc = ble_svc_gap_device_name_set(BLEDevice::getDeviceName().c_str());
|
||||
if (nameRc != 0) {
|
||||
LOG_WARN("Hosted BLE device_name_set rc=%d %s", nameRc, BLEUtils::returnCodeToString(nameRc));
|
||||
}
|
||||
|
||||
bleServer->setCallbacks(new HostedBluetoothServerCallback(this));
|
||||
|
||||
BLEService *meshService = bleServer->createService(MESH_SERVICE_UUID);
|
||||
if (!meshService) {
|
||||
LOG_ERROR("Hosted BLE mesh service creation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
BLECharacteristic *toRadioCharacteristic = nullptr;
|
||||
BLECharacteristic *fromRadioCharacteristic = nullptr;
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
toRadioCharacteristic = meshService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE);
|
||||
fromRadioCharacteristic = meshService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ);
|
||||
fromNumCharacteristic = meshService->createCharacteristic(FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY |
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
logRadioCharacteristic = meshService->createCharacteristic(LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY |
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
} else {
|
||||
toRadioCharacteristic = meshService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE |
|
||||
BLECharacteristic::PROPERTY_WRITE_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_WRITE_ENC);
|
||||
fromRadioCharacteristic = meshService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_READ_ENC);
|
||||
fromNumCharacteristic = meshService->createCharacteristic(
|
||||
FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
logRadioCharacteristic = meshService->createCharacteristic(
|
||||
LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
}
|
||||
|
||||
if (!toRadioCharacteristic || !fromRadioCharacteristic || !fromNumCharacteristic || !logRadioCharacteristic) {
|
||||
LOG_ERROR("Hosted BLE characteristic creation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bluetoothPhoneAPI) {
|
||||
bluetoothPhoneAPI = new HostedBluetoothPhoneAPI();
|
||||
}
|
||||
|
||||
if (!toRadioCallbacks) {
|
||||
toRadioCallbacks = new HostedBluetoothToRadioCallback();
|
||||
}
|
||||
if (!fromRadioCallbacks) {
|
||||
fromRadioCallbacks = new HostedBluetoothFromRadioCallback();
|
||||
}
|
||||
|
||||
toRadioCharacteristic->setCallbacks(toRadioCallbacks);
|
||||
fromRadioCharacteristic->setCallbacks(fromRadioCallbacks);
|
||||
meshService->start();
|
||||
|
||||
startAdvertising();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostedBluetooth::shutdownGatt()
|
||||
{
|
||||
if (bluetoothPhoneAPI) {
|
||||
bluetoothPhoneAPI->close();
|
||||
delete bluetoothPhoneAPI;
|
||||
bluetoothPhoneAPI = nullptr;
|
||||
}
|
||||
|
||||
delete toRadioCallbacks;
|
||||
toRadioCallbacks = nullptr;
|
||||
|
||||
delete fromRadioCallbacks;
|
||||
fromRadioCallbacks = nullptr;
|
||||
|
||||
delete securityCallbacks;
|
||||
securityCallbacks = nullptr;
|
||||
|
||||
if (bleServer) {
|
||||
BLEAdvertising *advertising = BLEDevice::getAdvertising();
|
||||
if (advertising) {
|
||||
advertising->stop();
|
||||
}
|
||||
}
|
||||
|
||||
fromNumCharacteristic = nullptr;
|
||||
logRadioCharacteristic = nullptr;
|
||||
bleServer = nullptr;
|
||||
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
}
|
||||
|
||||
void HostedBluetooth::startAdvertising()
|
||||
{
|
||||
BLEAdvertising *advertising = BLEDevice::getAdvertising();
|
||||
if (!advertising) {
|
||||
LOG_ERROR("Hosted BLE getAdvertising failed");
|
||||
return;
|
||||
}
|
||||
|
||||
advertising->stop();
|
||||
advertising->reset();
|
||||
advertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
|
||||
BLEAdvertisementData scan;
|
||||
scan.setName(getDeviceName());
|
||||
advertising->setScanResponseData(scan);
|
||||
advertising->setMinPreferred(0x06);
|
||||
advertising->setMaxPreferred(0x12);
|
||||
|
||||
if (!advertising->start(0)) {
|
||||
LOG_ERROR("Hosted BLE failed to start advertising");
|
||||
} else {
|
||||
LOG_INFO("Hosted BLE advertising started");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "BluetoothCommon.h"
|
||||
#include <atomic>
|
||||
|
||||
/**
|
||||
* Placeholder backend for ESP32-P4 + ESP-Hosted BLE transport.
|
||||
*
|
||||
* This intentionally mirrors the NimbleBluetooth surface used by the firmware,
|
||||
* so the caller side can stay stable while we implement esp-hosted internals.
|
||||
*/
|
||||
class HostedBluetooth : public BluetoothApi
|
||||
{
|
||||
public:
|
||||
HostedBluetooth();
|
||||
~HostedBluetooth() override;
|
||||
|
||||
void setup() override;
|
||||
void shutdown() override;
|
||||
void deinit() override;
|
||||
void clearBonds() override;
|
||||
bool isActive() override;
|
||||
bool isConnected() override;
|
||||
int getRssi() override;
|
||||
void sendLog(const uint8_t *logMessage, size_t length) override;
|
||||
void startAdvertising();
|
||||
|
||||
void setConnected(bool value);
|
||||
void setRssi(int value);
|
||||
|
||||
private:
|
||||
bool setupGatt();
|
||||
void shutdownGatt();
|
||||
void maybeLogFirstRssi(int value);
|
||||
bool registerCallbacks();
|
||||
void unregisterCallbacks();
|
||||
|
||||
bool active = false;
|
||||
std::atomic<bool> connected{false};
|
||||
std::atomic<int> rssi{0};
|
||||
std::atomic<bool> firstRssiLogged{false};
|
||||
bool callbacksRegistered = false;
|
||||
};
|
||||
+1
-1
@@ -80,7 +80,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Pre-hop drop handling (compile-time flag).
|
||||
#ifndef MESHTASTIC_PREHOP_DROP
|
||||
#define MESHTASTIC_PREHOP_DROP 0
|
||||
#define MESHTASTIC_PREHOP_DROP 1
|
||||
#endif
|
||||
|
||||
/// Convert a preprocessor name into a quoted string
|
||||
|
||||
@@ -53,10 +53,11 @@ void InkHUD::MessageStore::saveToFlash()
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i), min(MAX_MESSAGE_SIZE, m.text.size()),
|
||||
m.text.c_str());
|
||||
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()),
|
||||
min((size_t)MAX_MESSAGE_SIZE, m.text.size())); // Write message text
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i),
|
||||
min((size_t)MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
|
||||
}
|
||||
|
||||
// Release firmware's SPI lock, because SafeFile::close needs it
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
|
||||
|
||||
// REVISIT esp_adc_cal.h
|
||||
// "legacy adc calibration driver is deprecated, please migrate to use esp_adc/adc_cali.h and esp_adc/adc_cali_scheme.h"
|
||||
#include <esp_adc_cal.h>
|
||||
#include <soc/adc_channel.h>
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ TLoraPagerKeyboard::TLoraPagerKeyboard()
|
||||
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(UINT8_MAX),
|
||||
next_key(UINT8_MAX), last_tap(0L), char_idx(0), tap_interval(0)
|
||||
{
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
|
||||
ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
|
||||
#else
|
||||
ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
|
||||
@@ -108,7 +108,7 @@ void TLoraPagerKeyboard::setBacklight(bool on)
|
||||
uint32_t _brightness = 0;
|
||||
if (on)
|
||||
_brightness = brightness;
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
|
||||
ledcWrite(KB_BL_PIN, _brightness);
|
||||
#else
|
||||
ledcWrite(LEDC_BACKLIGHT_CHANNEL, _brightness);
|
||||
|
||||
+1
-2
@@ -49,8 +49,7 @@
|
||||
#include "mesh/http/WebServer.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
NimbleBluetooth *nimbleBluetooth = nullptr;
|
||||
BluetoothApi *bluetoothApi = nullptr;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
#include <SPI.h>
|
||||
#include <map>
|
||||
#if defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
extern NimbleBluetooth *nimbleBluetooth;
|
||||
#include "BluetoothCommon.h"
|
||||
extern BluetoothApi *bluetoothApi;
|
||||
#endif
|
||||
#ifdef ARCH_NRF52
|
||||
#include "NRF52Bluetooth.h"
|
||||
|
||||
+1
-3
@@ -1611,12 +1611,10 @@ bool NodeDB::saveToDisk(int saveWhat)
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR("Failed to save to disk, retrying");
|
||||
#ifdef ARCH_NRF52 // @geeksville is not ready yet to say we should do this on other platforms. See bug #4184 discussion
|
||||
spiLock->lock();
|
||||
FSCom.format();
|
||||
fsFormat();
|
||||
spiLock->unlock();
|
||||
|
||||
#endif
|
||||
success = saveToDiskNoRetry(saveWhat);
|
||||
|
||||
RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE
|
||||
|
||||
@@ -522,11 +522,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();
|
||||
@@ -657,9 +652,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)
|
||||
@@ -673,11 +670,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
#include "ServerAPI.h"
|
||||
#include <WiFi.h>
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
#include <WebServer.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
|
||||
#include <AsyncUDP.h>
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#define UDP_MULTICAST_DEFAUL_PORT 4403 // Default port for UDP multicast is same as TCP api server
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
#include "target_specific.h"
|
||||
#include <WiFi.h>
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#include <WiFiUdp.h>
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
extern bool needReconnect;
|
||||
|
||||
@@ -262,7 +262,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
break;
|
||||
}
|
||||
case meshtastic_AdminMessage_ota_request_tag: {
|
||||
#if defined(ARCH_ESP32)
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
||||
LOG_INFO("OTA Requested");
|
||||
|
||||
if (r->ota_request.ota_hash.size != 32) {
|
||||
@@ -1336,9 +1336,9 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
|
||||
conn.has_bluetooth = true;
|
||||
conn.bluetooth.pin = config.bluetooth.fixed_pin;
|
||||
#ifdef ARCH_ESP32
|
||||
if (config.bluetooth.enabled && nimbleBluetooth) {
|
||||
conn.bluetooth.is_connected = nimbleBluetooth->isConnected();
|
||||
conn.bluetooth.rssi = nimbleBluetooth->getRssi();
|
||||
if (config.bluetooth.enabled && bluetoothApi) {
|
||||
conn.bluetooth.is_connected = bluetoothApi->isConnected();
|
||||
conn.bluetooth.rssi = bluetoothApi->getRssi();
|
||||
}
|
||||
#elif defined(ARCH_NRF52)
|
||||
if (config.bluetooth.enabled && nrf52Bluetooth) {
|
||||
@@ -1412,7 +1412,9 @@ void AdminModule::saveChanges(int saveWhat, bool shouldReboot)
|
||||
|
||||
void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg)
|
||||
{
|
||||
#if HAS_SCREEN
|
||||
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
||||
@@ -1597,8 +1599,8 @@ void disableBluetooth()
|
||||
{
|
||||
#if HAS_BLUETOOTH
|
||||
#ifdef ARCH_ESP32
|
||||
if (nimbleBluetooth)
|
||||
nimbleBluetooth->deinit();
|
||||
if (bluetoothApi)
|
||||
bluetoothApi->deinit();
|
||||
#elif defined(ARCH_NRF52)
|
||||
if (nrf52Bluetooth)
|
||||
nrf52Bluetooth->shutdown();
|
||||
|
||||
@@ -16,10 +16,28 @@ int32_t INA3221Sensor::runOnce()
|
||||
return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS;
|
||||
}
|
||||
if (!status) {
|
||||
ina3221.begin(nodeTelemetrySensorsMap[sensorType].second);
|
||||
ina3221.setShuntRes(100, 100, 100); // 0.1 Ohm shunt resistors
|
||||
status = true;
|
||||
// Re-initialise with the address and Wire bus from the telemetry sensors map.
|
||||
// (Rob Tillaart INA3221_RT takes address + TwoWire*, unlike sgtwilko which took Wire in begin().)
|
||||
ina3221 = INA3221(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second);
|
||||
status = ina3221.begin();
|
||||
if (status) {
|
||||
// Default all three channels to a 0.1 Ω shunt resistor.
|
||||
// Override per-variant by defining INA3221_SHUNT_R_CH1/CH2/CH3 (in Ohms) in variant.h.
|
||||
#ifndef INA3221_SHUNT_R_CH1
|
||||
#define INA3221_SHUNT_R_CH1 0.1f
|
||||
#endif
|
||||
#ifndef INA3221_SHUNT_R_CH2
|
||||
#define INA3221_SHUNT_R_CH2 0.1f
|
||||
#endif
|
||||
#ifndef INA3221_SHUNT_R_CH3
|
||||
#define INA3221_SHUNT_R_CH3 0.1f
|
||||
#endif
|
||||
ina3221.setShuntR(0, INA3221_SHUNT_R_CH1);
|
||||
ina3221.setShuntR(1, INA3221_SHUNT_R_CH2);
|
||||
ina3221.setShuntR(2, INA3221_SHUNT_R_CH3);
|
||||
}
|
||||
} else {
|
||||
// Already initialised; status stays true and initI2CSensor() returns next poll interval.
|
||||
status = true;
|
||||
}
|
||||
return initI2CSensor();
|
||||
@@ -27,12 +45,14 @@ int32_t INA3221Sensor::runOnce()
|
||||
|
||||
void INA3221Sensor::setup() {}
|
||||
|
||||
struct _INA3221Measurement INA3221Sensor::getMeasurement(ina3221_ch_t ch)
|
||||
struct _INA3221Measurement INA3221Sensor::getMeasurement(uint8_t ch)
|
||||
{
|
||||
struct _INA3221Measurement measurement;
|
||||
|
||||
measurement.voltage = ina3221.getVoltage(ch);
|
||||
measurement.current = ina3221.getCurrent(ch);
|
||||
measurement.voltage = ina3221.getBusVoltage(ch); // Volts
|
||||
// getCurrent_mA() is used instead of getCurrent() because Rob Tillaart's getCurrent()
|
||||
// returns Amperes; the telemetry proto and VoltageSensor/CurrentSensor interfaces expect mA.
|
||||
measurement.current = ina3221.getCurrent_mA(ch); // milliAmps
|
||||
|
||||
return measurement;
|
||||
}
|
||||
@@ -43,7 +63,7 @@ struct _INA3221Measurements INA3221Sensor::getMeasurements()
|
||||
|
||||
// INA3221 has 3 channels starting from 0
|
||||
for (int i = 0; i < 3; i++) {
|
||||
measurements.measurements[i] = getMeasurement((ina3221_ch_t)i);
|
||||
measurements.measurements[i] = getMeasurement((uint8_t)i);
|
||||
}
|
||||
|
||||
return measurements;
|
||||
@@ -87,24 +107,41 @@ bool INA3221Sensor::getPowerMetrics(meshtastic_Telemetry *measurement)
|
||||
measurement->variant.power_metrics.has_ch3_voltage = true;
|
||||
measurement->variant.power_metrics.has_ch3_current = true;
|
||||
|
||||
measurement->variant.power_metrics.ch1_voltage = m.measurements[INA3221_CH1].voltage;
|
||||
measurement->variant.power_metrics.ch1_current = m.measurements[INA3221_CH1].current;
|
||||
measurement->variant.power_metrics.ch2_voltage = m.measurements[INA3221_CH2].voltage;
|
||||
measurement->variant.power_metrics.ch2_current = m.measurements[INA3221_CH2].current;
|
||||
measurement->variant.power_metrics.ch3_voltage = m.measurements[INA3221_CH3].voltage;
|
||||
measurement->variant.power_metrics.ch3_current = m.measurements[INA3221_CH3].current;
|
||||
// INA3221 channel indices are zero-based (0=CH1, 1=CH2, 2=CH3).
|
||||
measurement->variant.power_metrics.ch1_voltage = m.measurements[0].voltage;
|
||||
measurement->variant.power_metrics.ch1_current = m.measurements[0].current;
|
||||
measurement->variant.power_metrics.ch2_voltage = m.measurements[1].voltage;
|
||||
measurement->variant.power_metrics.ch2_current = m.measurements[1].current;
|
||||
measurement->variant.power_metrics.ch3_voltage = m.measurements[2].voltage;
|
||||
measurement->variant.power_metrics.ch3_current = m.measurements[2].current;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t INA3221Sensor::getBusVoltageMv()
|
||||
{
|
||||
return lround(ina3221.getVoltage(BAT_CH) * 1000);
|
||||
return lround(ina3221.getBusVoltage_mV(BAT_CH));
|
||||
}
|
||||
|
||||
int16_t INA3221Sensor::getCurrentMa()
|
||||
{
|
||||
return lround(ina3221.getCurrent(BAT_CH));
|
||||
return lround(ina3221.getCurrent_mA(BAT_CH));
|
||||
}
|
||||
|
||||
// Bus voltage register (0x02 + ch*2): bits [15:3] unsigned, 1 LSB = 8 mV (datasheet p.6).
|
||||
// Voltage raw units: 1 count = 8 mV, so V_mV = raw * 8.
|
||||
int16_t INA3221Sensor::getRawBusVoltage(uint8_t ch)
|
||||
{
|
||||
return (int16_t)(ina3221.getRegister(0x02 + ch * 2) >> 3);
|
||||
}
|
||||
|
||||
// Shunt voltage register (0x01 + ch*2): bits [15:3] signed two's complement, 1 LSB = 40 µV (datasheet p.6).
|
||||
// Current raw units are shunt-voltage counts: 1 count = 40 uV, signed.
|
||||
// I_mA = (raw * 40 uV) / R_mOhm, because uV / mOhm = mA.
|
||||
// Example for 100 mOhm shunt: I_mA = raw * 40 / 100 = raw * 0.4.
|
||||
int16_t INA3221Sensor::getRawShuntCurrent(uint8_t ch)
|
||||
{
|
||||
return (int16_t)(ina3221.getRegister(0x01 + ch * 2) >> 3);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,3 +1,9 @@
|
||||
// INA3221 channel aliases (zero-based: 0 = CH1, 1 = CH2, 2 = CH3).
|
||||
// Defined before configuration.h so variant.h can use them in INA3221_ENV_CH / INA3221_BAT_CH.
|
||||
#define INA3221_CH1 0
|
||||
#define INA3221_CH2 1
|
||||
#define INA3221_CH3 2
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<INA3221.h>)
|
||||
@@ -9,26 +15,29 @@
|
||||
#include <INA3221.h>
|
||||
|
||||
#ifndef INA3221_ENV_CH
|
||||
#define INA3221_ENV_CH INA3221_CH1
|
||||
#define INA3221_ENV_CH INA3221_CH1 // channel to report in environment metrics (default: CH1)
|
||||
#endif
|
||||
|
||||
#ifndef INA3221_BAT_CH
|
||||
#define INA3221_BAT_CH INA3221_CH1
|
||||
#define INA3221_BAT_CH INA3221_CH1 // channel for device_battery_ina_address (default: CH1)
|
||||
#endif
|
||||
|
||||
class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
|
||||
{
|
||||
private:
|
||||
INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA);
|
||||
// Placeholder constructor; re-initialised with correct address and Wire in runOnce().
|
||||
INA3221 ina3221 = INA3221(INA3221_ADDR);
|
||||
|
||||
// channel to report voltage/current for environment metrics
|
||||
static const ina3221_ch_t ENV_CH = INA3221_ENV_CH;
|
||||
static const uint8_t ENV_CH = INA3221_ENV_CH;
|
||||
static_assert(INA3221_ENV_CH >= 0 && INA3221_ENV_CH <= 2, "INA3221_ENV_CH must be 0, 1, or 2");
|
||||
|
||||
// channel to report battery voltage for device_battery_ina_address
|
||||
static const ina3221_ch_t BAT_CH = INA3221_BAT_CH;
|
||||
static const uint8_t BAT_CH = INA3221_BAT_CH;
|
||||
static_assert(INA3221_BAT_CH >= 0 && INA3221_BAT_CH <= 2, "INA3221_BAT_CH must be 0, 1, or 2");
|
||||
|
||||
// get a single measurement for a channel
|
||||
struct _INA3221Measurement getMeasurement(ina3221_ch_t ch);
|
||||
struct _INA3221Measurement getMeasurement(uint8_t ch);
|
||||
|
||||
// get all measurements for all channels
|
||||
struct _INA3221Measurements getMeasurements();
|
||||
@@ -45,6 +54,10 @@ class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor
|
||||
bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual uint16_t getBusVoltageMv() override;
|
||||
virtual int16_t getCurrentMa() override;
|
||||
|
||||
// Raw register reads (bits [15:3] right-shifted), no conversion applied.
|
||||
int16_t getRawBusVoltage(uint8_t ch);
|
||||
int16_t getRawShuntCurrent(uint8_t ch);
|
||||
};
|
||||
|
||||
struct _INA3221Measurement {
|
||||
|
||||
@@ -90,8 +90,8 @@ int32_t PaxcounterModule::runOnce()
|
||||
configuration.blecounter = 1;
|
||||
configuration.blescantime = 0; // infinite
|
||||
configuration.wificounter = 1;
|
||||
configuration.wifi_channel_map = WIFI_CHANNEL_ALL;
|
||||
configuration.wifi_channel_switch_interval = 50;
|
||||
// configuration.wifi_channel_map = WIFI_CHANNEL_ALL;
|
||||
// configuration.wifi_channel_switch_interval = 50;
|
||||
configuration.wifi_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.wifi_threshold, -80);
|
||||
configuration.ble_rssi_threshold = Default::getConfiguredOrDefault(moduleConfig.paxcounter.ble_threshold, -80);
|
||||
libpax_update_config(&configuration);
|
||||
|
||||
+2
-3
@@ -19,9 +19,8 @@
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#if HAS_ETHERNET && defined(USE_WS5500)
|
||||
#include <ETHClass2.h>
|
||||
#define ETH ETH2
|
||||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||||
#include <ETH.h>
|
||||
#endif // HAS_ETHERNET
|
||||
#include "Default.h"
|
||||
#if !defined(ARCH_NRF52) || NRF52_USE_JSON
|
||||
|
||||
+178
-245
@@ -1,5 +1,5 @@
|
||||
#include "configuration.h"
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH && !defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
#include "BluetoothCommon.h"
|
||||
#include "NimbleBluetooth.h"
|
||||
#include "PowerFSM.h"
|
||||
@@ -10,31 +10,28 @@
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include "sleep.h"
|
||||
#include <NimBLEDevice.h>
|
||||
#include <BLE2904.h>
|
||||
#include <BLEAdvertising.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLESecurity.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
#include "NimBLEAdvertising.h"
|
||||
#include "NimBLEExtAdvertising.h"
|
||||
#include "PowerStatus.h"
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_NIMBLE_CPP_IDF)
|
||||
#include "host/ble_gap.h"
|
||||
#else
|
||||
#include "nimble/nimble/host/include/host/ble_gap.h"
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
#include "host/ble_store.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Maintainer note: this backend intentionally diverges from HostedBluetooth in a few platform-specific areas.
|
||||
// If you change shared BLE flow here (PhoneAPI queue/sync, security/pairing, mesh GATT/advertising,
|
||||
// connect/disconnect handling), review and update HostedBluetooth.cpp as needed.
|
||||
constexpr uint16_t kPreferredBleMtu = 517;
|
||||
constexpr uint16_t kPreferredBleTxOctets = 251;
|
||||
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// Debugging options: careful, they slow things down quite a bit!
|
||||
// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
|
||||
@@ -44,10 +41,10 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
#define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3
|
||||
#define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3
|
||||
|
||||
NimBLECharacteristic *fromNumCharacteristic;
|
||||
NimBLECharacteristic *BatteryCharacteristic;
|
||||
NimBLECharacteristic *logRadioCharacteristic;
|
||||
NimBLEServer *bleServer;
|
||||
BLECharacteristic *fromNumCharacteristic;
|
||||
BLECharacteristic *BatteryCharacteristic;
|
||||
BLECharacteristic *logRadioCharacteristic;
|
||||
BLEServer *bleServer;
|
||||
|
||||
static bool passkeyShowing;
|
||||
static std::atomic<uint16_t> nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection"
|
||||
@@ -118,7 +115,8 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
- Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management.
|
||||
|
||||
NOTIFY IS BROKEN:
|
||||
- Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible.
|
||||
- Adding BLECharacteristic::PROPERTY_NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards
|
||||
compatible.
|
||||
|
||||
ZERO-SIZE READS:
|
||||
- Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we
|
||||
@@ -139,7 +137,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
std::mutex fromPhoneMutex;
|
||||
std::atomic<size_t> fromPhoneQueueSize{0};
|
||||
// We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
|
||||
std::array<NimBLEAttValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
|
||||
std::array<BLEValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
|
||||
|
||||
/* Packets to phone (BLE onRead callback) */
|
||||
std::mutex toPhoneMutex;
|
||||
@@ -301,7 +299,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load());
|
||||
|
||||
// Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop.
|
||||
NimBLEAttValue val;
|
||||
BLEValue val;
|
||||
{ // scope for fromPhoneMutex mutex
|
||||
std::lock_guard<std::mutex> guard(fromPhoneMutex);
|
||||
val = fromPhoneQueue[0];
|
||||
@@ -316,7 +314,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
fromPhoneQueueSize--;
|
||||
}
|
||||
|
||||
handleToRadio(val.data(), val.length());
|
||||
handleToRadio(val.getData(), val.getLength());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,9 +326,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
|
||||
#ifdef DEBUG_NIMBLE_NOTIFY
|
||||
|
||||
int currentNotifyCount = notifyCount.fetch_add(1);
|
||||
|
||||
uint8_t cc = bleServer->getConnectedCount();
|
||||
// This logging slows things down when there are lots of packets going to the phone, like initial connection:
|
||||
LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc);
|
||||
@@ -340,13 +336,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
put_le32(val, fromRadioNum);
|
||||
|
||||
fromNumCharacteristic->setValue(val, sizeof(val));
|
||||
#ifdef NIMBLE_TWO
|
||||
// NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be
|
||||
// notify().
|
||||
fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
fromNumCharacteristic->notify();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
@@ -409,14 +399,9 @@ static BluetoothPhoneAPI *bluetoothPhoneAPI;
|
||||
// Last ToRadio value received from the phone
|
||||
static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
|
||||
|
||||
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
class NimbleBluetoothToRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
||||
#else
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic) override
|
||||
|
||||
#endif
|
||||
void onWrite(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||
// Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls.
|
||||
@@ -428,15 +413,17 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis);
|
||||
#endif
|
||||
|
||||
auto val = pCharacteristic->getValue();
|
||||
// Create a BLEValue and populate it with the received data
|
||||
BLEValue val;
|
||||
val.setValue(pCharacteristic->getData(), pCharacteristic->getLength());
|
||||
|
||||
if (memcmp(lastToRadio, val.data(), val.length()) != 0) {
|
||||
if (memcmp(lastToRadio, val.getData(), val.getLength()) != 0) {
|
||||
if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) {
|
||||
// Note: the comparison above is safe without a mutex because we are the only method that *increases*
|
||||
// fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.)
|
||||
memcpy(lastToRadio, val.data(), val.length());
|
||||
memcpy(lastToRadio, val.getData(), val.getLength());
|
||||
|
||||
{ // scope for fromPhoneMutex mutex
|
||||
{ // scope for fromPhoneMutex mutexv, pCharacteristic->getLen
|
||||
// Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible.
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
|
||||
@@ -450,24 +437,21 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
|
||||
int finishMillis = millis();
|
||||
LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount,
|
||||
finishMillis - startMillis, val.length());
|
||||
finishMillis - startMillis, val.getLength());
|
||||
#endif
|
||||
} else {
|
||||
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length());
|
||||
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount,
|
||||
val.getLength());
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length());
|
||||
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.getLength());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||
class NimbleBluetoothFromRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
||||
#else
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic) override
|
||||
#endif
|
||||
void onRead(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||
|
||||
@@ -573,34 +557,15 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||
}
|
||||
};
|
||||
|
||||
class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
class NimbleBluetoothSecurityCallback : public BLESecurityCallbacks
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
public:
|
||||
NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; }
|
||||
|
||||
private:
|
||||
NimbleBluetooth *ble;
|
||||
|
||||
virtual uint32_t onPassKeyDisplay() override
|
||||
#else
|
||||
virtual uint32_t onPassKeyRequest() override
|
||||
#endif
|
||||
void onPassKeyNotify(uint32_t passkey) override
|
||||
{
|
||||
uint32_t passkey = config.bluetooth.fixed_pin;
|
||||
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Use random passkey");
|
||||
// This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits
|
||||
passkey = random(100000, 999999);
|
||||
}
|
||||
LOG_INFO("*** Enter passkey %d on the peer side ***", passkey);
|
||||
|
||||
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey);
|
||||
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
|
||||
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
@@ -630,15 +595,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
}
|
||||
#endif
|
||||
passkeyShowing = true;
|
||||
|
||||
return passkey;
|
||||
}
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) override
|
||||
#else
|
||||
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) override
|
||||
#endif
|
||||
void onAuthenticationComplete(ble_gap_conn_desc *desc) override
|
||||
{
|
||||
LOG_INFO("BLE authentication complete");
|
||||
|
||||
@@ -646,58 +604,47 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
clearPairingDisplay();
|
||||
|
||||
// Store the connection handle for future use
|
||||
#ifdef NIMBLE_TWO
|
||||
nimbleBluetoothConnHandle = connInfo.getConnHandle();
|
||||
#else
|
||||
nimbleBluetoothConnHandle = desc->conn_handle;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override
|
||||
class NimbleBluetoothServerCallback : public BLEServerCallbacks
|
||||
{
|
||||
public:
|
||||
explicit NimbleBluetoothServerCallback(NimbleBluetooth *ble) : ble(ble) {}
|
||||
|
||||
private:
|
||||
NimbleBluetooth *ble;
|
||||
|
||||
void onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
|
||||
{
|
||||
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
|
||||
BLEAddress peer_addr(desc->peer_id_addr);
|
||||
LOG_INFO("BLE incoming connection %s", peer_addr.toString().c_str());
|
||||
|
||||
const uint16_t connHandle = connInfo.getConnHandle();
|
||||
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
|
||||
int phyResult =
|
||||
ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY);
|
||||
if (phyResult == 0) {
|
||||
LOG_INFO("BLE conn %u requested 2M PHY", connHandle);
|
||||
} else {
|
||||
LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult);
|
||||
}
|
||||
#endif
|
||||
const uint16_t connHandle = desc->conn_handle;
|
||||
|
||||
// With Google Pixel 8 Android devices, this causes ESP32 device crash
|
||||
// when phone reconnects. Disable this to make progress on the
|
||||
// Arduino v3 migration while we investigate the Android compatibility
|
||||
// issue.
|
||||
#if 0
|
||||
int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
|
||||
if (dataLenResult == 0) {
|
||||
LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets);
|
||||
} else {
|
||||
LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult);
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu);
|
||||
LOG_INFO("BLE conn %u peer MTU %u (target %u)", connHandle, pServer->getPeerMTU(connHandle), kPreferredBleMtu);
|
||||
pServer->updateConnParams(connHandle, 6, 12, 0, 200);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override
|
||||
void onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
|
||||
{
|
||||
LOG_INFO("BLE disconnect reason: %d", reason);
|
||||
#else
|
||||
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) override
|
||||
{
|
||||
LOG_INFO("BLE disconnect");
|
||||
#endif
|
||||
#ifdef NIMBLE_TWO
|
||||
LOG_INFO("BLE disconnected");
|
||||
if (ble->isDeInit)
|
||||
return;
|
||||
#else
|
||||
if (nimbleBluetooth && nimbleBluetooth->isDeInit)
|
||||
return;
|
||||
#endif
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
@@ -722,43 +669,51 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
bluetoothPhoneAPI->writeCount = 0;
|
||||
}
|
||||
|
||||
// Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
|
||||
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection"
|
||||
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
// Restart Advertising
|
||||
ble->startAdvertising();
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
if (!pAdvertising->start(0)) {
|
||||
if (pAdvertising->isAdvertising()) {
|
||||
LOG_DEBUG("BLE advertising already running");
|
||||
} else {
|
||||
LOG_ERROR("BLE failed to restart advertising");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
static NimbleBluetoothToRadioCallback *toRadioCallbacks;
|
||||
static NimbleBluetoothFromRadioCallback *fromRadioCallbacks;
|
||||
|
||||
void NimbleBluetooth::startAdvertising()
|
||||
{
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->stop();
|
||||
pAdvertising->reset();
|
||||
|
||||
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
// if (powerStatus->getHasBattery() == 1) {
|
||||
// pAdvertising->addServiceUUID(BLEUUID((uint16_t)0x180f));
|
||||
// }
|
||||
|
||||
BLEAdvertisementData scan = BLEAdvertisementData();
|
||||
scan.setName(getDeviceName());
|
||||
pAdvertising->setScanResponseData(scan);
|
||||
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
|
||||
pAdvertising->setMaxPreferred(0x12);
|
||||
|
||||
if (!pAdvertising->start(0)) {
|
||||
LOG_ERROR("BLE failed to start advertising");
|
||||
} else {
|
||||
LOG_DEBUG("BLE Advertising started");
|
||||
}
|
||||
}
|
||||
|
||||
void NimbleBluetooth::shutdown()
|
||||
{
|
||||
// No measurable power saving for ESP32 during light-sleep(?)
|
||||
#ifndef ARCH_ESP32
|
||||
// Shutdown bluetooth for minimum power draw
|
||||
LOG_INFO("Disable bluetooth");
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->stop();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Proper shutdown for ESP32. Needs reboot to reverse.
|
||||
void NimbleBluetooth::deinit()
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -768,21 +723,19 @@ void NimbleBluetooth::deinit()
|
||||
#ifdef BLE_LED
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
#ifndef NIMBLE_TWO
|
||||
NimBLEDevice::deinit();
|
||||
#endif
|
||||
|
||||
BLEDevice::deinit(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Has initial setup been completed
|
||||
bool NimbleBluetooth::isActive()
|
||||
{
|
||||
return bleServer;
|
||||
return bleServer != nullptr;
|
||||
}
|
||||
|
||||
bool NimbleBluetooth::isConnected()
|
||||
{
|
||||
return bleServer->getConnectedCount() > 0;
|
||||
return bleServer && bleServer->getConnectedCount() > 0;
|
||||
}
|
||||
|
||||
int NimbleBluetooth::getRssi()
|
||||
@@ -795,9 +748,9 @@ int NimbleBluetooth::getRssi()
|
||||
uint16_t connHandle = nimbleBluetoothConnHandle.load();
|
||||
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
const auto peers = bleServer->getPeerDevices();
|
||||
const auto peers = bleServer->getPeerDevices(true);
|
||||
if (!peers.empty()) {
|
||||
connHandle = peers.front();
|
||||
connHandle = peers.begin()->first;
|
||||
nimbleBluetoothConnHandle = connHandle;
|
||||
}
|
||||
}
|
||||
@@ -825,74 +778,84 @@ void NimbleBluetooth::setup()
|
||||
|
||||
LOG_INFO("Init the NimBLE bluetooth module");
|
||||
|
||||
NimBLEDevice::init(getDeviceName());
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
BLEDevice::init(getDeviceName());
|
||||
BLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
|
||||
int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu);
|
||||
int mtuResult = BLEDevice::setMTU(kPreferredBleMtu);
|
||||
if (mtuResult == 0) {
|
||||
LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu);
|
||||
} else {
|
||||
LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult);
|
||||
}
|
||||
|
||||
int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK);
|
||||
if (phyResult == 0) {
|
||||
LOG_INFO("BLE default PHY preference set to 2M");
|
||||
} else {
|
||||
LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult);
|
||||
}
|
||||
|
||||
int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs);
|
||||
if (dataLenResult == 0) {
|
||||
LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets);
|
||||
} else {
|
||||
LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs,
|
||||
dataLenResult);
|
||||
}
|
||||
#endif
|
||||
|
||||
BLESecurity *pSecurity = new BLESecurity();
|
||||
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
pSecurity->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC);
|
||||
NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
|
||||
NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
|
||||
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
|
||||
// Set IO capability to DisplayOnly for MITM authentication
|
||||
pSecurity->setCapability(ESP_IO_CAP_OUT);
|
||||
// Set the passkey
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Use random passkey");
|
||||
pSecurity->setPassKey(false); // generate a random passkey
|
||||
} else {
|
||||
LOG_INFO("Use fixed passkey");
|
||||
pSecurity->setPassKey(true, config.bluetooth.fixed_pin);
|
||||
}
|
||||
// Enable authorization requirements:
|
||||
// - bonding: true (for persistent storage of the keys)
|
||||
// - MITM: true (enables Man-In-The-Middle protection for password prompts)
|
||||
// - secure connection: true (enables secure connection for encryption)
|
||||
pSecurity->setAuthenticationMode(true, true, true);
|
||||
} else {
|
||||
// No IO capability for no PIN mode
|
||||
pSecurity->setCapability(ESP_IO_CAP_NONE);
|
||||
// No PIN mode: no MITM protection
|
||||
pSecurity->setAuthenticationMode(true, false, false);
|
||||
}
|
||||
bleServer = NimBLEDevice::createServer();
|
||||
#ifdef NIMBLE_TWO
|
||||
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this);
|
||||
#else
|
||||
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback();
|
||||
#endif
|
||||
bleServer->setCallbacks(serverCallbacks, true);
|
||||
// Set the security callbacks
|
||||
BLEDevice::setSecurityCallbacks(new NimbleBluetoothSecurityCallback());
|
||||
bleServer = BLEDevice::createServer();
|
||||
|
||||
// BLEDevice::createServer calls ble_svc_gap_init, which resets the device
|
||||
// name to default, so set it again.
|
||||
int nameRc = ble_svc_gap_device_name_set(BLEDevice::getDeviceName().c_str());
|
||||
if (nameRc != 0) {
|
||||
LOG_ERROR("ble_svc_gap_device_name_set: rc=%d %s", nameRc, BLEUtils::returnCodeToString(nameRc));
|
||||
}
|
||||
|
||||
bleServer->setCallbacks(new NimbleBluetoothServerCallback(this));
|
||||
setupService();
|
||||
startAdvertising();
|
||||
}
|
||||
|
||||
void NimbleBluetooth::setupService()
|
||||
{
|
||||
NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID);
|
||||
NimBLECharacteristic *ToRadioCharacteristic;
|
||||
NimBLECharacteristic *FromRadioCharacteristic;
|
||||
BLEService *bleService = bleServer->createService(MESH_SERVICE_UUID);
|
||||
BLECharacteristic *ToRadioCharacteristic;
|
||||
BLECharacteristic *FromRadioCharacteristic;
|
||||
// Define the characteristics that the app is looking for
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE);
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE);
|
||||
// Allow notifications so phones can stream FromRadio without polling.
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ);
|
||||
fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ);
|
||||
logRadioCharacteristic =
|
||||
bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U);
|
||||
} else {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(
|
||||
TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(
|
||||
FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ);
|
||||
fromNumCharacteristic =
|
||||
bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ |
|
||||
NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
|
||||
bleService->createCharacteristic(FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ);
|
||||
logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY |
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
} else {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE |
|
||||
BLECharacteristic::PROPERTY_WRITE_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_WRITE_ENC);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_READ_ENC);
|
||||
fromNumCharacteristic = bleService->createCharacteristic(
|
||||
FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
logRadioCharacteristic = bleService->createCharacteristic(
|
||||
LOGRADIO_UUID,
|
||||
NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U);
|
||||
LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
}
|
||||
bluetoothPhoneAPI = new BluetoothPhoneAPI();
|
||||
|
||||
@@ -905,76 +868,31 @@ void NimbleBluetooth::setupService()
|
||||
bleService->start();
|
||||
|
||||
// Setup the battery service
|
||||
NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
|
||||
(uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1);
|
||||
#ifdef NIMBLE_TWO
|
||||
NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904();
|
||||
#else
|
||||
NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904);
|
||||
#endif
|
||||
batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8);
|
||||
BLEService *batteryService = bleServer->createService(BLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
BLE2904 *batteryLevelDescriptor = new BLE2904();
|
||||
batteryLevelDescriptor->setFormat(BLE2904::FORMAT_UINT8);
|
||||
batteryLevelDescriptor->setNamespace(1);
|
||||
batteryLevelDescriptor->setUnit(0x27ad);
|
||||
|
||||
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
|
||||
(uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
BatteryCharacteristic->addDescriptor(batteryLevelDescriptor);
|
||||
batteryService->start();
|
||||
}
|
||||
|
||||
void NimbleBluetooth::startAdvertising()
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
NimBLEExtAdvertisement legacyAdvertising;
|
||||
|
||||
legacyAdvertising.setLegacyAdvertising(true);
|
||||
legacyAdvertising.setScannable(true);
|
||||
legacyAdvertising.setConnectable(true);
|
||||
legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN);
|
||||
if (powerStatus->getHasBattery() == 1) {
|
||||
legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f));
|
||||
}
|
||||
legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID));
|
||||
legacyAdvertising.setMinInterval(500);
|
||||
legacyAdvertising.setMaxInterval(1000);
|
||||
|
||||
NimBLEExtAdvertisement legacyScanResponse;
|
||||
legacyScanResponse.setLegacyAdvertising(true);
|
||||
legacyScanResponse.setConnectable(true);
|
||||
legacyScanResponse.setName(getDeviceName());
|
||||
|
||||
if (!pAdvertising->setInstanceData(0, legacyAdvertising)) {
|
||||
LOG_ERROR("BLE failed to set legacyAdvertising");
|
||||
} else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) {
|
||||
LOG_ERROR("BLE failed to set legacyScanResponse");
|
||||
} else if (!pAdvertising->start(0, 0, 0)) {
|
||||
LOG_ERROR("BLE failed to start legacyAdvertising");
|
||||
}
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
pAdvertising->start(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
if ((config.bluetooth.enabled == true) && bleServer && nimbleBluetooth->isConnected()) {
|
||||
if ((config.bluetooth.enabled == true) && bleServer && bluetoothApi && bluetoothApi->isConnected()) {
|
||||
BatteryCharacteristic->setValue(&level, 1);
|
||||
#ifdef NIMBLE_TWO
|
||||
BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
BatteryCharacteristic->notify();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void NimbleBluetooth::clearBonds()
|
||||
{
|
||||
LOG_INFO("Clearing bluetooth bonds!");
|
||||
NimBLEDevice::deleteAllBonds();
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
|
||||
}
|
||||
|
||||
void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
@@ -982,11 +900,26 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
if (!bleServer || !isConnected() || length > 512) {
|
||||
return;
|
||||
}
|
||||
#ifdef NIMBLE_TWO
|
||||
logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
logRadioCharacteristic->notify(logMessage, length, true);
|
||||
logRadioCharacteristic->setValue(logMessage, length);
|
||||
logRadioCharacteristic->notify();
|
||||
}
|
||||
|
||||
void clearNVS()
|
||||
{
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
|
||||
#ifdef ARCH_ESP32
|
||||
ESP.restart();
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
(void)level;
|
||||
}
|
||||
|
||||
void clearNVS() {}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
#pragma once
|
||||
#include "BluetoothCommon.h"
|
||||
|
||||
class NimbleBluetooth : BluetoothApi
|
||||
class NimbleBluetooth : public BluetoothApi
|
||||
{
|
||||
public:
|
||||
void setup();
|
||||
void shutdown();
|
||||
void deinit();
|
||||
void clearBonds();
|
||||
bool isActive();
|
||||
bool isConnected();
|
||||
int getRssi();
|
||||
void sendLog(const uint8_t *logMessage, size_t length);
|
||||
#if defined(NIMBLE_TWO)
|
||||
void setup() override;
|
||||
void shutdown() override;
|
||||
void deinit() override;
|
||||
void clearBonds() override;
|
||||
bool isActive() override;
|
||||
bool isConnected() override;
|
||||
int getRssi() override;
|
||||
void sendLog(const uint8_t *logMessage, size_t length) override;
|
||||
void startAdvertising();
|
||||
#endif
|
||||
virtual ~NimbleBluetooth() {}
|
||||
|
||||
bool isDeInit = false;
|
||||
|
||||
private:
|
||||
void setupService();
|
||||
#if !defined(NIMBLE_TWO)
|
||||
void startAdvertising();
|
||||
#endif
|
||||
};
|
||||
|
||||
void setBluetoothEnable(bool enable);
|
||||
@@ -0,0 +1,42 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_attr.h"
|
||||
|
||||
#ifdef ESP32_FORCE_IRAM_MEMSET
|
||||
|
||||
/*
|
||||
* T-Beam/classic ESP32 boot workaround
|
||||
* ------------------------------------
|
||||
* During early flash operations the ESP32 disables cache, but some IRAM flash
|
||||
* code paths still reach libc memcpy/memset. If those resolve to flash-resident
|
||||
* implementations, startup can panic with cache-disabled access errors.
|
||||
*
|
||||
* We wrap memcpy/memset for the T-Beam environment. Fast path uses the
|
||||
* normal libc routines when cache is enabled; slow path uses IRAM-safe byte
|
||||
* loops when cache is disabled.
|
||||
*/
|
||||
|
||||
extern void *__real_memcpy(void *dst, const void *src, size_t n);
|
||||
|
||||
static inline bool IRAM_ATTR cache_is_enabled(void)
|
||||
{
|
||||
return (*(volatile uint32_t *)0x3FF00040u) & (1u << 3);
|
||||
}
|
||||
|
||||
extern void *IRAM_ATTR __wrap_memcpy(void *dst, const void *src, size_t n)
|
||||
{
|
||||
if (cache_is_enabled()) {
|
||||
return __real_memcpy(dst, src, n);
|
||||
}
|
||||
|
||||
uint8_t *d = (uint8_t *)dst;
|
||||
const uint8_t *s = (const uint8_t *)src;
|
||||
while (n--) {
|
||||
*d++ = *s++;
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_attr.h"
|
||||
|
||||
#ifdef ESP32_FORCE_IRAM_MEMSET
|
||||
|
||||
/*
|
||||
* T-Beam/classic ESP32 boot workaround
|
||||
* ------------------------------------
|
||||
* During early flash operations the ESP32 disables cache, but some IRAM flash
|
||||
* code paths still reach libc memcpy/memset. If those resolve to flash-resident
|
||||
* implementations, startup can panic with cache-disabled access errors.
|
||||
*
|
||||
* We wrap memcpy/memset for the T-Beam environment. Fast path uses the
|
||||
* normal libc routines when cache is enabled; slow path uses IRAM-safe byte
|
||||
* loops when cache is disabled.
|
||||
*/
|
||||
|
||||
extern void *__real_memset(void *dst, int c, size_t n);
|
||||
|
||||
static inline bool IRAM_ATTR cache_is_enabled(void)
|
||||
{
|
||||
return (*(volatile uint32_t *)0x3FF00040u) & (1u << 3);
|
||||
}
|
||||
|
||||
extern void *IRAM_ATTR __wrap_memset(void *dst, int c, size_t n)
|
||||
{
|
||||
if (cache_is_enabled()) {
|
||||
return __real_memset(dst, c, n);
|
||||
}
|
||||
|
||||
uint8_t *ptr = (uint8_t *)dst;
|
||||
const uint8_t fill = (uint8_t)c;
|
||||
while (n--) {
|
||||
*ptr++ = fill;
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
/* Arduino fix: catch esp_event's orphaned .text.handler_execute section and align to 4 bytes */
|
||||
SECTIONS
|
||||
{
|
||||
.text.handler_execute ALIGN(4) :
|
||||
{
|
||||
KEEP(*(.text.handler_execute))
|
||||
KEEP(*(.text.handler_execute.*))
|
||||
. = ALIGN(4);
|
||||
}
|
||||
}
|
||||
INSERT AFTER .flash.text;
|
||||
@@ -4,9 +4,13 @@
|
||||
#include "esp_task_wdt.h"
|
||||
#include "main.h"
|
||||
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
#include "bluetooth/HostedBluetooth.h"
|
||||
#elif !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <MeshtasticOTA.h>
|
||||
|
||||
@@ -40,16 +44,30 @@ void setBluetoothEnable(bool enable)
|
||||
if (config.bluetooth.enabled == true)
|
||||
#endif
|
||||
{
|
||||
if (!nimbleBluetooth) {
|
||||
nimbleBluetooth = new NimbleBluetooth();
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
if (!enable) {
|
||||
if (bluetoothApi && bluetoothApi->isActive()) {
|
||||
bluetoothApi->shutdown();
|
||||
powerMon->clearState(meshtastic_PowerMon_State_BT_On);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (enable && !nimbleBluetooth->isActive()) {
|
||||
#endif
|
||||
|
||||
if (!bluetoothApi) {
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32P4)
|
||||
bluetoothApi = new HostedBluetooth();
|
||||
#else
|
||||
bluetoothApi = new NimbleBluetooth();
|
||||
#endif
|
||||
}
|
||||
if (enable && !bluetoothApi->isActive()) {
|
||||
powerMon->setState(meshtastic_PowerMon_State_BT_On);
|
||||
nimbleBluetooth->setup();
|
||||
bluetoothApi->setup();
|
||||
}
|
||||
// For ESP32, no way to recover from bluetooth shutdown without reboot
|
||||
// BLE advertising automatically stops when MCU enters light-sleep(?)
|
||||
// For deep-sleep, shutdown hardware with nimbleBluetooth->deinit(). Requires reboot to reverse
|
||||
// For deep-sleep, shutdown hardware with bluetoothApi->deinit(). Requires reboot to reverse
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -165,17 +183,30 @@ void esp32Setup()
|
||||
// #define APP_WATCHDOG_SECS 45
|
||||
#define APP_WATCHDOG_SECS 90
|
||||
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32C6
|
||||
esp_task_wdt_config_t *wdt_config = (esp_task_wdt_config_t *)malloc(sizeof(esp_task_wdt_config_t));
|
||||
wdt_config->timeout_ms = APP_WATCHDOG_SECS * 1000;
|
||||
wdt_config->trigger_panic = true;
|
||||
res = esp_task_wdt_init(wdt_config);
|
||||
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
|
||||
const esp_task_wdt_config_t wdt_config = {
|
||||
.timeout_ms = APP_WATCHDOG_SECS * 1000,
|
||||
.idle_core_mask = (1U << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1U,
|
||||
.trigger_panic = true,
|
||||
};
|
||||
res = esp_task_wdt_init(&wdt_config);
|
||||
if (res == ESP_ERR_INVALID_STATE) {
|
||||
LOG_WARN("Task watchdog already initialized, reconfiguring existing instance");
|
||||
res = esp_task_wdt_reconfigure(&wdt_config);
|
||||
}
|
||||
assert(res == ESP_OK);
|
||||
#else
|
||||
res = esp_task_wdt_init(APP_WATCHDOG_SECS, true);
|
||||
if (res == ESP_ERR_INVALID_STATE) {
|
||||
LOG_WARN("Task watchdog already initialized, reusing existing instance");
|
||||
res = ESP_OK;
|
||||
}
|
||||
assert(res == ESP_OK);
|
||||
#endif
|
||||
res = esp_task_wdt_add(NULL);
|
||||
res = esp_task_wdt_status(NULL);
|
||||
if (res == ESP_ERR_NOT_FOUND) {
|
||||
res = esp_task_wdt_add(NULL);
|
||||
}
|
||||
assert(res == ESP_OK);
|
||||
|
||||
#if HAS_32768HZ
|
||||
@@ -258,8 +289,10 @@ void cpuDeepSleep(uint32_t msecToWake)
|
||||
#endif
|
||||
variant_shutdown();
|
||||
|
||||
#if SOC_PM_SUPPORT_RTC_PERIPH_PD
|
||||
// We want RTC peripherals to stay on
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
#endif
|
||||
|
||||
esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs
|
||||
esp_deep_sleep_start(); // TBD mA sleep current (battery)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <ErriezCRC32.h>
|
||||
#include <Utility.h>
|
||||
#include <assert.h>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -32,6 +33,16 @@
|
||||
#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;
|
||||
@@ -155,9 +166,35 @@ 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 BlueZ on non-Linux hosts (e.g. macOS). Leave dmac at its default;
|
||||
// the caller can override via the --hwid CLI flag or the YAML config.
|
||||
// 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
|
||||
}
|
||||
@@ -1056,17 +1093,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)
|
||||
|
||||
@@ -25,23 +25,27 @@
|
||||
#include "LittleFS.h"
|
||||
#include "stm32wlxx_hal_flash.h"
|
||||
|
||||
/**********************************************************************************************************************
|
||||
* Macro definitions
|
||||
**********************************************************************************************************************/
|
||||
/** This macro is used to suppress compiler messages about a parameter not being used in a function. */
|
||||
/** Suppress unused-parameter warnings. */
|
||||
#define LFS_UNUSED(p) (void)((p))
|
||||
|
||||
#define STM32WL_PAGE_SIZE (FLASH_PAGE_SIZE)
|
||||
#define STM32WL_PAGE_SIZE (FLASH_PAGE_SIZE) // physical flash erase granularity: 2048 B
|
||||
#define STM32WL_PAGE_COUNT (FLASH_PAGE_NB)
|
||||
#define STM32WL_FLASH_BASE (FLASH_BASE)
|
||||
|
||||
/*
|
||||
* FLASH_SIZE from stm32wle5xx.h will read the actual FLASH size from the chip.
|
||||
* FLASH_END_ADDR is calculated from FLASH_SIZE.
|
||||
* Use the last 28 KiB of the FLASH
|
||||
* LFS tunables — all of these are stored in the LFS superblock.
|
||||
* Changing ANY of them is incompatible with the existing on-disk format;
|
||||
* the filesystem will be detected as corrupted and reformatted on first boot.
|
||||
*
|
||||
* LFS_FLASH_TOTAL_SIZE and LFS_BLOCK_SIZE are the only values to edit here.
|
||||
* All other parameters are derived.
|
||||
*
|
||||
* FLASH_END_ADDR is computed from FLASH_SIZE (read from the chip at link time).
|
||||
*/
|
||||
#define LFS_FLASH_TOTAL_SIZE (14 * 2048) /* needs to be a multiple of LFS_BLOCK_SIZE */
|
||||
#define LFS_BLOCK_SIZE (2048)
|
||||
#define LFS_FLASH_TOTAL_SIZE \
|
||||
(7 * STM32WL_PAGE_SIZE) /* 14 KiB — last 7 physical pages (FORMAT BREAK: reduced from 10 pages / 20 KiB) */
|
||||
#define LFS_BLOCK_SIZE (256) /* virtual block size (FORMAT BREAK if changed) */
|
||||
|
||||
#define LFS_FLASH_ADDR_END (FLASH_END_ADDR)
|
||||
#define LFS_FLASH_ADDR_BASE (LFS_FLASH_ADDR_END - LFS_FLASH_TOTAL_SIZE + 1)
|
||||
|
||||
@@ -51,6 +55,80 @@
|
||||
#define _LFS_DBG(fmt, ...) printf("%s:%d (%s): " fmt "\n", __FILE__, __LINE__, __func__, __VA_ARGS__)
|
||||
#endif
|
||||
|
||||
//--------------------------------------------------------------------+
|
||||
// Write-behind page cache
|
||||
//
|
||||
// LFS requires block_size == erase granularity, but the STM32WL flash
|
||||
// erases in 2048-byte pages. To use smaller virtual LFS blocks we
|
||||
// maintain a single-page RAM cache: the LFS erase/prog callbacks only
|
||||
// update this buffer; the physical erase+reprogram is deferred to
|
||||
// _internal_flash_sync() (or triggered automatically when a different
|
||||
// physical page is addressed).
|
||||
//
|
||||
// This mirrors the approach used by the NRF52 Adafruit driver
|
||||
// (flash_cache.c / flash_nrf5x.c) but adapted for the 2048-byte STM32WL
|
||||
// page size and HAL doubleword-program requirement.
|
||||
//--------------------------------------------------------------------+
|
||||
|
||||
alignas(8) static uint8_t _page_cache[STM32WL_PAGE_SIZE];
|
||||
static uint32_t _page_cache_addr = UINT32_MAX; // UINT32_MAX = no page cached
|
||||
static bool _page_cache_dirty = false;
|
||||
|
||||
/** Flush the cached page to flash (physical erase + doubleword program). */
|
||||
static int _flash_cache_flush(void)
|
||||
{
|
||||
if (!_page_cache_dirty)
|
||||
return LFS_ERR_OK;
|
||||
|
||||
FLASH_EraseInitTypeDef erase = {
|
||||
.TypeErase = FLASH_TYPEERASE_PAGES,
|
||||
.Page = (_page_cache_addr - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE,
|
||||
.NbPages = 1,
|
||||
};
|
||||
uint32_t page_error = 0;
|
||||
|
||||
if (HAL_FLASH_Unlock() != HAL_OK)
|
||||
return LFS_ERR_IO;
|
||||
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS);
|
||||
|
||||
HAL_StatusTypeDef rc = HAL_FLASHEx_Erase(&erase, &page_error);
|
||||
if (rc == HAL_OK) {
|
||||
const uint64_t *p = (const uint64_t *)_page_cache;
|
||||
uint32_t addr = _page_cache_addr;
|
||||
for (size_t i = 0; i < STM32WL_PAGE_SIZE / 8; i++) {
|
||||
rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, *p++);
|
||||
if (rc != HAL_OK)
|
||||
break;
|
||||
addr += 8;
|
||||
}
|
||||
}
|
||||
HAL_StatusTypeDef lock_rc = HAL_FLASH_Lock();
|
||||
if (rc == HAL_OK)
|
||||
rc = lock_rc;
|
||||
|
||||
if (rc == HAL_OK)
|
||||
_page_cache_dirty = false;
|
||||
return rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the physical page containing `page_addr` is loaded into the cache.
|
||||
* If a different dirty page is already cached it is flushed first.
|
||||
*/
|
||||
static int _flash_cache_load(uint32_t page_addr)
|
||||
{
|
||||
if (_page_cache_addr == page_addr)
|
||||
return LFS_ERR_OK; // already cached
|
||||
|
||||
int rc = _flash_cache_flush();
|
||||
if (rc != LFS_ERR_OK)
|
||||
return rc;
|
||||
|
||||
memcpy(_page_cache, (const void *)page_addr, STM32WL_PAGE_SIZE);
|
||||
_page_cache_addr = page_addr;
|
||||
return LFS_ERR_OK;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------+
|
||||
// LFS Disk IO
|
||||
//--------------------------------------------------------------------+
|
||||
@@ -59,111 +137,82 @@ static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, l
|
||||
{
|
||||
LFS_UNUSED(c);
|
||||
|
||||
if (!buffer || !size) {
|
||||
_LFS_DBG("%s Invalid parameter!\r\n", __func__);
|
||||
return LFS_ERR_INVAL;
|
||||
}
|
||||
uint32_t addr = LFS_FLASH_ADDR_BASE + block * LFS_BLOCK_SIZE + off;
|
||||
uint32_t page_addr = addr & ~(uint32_t)(STM32WL_PAGE_SIZE - 1);
|
||||
uint32_t page_off = addr & (uint32_t)(STM32WL_PAGE_SIZE - 1);
|
||||
|
||||
lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off);
|
||||
|
||||
memcpy(buffer, (void *)address, size);
|
||||
if (_page_cache_addr == page_addr)
|
||||
memcpy(buffer, _page_cache + page_off, size);
|
||||
else
|
||||
memcpy(buffer, (const void *)addr, size);
|
||||
|
||||
return LFS_ERR_OK;
|
||||
}
|
||||
|
||||
// Program a region in a block. The block must have previously
|
||||
// been erased. Negative error codes are propogated to the user.
|
||||
// May return LFS_ERR_CORRUPT if the block should be considered bad.
|
||||
// Program a region in a block. The block must have previously been erased.
|
||||
// Writes are accumulated in the page cache and flushed on sync or page eviction.
|
||||
static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size)
|
||||
{
|
||||
lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE + off);
|
||||
HAL_StatusTypeDef hal_rc = HAL_OK;
|
||||
uint32_t dw_count = size / 8;
|
||||
uint64_t *bufp = (uint64_t *)buffer;
|
||||
|
||||
LFS_UNUSED(c);
|
||||
|
||||
_LFS_DBG("Programming %d bytes/%d doublewords at address 0x%08x/block %d, offset %d.", size, dw_count, address, block, off);
|
||||
if (HAL_FLASH_Unlock() != HAL_OK) {
|
||||
return LFS_ERR_IO;
|
||||
}
|
||||
for (uint32_t i = 0; i < dw_count; i++) {
|
||||
if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) {
|
||||
_LFS_DBG("Wanted to program out of bound of FLASH: 0x%08x.\n", address);
|
||||
HAL_FLASH_Lock();
|
||||
return LFS_ERR_INVAL;
|
||||
}
|
||||
hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, address, *bufp);
|
||||
if (hal_rc != HAL_OK) {
|
||||
/* Error occurred while writing data in Flash memory.
|
||||
* User can add here some code to deal with this error.
|
||||
*/
|
||||
_LFS_DBG("Program error at (0x%08x), 0x%X, error: 0x%08x\n", address, hal_rc, HAL_FLASH_GetError());
|
||||
}
|
||||
address += 8;
|
||||
bufp += 1;
|
||||
}
|
||||
if (HAL_FLASH_Lock() != HAL_OK) {
|
||||
return LFS_ERR_IO;
|
||||
}
|
||||
uint32_t addr = LFS_FLASH_ADDR_BASE + block * LFS_BLOCK_SIZE + off;
|
||||
uint32_t page_addr = addr & ~(uint32_t)(STM32WL_PAGE_SIZE - 1);
|
||||
uint32_t page_off = addr & (uint32_t)(STM32WL_PAGE_SIZE - 1);
|
||||
|
||||
return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO
|
||||
int rc = _flash_cache_load(page_addr);
|
||||
if (rc != LFS_ERR_OK)
|
||||
return rc;
|
||||
|
||||
memcpy(_page_cache + page_off, buffer, size);
|
||||
_page_cache_dirty = true;
|
||||
return LFS_ERR_OK;
|
||||
}
|
||||
|
||||
// Erase a block. A block must be erased before being programmed.
|
||||
// The state of an erased block is undefined. Negative error codes
|
||||
// are propogated to the user.
|
||||
// May return LFS_ERR_CORRUPT if the block should be considered bad.
|
||||
// Erase a virtual block. Marks the corresponding region in the page cache as 0xFF.
|
||||
// Physical erase of the containing page is deferred until sync or page eviction.
|
||||
static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block)
|
||||
{
|
||||
lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * STM32WL_PAGE_SIZE);
|
||||
HAL_StatusTypeDef hal_rc;
|
||||
FLASH_EraseInitTypeDef EraseInitStruct = {.TypeErase = FLASH_TYPEERASE_PAGES, .Page = 0, .NbPages = 1};
|
||||
uint32_t PAGEError = 0;
|
||||
|
||||
LFS_UNUSED(c);
|
||||
|
||||
if ((address < LFS_FLASH_ADDR_BASE) || (address > LFS_FLASH_ADDR_END)) {
|
||||
_LFS_DBG("Wanted to erase out of bound of FLASH: 0x%08x.\n", address);
|
||||
return LFS_ERR_INVAL;
|
||||
}
|
||||
/* calculate the absolute page, i.e. what the ST wants */
|
||||
EraseInitStruct.Page = (address - STM32WL_FLASH_BASE) / STM32WL_PAGE_SIZE;
|
||||
_LFS_DBG("Erasing block %d at 0x%08x... ", block, address);
|
||||
HAL_FLASH_Unlock();
|
||||
hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &PAGEError);
|
||||
HAL_FLASH_Lock();
|
||||
uint32_t addr = LFS_FLASH_ADDR_BASE + block * LFS_BLOCK_SIZE;
|
||||
uint32_t page_addr = addr & ~(uint32_t)(STM32WL_PAGE_SIZE - 1);
|
||||
uint32_t page_off = addr & (uint32_t)(STM32WL_PAGE_SIZE - 1);
|
||||
|
||||
return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO; // If HAL_OK, return LFS_ERR_OK, else return LFS_ERR_IO
|
||||
int rc = _flash_cache_load(page_addr);
|
||||
if (rc != LFS_ERR_OK)
|
||||
return rc;
|
||||
|
||||
memset(_page_cache + page_off, 0xFF, LFS_BLOCK_SIZE);
|
||||
_page_cache_dirty = true;
|
||||
return LFS_ERR_OK;
|
||||
}
|
||||
|
||||
// Sync the state of the underlying block device. Negative error codes
|
||||
// are propogated to the user.
|
||||
// Flush the write-behind cache to flash.
|
||||
static int _internal_flash_sync(const struct lfs_config *c)
|
||||
{
|
||||
LFS_UNUSED(c);
|
||||
// write function performs no caching. No need for sync.
|
||||
|
||||
return LFS_ERR_OK;
|
||||
return _flash_cache_flush();
|
||||
}
|
||||
|
||||
static struct lfs_config _InternalFSConfig = {.context = NULL,
|
||||
static struct lfs_config _InternalFSConfig = {
|
||||
.context = NULL,
|
||||
|
||||
.read = _internal_flash_read,
|
||||
.prog = _internal_flash_prog,
|
||||
.erase = _internal_flash_erase,
|
||||
.sync = _internal_flash_sync,
|
||||
.read = _internal_flash_read,
|
||||
.prog = _internal_flash_prog,
|
||||
.erase = _internal_flash_erase,
|
||||
.sync = _internal_flash_sync,
|
||||
|
||||
.read_size = LFS_BLOCK_SIZE,
|
||||
.prog_size = LFS_BLOCK_SIZE,
|
||||
.block_size = LFS_BLOCK_SIZE,
|
||||
.block_count = LFS_FLASH_TOTAL_SIZE / LFS_BLOCK_SIZE,
|
||||
.lookahead = 128,
|
||||
.read_size = LFS_BLOCK_SIZE,
|
||||
.prog_size = LFS_BLOCK_SIZE,
|
||||
.block_size = LFS_BLOCK_SIZE,
|
||||
.block_count = LFS_FLASH_TOTAL_SIZE / LFS_BLOCK_SIZE,
|
||||
.lookahead = 128,
|
||||
|
||||
.read_buffer = NULL,
|
||||
.prog_buffer = NULL,
|
||||
.lookahead_buffer = NULL,
|
||||
.file_buffer = NULL};
|
||||
.read_buffer = NULL,
|
||||
.prog_buffer = NULL,
|
||||
.lookahead_buffer = NULL,
|
||||
.file_buffer = NULL,
|
||||
};
|
||||
|
||||
LittleFS InternalFS;
|
||||
|
||||
@@ -179,17 +228,17 @@ bool LittleFS::begin(void)
|
||||
/* There is not enough space on this device for a filesystem. */
|
||||
return false;
|
||||
}
|
||||
// failed to mount, erase all pages then format and mount again
|
||||
// failed to mount, erase all virtual blocks then format and mount again
|
||||
if (!STM32_LittleFS::begin()) {
|
||||
// Erase all pages of internal flash region for Filesystem.
|
||||
for (uint32_t addr = LFS_FLASH_ADDR_BASE; addr < (LFS_FLASH_ADDR_END + 1); addr += STM32WL_PAGE_SIZE) {
|
||||
_internal_flash_erase(&_InternalFSConfig, (addr - LFS_FLASH_ADDR_BASE) / STM32WL_PAGE_SIZE);
|
||||
for (lfs_block_t block = 0; block < _InternalFSConfig.block_count; block++) {
|
||||
_internal_flash_erase(&_InternalFSConfig, block);
|
||||
}
|
||||
_flash_cache_flush(); // flush the last cached page
|
||||
|
||||
// lfs format
|
||||
this->format();
|
||||
|
||||
// mount again if still failed, give up
|
||||
// mount again; if still failed, give up
|
||||
if (!STM32_LittleFS::begin())
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "LittleFS.h"
|
||||
#include "STM32_LittleFS.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
@@ -391,3 +392,8 @@ void File::rewindDirectory(void)
|
||||
}
|
||||
_fs->_unlockFS();
|
||||
}
|
||||
|
||||
// Default constructor — binds to the global InternalFS instance.
|
||||
// Allows File to be declared without an explicit filesystem argument,
|
||||
// matching the API of ESP32/RP2040/Portduino File objects.
|
||||
File::File() : File(InternalFS) {}
|
||||
|
||||
@@ -44,6 +44,7 @@ class File : public Stream
|
||||
public:
|
||||
explicit File(STM32_LittleFS &fs);
|
||||
File(char const *filename, uint8_t mode, STM32_LittleFS &fs);
|
||||
File(); // default-constructs against InternalFS; defined in STM32_LittleFS_File.cpp
|
||||
|
||||
public:
|
||||
bool open(char const *filename, uint8_t mode);
|
||||
|
||||
+4
-7
@@ -4,8 +4,10 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// "legacy adc calibration driver is deprecated, please migrate to use esp_adc/adc_cali.h and esp_adc/adc_cali_scheme.h
|
||||
#include <esp_adc_cal.h>
|
||||
// #include <driver/adc.h>
|
||||
#include <esp_adc/adc_cali.h>
|
||||
#include <esp_adc/adc_cali_scheme.h>
|
||||
#include <esp_adc/adc_oneshot.h>
|
||||
#include <soc/adc_channel.h>
|
||||
#endif
|
||||
|
||||
@@ -28,11 +30,6 @@
|
||||
#define NUM_CELLS 1
|
||||
#endif
|
||||
|
||||
#ifdef BAT_MEASURE_ADC_UNIT
|
||||
extern RTC_NOINIT_ATTR uint64_t RTC_reg_b;
|
||||
#include "soc/sens_reg.h" // needed for adc pin reset
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
#include "modules/Telemetry/Sensor/nullSensor.h"
|
||||
#if __has_include(<Adafruit_INA219.h>)
|
||||
|
||||
+32
-14
@@ -18,8 +18,6 @@
|
||||
#include "target_specific.h"
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// "esp_pm_config_esp32_t is deprecated, please include esp_pm.h and use esp_pm_config_t instead"
|
||||
#include "esp32/pm.h"
|
||||
#include "esp_pm.h"
|
||||
#if HAS_WIFI
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
@@ -146,15 +144,31 @@ void initDeepSleep()
|
||||
// If we booted because our timer ran out or the user pressed reset, send those as fake events
|
||||
RESET_REASON hwReason = rtc_get_reset_reason(0);
|
||||
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32P4
|
||||
if (hwReason == BROWN_OUT_RESET)
|
||||
reason = "brownout";
|
||||
else if (hwReason == HP_CORE_HP_WDT_RESET)
|
||||
reason = "taskWatchdog";
|
||||
else if (hwReason == HP_CORE_LP_WDT_RESET)
|
||||
reason = "intWatchdog";
|
||||
else if (hwReason == CHIP_LP_WDT_RESET)
|
||||
reason = "chipWatchdog";
|
||||
else if (hwReason == SUPER_WDT_RESET)
|
||||
reason = "superWatchdog";
|
||||
else if (hwReason == HP_SYS_HP_WDT_RESET)
|
||||
reason = "systemWatchdog";
|
||||
else if (hwReason == HP_SYS_LP_WDT_RESET)
|
||||
reason = "systemLowPowerWatchdog";
|
||||
#else
|
||||
if (hwReason == RTCWDT_BROWN_OUT_RESET)
|
||||
reason = "brownout";
|
||||
|
||||
if (hwReason == TG0WDT_SYS_RESET)
|
||||
else if (hwReason == RTCWDT_RTC_RESET)
|
||||
reason = "rtcWatchdog";
|
||||
else if (hwReason == TG0WDT_SYS_RESET)
|
||||
reason = "taskWatchdog";
|
||||
|
||||
if (hwReason == TG1WDT_SYS_RESET)
|
||||
else if (hwReason == TG1WDT_SYS_RESET)
|
||||
reason = "intWatchdog";
|
||||
|
||||
#endif
|
||||
LOG_INFO("Booted, wake cause %d (boot count %d), reset_reason=%s", wakeCause, bootCount, reason);
|
||||
#endif
|
||||
|
||||
@@ -220,8 +234,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
||||
|
||||
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
// Full shutdown of bluetooth hardware
|
||||
if (nimbleBluetooth)
|
||||
nimbleBluetooth->deinit();
|
||||
if (bluetoothApi)
|
||||
bluetoothApi->deinit();
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -397,8 +411,10 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
|
||||
|
||||
// NOTE! ESP docs say we must disable bluetooth and wifi before light sleep
|
||||
|
||||
#if SOC_PM_SUPPORT_RTC_PERIPH_PD
|
||||
// We want RTC peripherals to stay on
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
#endif
|
||||
|
||||
#if defined(BUTTON_PIN) && defined(BUTTON_NEED_PULLUP)
|
||||
gpio_pullup_en((gpio_num_t)BUTTON_PIN);
|
||||
@@ -532,11 +548,7 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
|
||||
*/
|
||||
void enableModemSleep()
|
||||
{
|
||||
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
|
||||
static esp_pm_config_t esp32_config; // filled with zeros because bss
|
||||
#else
|
||||
static esp_pm_config_esp32_t esp32_config; // filled with zeros because bss
|
||||
#endif
|
||||
#if CONFIG_IDF_TARGET_ESP32S3
|
||||
esp32_config.max_freq_mhz = CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ;
|
||||
#elif CONFIG_IDF_TARGET_ESP32S2
|
||||
@@ -545,6 +557,12 @@ void enableModemSleep()
|
||||
esp32_config.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ;
|
||||
#elif CONFIG_IDF_TARGET_ESP32C3
|
||||
esp32_config.max_freq_mhz = CONFIG_ESP32C3_DEFAULT_CPU_FREQ_MHZ;
|
||||
#elif CONFIG_IDF_TARGET_ESP32P4
|
||||
#if CONFIG_ESP32P4_REV_MIN_FULL < 300
|
||||
esp32_config.max_freq_mhz = 360;
|
||||
#else
|
||||
esp32_config.max_freq_mhz = 400;
|
||||
#endif
|
||||
#else
|
||||
esp32_config.max_freq_mhz = CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ;
|
||||
#endif
|
||||
@@ -562,8 +580,8 @@ bool shouldLoraWake(uint32_t msecToWake)
|
||||
|
||||
void enableLoraInterrupt()
|
||||
{
|
||||
esp_err_t res;
|
||||
#if SOC_PM_SUPPORT_EXT_WAKEUP && defined(LORA_DIO1) && (LORA_DIO1 != RADIOLIB_NC)
|
||||
esp_err_t res;
|
||||
res = gpio_pulldown_en((gpio_num_t)LORA_DIO1);
|
||||
if (res != ESP_OK) {
|
||||
LOG_ERROR("gpio_pulldown_en(LORA_DIO1) result %d", res);
|
||||
|
||||
@@ -61,11 +61,7 @@ class XModemAdapter
|
||||
|
||||
uint16_t packetno = 0;
|
||||
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_STM32WL)
|
||||
File file = File(FSCom);
|
||||
#else
|
||||
File file;
|
||||
#endif
|
||||
|
||||
char filename[sizeof(meshtastic_XModem_buffer_t::bytes)] = {0};
|
||||
|
||||
|
||||
@@ -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() {}
|
||||
@@ -70,7 +70,7 @@
|
||||
// Battery
|
||||
|
||||
#define BATTERY_PIN 34 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO34_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_6
|
||||
#define ADC_ATTENUATION \
|
||||
ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32
|
||||
// ESP32-S2/C3/S3 are different
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
// Battery sense
|
||||
#define BATTERY_PIN 35
|
||||
#define ADC_MULTIPLIER 2.01 // 100k + 100k, and add 1% tolerance
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS ADC_RESOLUTION
|
||||
|
||||
// SPI
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define I2C_SDA 4
|
||||
#define I2C_SCL 5
|
||||
#define BATTERY_PIN 34
|
||||
#define ADC_CHANNEL ADC1_GPIO34_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_6
|
||||
|
||||
// GPS
|
||||
#undef GPS_RX_PIN
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// Note: On the ESP32 base version, gpio34-39 are input-only, and do not have internal pull-ups.
|
||||
// If 39 is not being used for a button, it is suggested to remove the #define.
|
||||
#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k)
|
||||
#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards
|
||||
#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975).
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam
|
||||
#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k)
|
||||
#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards
|
||||
#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975).
|
||||
|
||||
+257
-15
@@ -4,8 +4,9 @@ extends = arduino_base
|
||||
custom_esp32_kind =
|
||||
custom_mtjson_part =
|
||||
platform =
|
||||
# renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32
|
||||
platformio/espressif32@6.13.0
|
||||
# TODO renovate
|
||||
https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
|
||||
; https://github.com/pioarduino/platform-espressif32.git#develop
|
||||
platform_packages =
|
||||
# renovate: datasource=custom.pio depName=platformio/tool-mklittlefs packageName=platformio/tool/tool-mklittlefs
|
||||
platformio/tool-mklittlefs@1.203.210628
|
||||
@@ -35,20 +36,18 @@ build_unflags =
|
||||
-std=gnu++11
|
||||
build_flags =
|
||||
${arduino_base.build_flags}
|
||||
-flto
|
||||
-Wall
|
||||
-Wextra
|
||||
-Isrc/platform/esp32
|
||||
-std=gnu++17
|
||||
-DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG
|
||||
# DO NOT INCREASE THIS TO DEBUG. It appears to trigger a bug in ESP-IDF
|
||||
# Bluetooth stack with Pixel 8 Android devices:
|
||||
# https://github.com/espressif/esp-idf/issues/18126#issuecomment-4286197744
|
||||
# Once the bug is resolved, we can remove this warning.
|
||||
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_INFO
|
||||
-DMYNEWT_VAL_BLE_HS_LOG_LVL=LOG_LEVEL_CRITICAL
|
||||
-DAXP_DEBUG_PORT=Serial
|
||||
-DCONFIG_BT_NIMBLE_ENABLED
|
||||
-DCONFIG_BT_NIMBLE_MAX_BONDS=6 # default is 3
|
||||
-DCONFIG_NIMBLE_CPP_LOG_LEVEL=2
|
||||
-DCONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||
-DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
|
||||
-DESP_OPENSSL_SUPPRESS_LEGACY_WARNING
|
||||
-DSERIAL_BUFFER_SIZE=4096
|
||||
-DSERIAL_HAS_ON_RECEIVE
|
||||
@@ -62,16 +61,13 @@ build_flags =
|
||||
lib_deps =
|
||||
${arduino_base.lib_deps}
|
||||
${networking_base.lib_deps}
|
||||
${networking_extra.lib_deps}
|
||||
${environmental_base.lib_deps}
|
||||
${environmental_extra.lib_deps}
|
||||
${radiolib_base.lib_deps}
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp32_https_server packageName=https://github.com/meshtastic/esp32_https_server gitBranch=master
|
||||
https://github.com/meshtastic/esp32_https_server/archive/0c71f380390ad483ff134ad938e07f6cf1226c5b.zip
|
||||
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
|
||||
h2zero/NimBLE-Arduino@1.4.3
|
||||
# renovate: datasource=git-refs depName=libpax packageName=https://github.com/dbinfrago/libpax gitBranch=master
|
||||
https://github.com/dbinfrago/libpax/archive/3cdc0371c375676a97967547f4065607d4c53fd1.zip
|
||||
# TODO renovate
|
||||
https://github.com/mverch67/libpax/archive/6f52ee989301cdabaeef00bcbf93bff55708ce2f.zip
|
||||
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
|
||||
lewisxhe/XPowersLib@0.3.3
|
||||
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
|
||||
@@ -79,7 +75,30 @@ lib_deps =
|
||||
|
||||
lib_ignore =
|
||||
segger_rtt
|
||||
ESP32 BLE Arduino
|
||||
; ESP32 BLE Arduino
|
||||
; BLE
|
||||
; Ignore select Bluetooth libs
|
||||
NimBLE-Arduino
|
||||
BluetoothSerial
|
||||
SimpleBLE
|
||||
WiFiProv
|
||||
ArduinoOTA
|
||||
; Ignore pioarduino esp32 libs we don't use
|
||||
ESP_I2S
|
||||
ESP_NOW
|
||||
ESP_SR
|
||||
Insights
|
||||
Matter
|
||||
OpenThread
|
||||
RainMaker
|
||||
;SPIFFS
|
||||
USB
|
||||
;NetworkClientSecure
|
||||
Zigbee
|
||||
Micro-RTSP
|
||||
; testing below
|
||||
mqtt
|
||||
esp-mqtt
|
||||
|
||||
; leave this commented out to avoid breaking Windows
|
||||
;upload_port = /dev/ttyUSB0
|
||||
@@ -92,3 +111,226 @@ lib_ignore =
|
||||
; customize the partition table
|
||||
; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables
|
||||
board_build.partitions = partition-table.csv
|
||||
|
||||
custom_component_remove =
|
||||
espressif/esp_hosted
|
||||
espressif/esp_wifi_remote
|
||||
espressif/esp_modem
|
||||
espressif/esp-dsp
|
||||
espressif/esp32-camera
|
||||
espressif/libsodium
|
||||
espressif/esp-modbus
|
||||
espressif/qrcode
|
||||
espressif/esp_insights
|
||||
espressif/esp_diag_data_store
|
||||
espressif/esp_diagnostics
|
||||
espressif/esp_rainmaker
|
||||
espressif/rmaker_common
|
||||
espressif/network_provisioning
|
||||
chmorgan/esp-libhelix-mp3
|
||||
espressif/esp-tflite-micro
|
||||
espressif/esp-sr
|
||||
espressif/esp_matter
|
||||
espressif/esp-zboss-lib
|
||||
espressif/esp-zigbee-lib
|
||||
espressif/mqtt
|
||||
|
||||
custom_sdkconfig =
|
||||
; CONFIG_LOG_DEFAULT_LEVEL=4
|
||||
; CONFIG_LOG_MAXIMUM_LEVEL=4
|
||||
'# CONFIG_BT_NIMBLE_LOG_LEVEL_INFO is not set'
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_ERROR=y
|
||||
CONFIG_LOG_COLORS=y
|
||||
CONFIG_ARDUHAL_LOG_COLORS=y
|
||||
CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y
|
||||
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
|
||||
;
|
||||
; Compiler options
|
||||
;
|
||||
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=n
|
||||
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y
|
||||
CONFIG_COMPILER_CXX_EXCEPTIONS=n
|
||||
CONFIG_COMPILER_STACK_CHECK_MODE_NORM=n
|
||||
CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y
|
||||
CONFIG_COMPILER_DISABLE_GCC12_WARNINGS=y
|
||||
CONFIG_COMPILER_DISABLE_GCC13_WARNINGS=y
|
||||
CONFIG_COMPILER_DISABLE_GCC14_WARNINGS=y
|
||||
CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=n
|
||||
CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE=y
|
||||
CONFIG_ESP_GDBSTUB_ENABLED=n
|
||||
CONFIG_ESP_TASK_WDT_INIT=n
|
||||
CONFIG_NEWLIB_NANO_FORMAT=y
|
||||
; LIBC_NEWLIB_NANO_FORMAT is enabled via 'esp32_extra.py' to allow float support
|
||||
CONFIG_ESP_COREDUMP_ENABLE=n
|
||||
CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=n
|
||||
CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y
|
||||
CONFIG_ESP32_ENABLE_COREDUMP=n
|
||||
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=n
|
||||
CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=n
|
||||
CONFIG_FREERTOS_USE_TRACE_FACILITY=n
|
||||
CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y
|
||||
CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH=y
|
||||
CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH=y
|
||||
CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM=y
|
||||
CONFIG_ESP_WIFI_IRAM_OPT=n
|
||||
CONFIG_ESP32_WIFI_RX_IRAM_OPT=n
|
||||
CONFIG_SPIRAM_CACHE_LIBCHAR_IN_IRAM=n
|
||||
CONFIG_SPIRAM_CACHE_LIBSTR_IN_IRAM=n
|
||||
CONFIG_SPIRAM_CACHE_LIBMISC_IN_IRAM=n
|
||||
CONFIG_SPIRAM_CACHE_LIBTIME_IN_IRAM=n
|
||||
CONFIG_UNITY_ENABLE_FLOAT=n
|
||||
CONFIG_UNITY_ENABLE_DOUBLE=n
|
||||
CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=n
|
||||
CONFIG_SUPPORT_TERMIOS=n
|
||||
CONFIG_VFS_SUPPORT_TERMIOS=n
|
||||
CONFIG_VFS_SUPPORT_SELECT=n
|
||||
CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=n
|
||||
CONFIG_WS_TRANSPORT=n
|
||||
CONFIG_PPP_SUPPORT=n
|
||||
CONFIG_NETWORK_PROV_NETWORK_TYPE_WIFI=n
|
||||
; CONFIG_LWIP_DHCPS=n
|
||||
CONFIG_LWIP_PPP_SUPPORT=n
|
||||
CONFIG_LWIP_IP_FORWARD=n
|
||||
CONFIG_LWIP_IPV4_NAPT=n
|
||||
;
|
||||
; MBEDTLS
|
||||
;
|
||||
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=8192
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y
|
||||
; Switch to custom CA bundle (for Meshtastic MQTT/etc) in the future
|
||||
; https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/kconfig-reference.html#certificate-bundle-configuration
|
||||
; CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y
|
||||
; CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH=""
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_MAX_CERTS=1
|
||||
; #shame
|
||||
CONFIG_MBEDTLS_ALLOW_WEAK_CERTIFICATE_VERIFICATION=y
|
||||
CONFIG_MBEDTLS_SSL_RENEGOTIATION=n
|
||||
CONFIG_MBEDTLS_SSL_PROTO_DTLS=n
|
||||
CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=n
|
||||
CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=n
|
||||
CONFIG_MBEDTLS_PKCS7_C=n
|
||||
CONFIG_MBEDTLS_CAMELLIA_C=n
|
||||
CONFIG_MBEDTLS_CCM_C=n
|
||||
CONFIG_MBEDTLS_CMAC_C=n
|
||||
CONFIG_MBEDTLS_SHA512_C=n
|
||||
CONFIG_MBEDTLS_X509_CRL_PARSE_C=n
|
||||
CONFIG_MBEDTLS_X509_CSR_PARSE_C=n
|
||||
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_PSK=n
|
||||
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=n
|
||||
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=n
|
||||
CONFIG_MBEDTLS_KEY_EXCHANGE_ECJPAKE=n
|
||||
CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=n
|
||||
CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=n
|
||||
CONFIG_MDNS_ENABLE_CONSOLE_CLI=n
|
||||
CONFIG_MDNS_PREDEF_NETIF_AP=n
|
||||
; CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n
|
||||
CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=n
|
||||
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=n
|
||||
CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=n
|
||||
CONFIG_ESP_WIFI_ENABLE_SAE_H2E=n
|
||||
CONFIG_OPENTHREAD_ENABLED=n
|
||||
CONFIG_ZB_ENABLED=n
|
||||
CONFIG_IEEE802154_ENABLED=n
|
||||
CONFIG_ESP_INSIGHTS_ENABLED=n
|
||||
CONFIG_HTTPD_WS_SUPPORT=n
|
||||
CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=n
|
||||
CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=n
|
||||
; using esp-idf5-https_server library instead, for now?
|
||||
CONFIG_ESP_HTTPS_SERVER_ENABLE=n
|
||||
;
|
||||
; Builtin ESP-MQTT, we don't use this (yet?)
|
||||
;
|
||||
CONFIG_MQTT_TRANSPORT_SSL=n
|
||||
CONFIG_MQTT_TRANSPORT_WEBSOCKET=n
|
||||
CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=n
|
||||
CONFIG_MQTT_PROTOCOL_311=n
|
||||
;
|
||||
; Ethernet
|
||||
; Disabled in esp32_common, re-enable on variants that need it
|
||||
;
|
||||
CONFIG_ETH_ENABLED=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Ethernet=n
|
||||
;
|
||||
; Bluetooth
|
||||
;
|
||||
CONFIG_BLE_MESH=n
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_NVS_PERSIST=y
|
||||
CONFIG_BT_NIMBLE_ROLE_CENTRAL=n
|
||||
CONFIG_BT_NIMBLE_ROLE_OBSERVER=n
|
||||
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=8192
|
||||
CONFIG_BT_NIMBLE_MAX_CCCDS=20
|
||||
CONFIG_BT_NIMBLE_MAX_BONDS=6
|
||||
CONFIG_BT_NIMBLE_ENABLE_PERIODIC_SYNC=n
|
||||
CONFIG_BT_NIMBLE_ENABLE_PERIODIC_ADV=n
|
||||
CONFIG_BT_NIMBLE_EXT_SCAN=n
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=n
|
||||
CONFIG_BT_NIMBLE_PERIODIC_ADV_SYNC_TRANSFER=n
|
||||
CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_2M_PHY=n
|
||||
CONFIG_BT_NIMBLE_BLUFI_ENABLE=n
|
||||
CONFIG_BT_NIMBLE_PROX_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_ANS_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_CTS_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_HTP_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_IPSS_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_TPS_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_IAS_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_LLS_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_SPS_SERVICE=y
|
||||
CONFIG_BT_NIMBLE_HR_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_HID_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_BAS_SERVICE=n
|
||||
CONFIG_BT_NIMBLE_DIS_SERVICE=n
|
||||
;
|
||||
; Arduino selective compilation
|
||||
; Disable unused Arduino libraries to save space
|
||||
;
|
||||
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
|
||||
CONFIG_ARDUINO_SELECTIVE_SPI=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Wire=y
|
||||
CONFIG_ARDUINO_SELECTIVE_ESP_SR=n
|
||||
CONFIG_ARDUINO_SELECTIVE_EEPROM=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Preferences=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Ticker=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Update=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Zigbee=n
|
||||
CONFIG_ARDUINO_SELECTIVE_FS=y
|
||||
CONFIG_ARDUINO_SELECTIVE_SD=y
|
||||
CONFIG_ARDUINO_SELECTIVE_SD_MMC=y
|
||||
CONFIG_ARDUINO_SELECTIVE_SPIFFS=y
|
||||
CONFIG_ARDUINO_SELECTIVE_FFat=n
|
||||
CONFIG_ARDUINO_SELECTIVE_LittleFS=y
|
||||
CONFIG_ARDUINO_SELECTIVE_Network=y
|
||||
; CONFIG_ARDUINO_SELECTIVE_Ethernet=n
|
||||
CONFIG_ARDUINO_SELECTIVE_PPP=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Hash=y
|
||||
CONFIG_ARDUINO_SELECTIVE_ArduinoOTA=n
|
||||
CONFIG_ARDUINO_SELECTIVE_AsyncUDP=y
|
||||
CONFIG_ARDUINO_SELECTIVE_DNSServer=n
|
||||
CONFIG_ARDUINO_SELECTIVE_ESPmDNS=y
|
||||
CONFIG_ARDUINO_SELECTIVE_HTTPClient=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Matter=n
|
||||
CONFIG_ARDUINO_SELECTIVE_NetBIOS=n
|
||||
CONFIG_ARDUINO_SELECTIVE_WebServer=n
|
||||
CONFIG_ARDUINO_SELECTIVE_WiFi=y
|
||||
CONFIG_ARDUINO_SELECTIVE_NetworkClientSecure=y
|
||||
CONFIG_ARDUINO_SELECTIVE_WiFiProv=n
|
||||
CONFIG_ARDUINO_SELECTIVE_BLE=y
|
||||
CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n
|
||||
CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n
|
||||
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
|
||||
CONFIG_ARDUINO_SELECTIVE_OpenThread=n
|
||||
CONFIG_ARDUINO_SELECTIVE_Insights=n
|
||||
|
||||
@@ -11,12 +11,39 @@ build_src_filter =
|
||||
|
||||
build_flags =
|
||||
${esp32_common.build_flags}
|
||||
-mtext-section-literals
|
||||
-D ESP32_FORCE_IRAM_MEMSET
|
||||
-Wl,--wrap=memset
|
||||
-Wl,--wrap=memcpy
|
||||
-DMESHTASTIC_EXCLUDE_AUDIO=1
|
||||
-DMESHTASTIC_EXCLUDE_ACCELEROMETER=1
|
||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||
-DMESHTASTIC_EXCLUDE_WEBSERVER=1
|
||||
-DMESHTASTIC_EXCLUDE_RANGETEST=1
|
||||
; HACK HACK HACK this is just a proof of concept ESP32+NimBLE but these
|
||||
; includes need to be done properly somehow
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/nimble/host/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/nimble/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/porting/nimble/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/port/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/porting/npl/freertos/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/nimble/transport/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/nimble/host/services/gap/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/esp-hci/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/nimble/host/services/gatt/include
|
||||
-I${platformio.packages_dir}/framework-espidf/components/bt/host/nimble/nimble/nimble/host/util/include
|
||||
|
||||
custom_sdkconfig =
|
||||
${esp32_common.custom_sdkconfig}
|
||||
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
|
||||
'# CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY is not set'
|
||||
'# CONFIG_BTDM_CTRL_MODE_BTDM is not set'
|
||||
'# CONFIG_BT_BLUEDROID_ENABLED is not set'
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP=y
|
||||
|
||||
; Override lib_deps to use environmental_extra_no_bsec instead of environmental_extra
|
||||
; BSEC library uses ~3.5KB DRAM which causes overflow on original ESP32 targets
|
||||
lib_deps =
|
||||
${arduino_base.lib_deps}
|
||||
${networking_base.lib_deps}
|
||||
@@ -24,8 +51,8 @@ lib_deps =
|
||||
${radiolib_base.lib_deps}
|
||||
${environmental_base.lib_deps}
|
||||
${environmental_extra_no_bsec.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=NimBLE-Arduino packageName=h2zero/library/NimBLE-Arduino
|
||||
h2zero/NimBLE-Arduino@1.4.3
|
||||
# TODO renovate
|
||||
https://github.com/mverch67/libpax/archive/6f52ee989301cdabaeef00bcbf93bff55708ce2f.zip
|
||||
# renovate: datasource=custom.pio depName=XPowersLib packageName=lewisxhe/library/XPowersLib
|
||||
lewisxhe/XPowersLib@0.3.3
|
||||
# renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#define ADC_CTRL 21
|
||||
#define ADC_CTRL_ENABLED LOW
|
||||
#define BATTERY_PIN 37 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_CHANNEL_1
|
||||
#define ADC_CHANNEL ADC_CHANNEL_1
|
||||
// ratio of voltage divider = 3.20 (R1=100k, R2=220k)
|
||||
#define ADC_MULTIPLIER 3.2
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
#define PIN_EINK_MOSI 23 // EPD_MOSI
|
||||
|
||||
#define BATTERY_PIN 35
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
// https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/schematic/Core/m5paper/M5_PAPER_SCH.pdf
|
||||
// https://github.com/m5stack/M5Core-Ink/blob/master/examples/Basics/FactoryTest/FactoryTest.ino#L58
|
||||
// VBAT
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#endif
|
||||
|
||||
#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
#define BATTERY_SENSE_SAMPLES 15 // Set the number of samples, It has an effect of increasing sensitivity.
|
||||
#define ADC_MULTIPLIER 2
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ build_flags =
|
||||
-DVTABLES_IN_FLASH=1
|
||||
-DCONFIG_DISABLE_HAL_LOCKS=1
|
||||
-DHAS_STK8XXX=1
|
||||
-O2
|
||||
; -O2
|
||||
-I variants/esp32/radiomaster_900_bandit
|
||||
board_build.f_cpu = 240000000L
|
||||
upload_protocol = esptool
|
||||
|
||||
@@ -47,7 +47,7 @@ static const uint8_t SCK = 33;
|
||||
|
||||
#define PIN_VBAT WB_A0
|
||||
#define BATTERY_PIN PIN_VBAT
|
||||
#define ADC_CHANNEL ADC1_GPIO36_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_0
|
||||
|
||||
// https://docs.rakwireless.com/Product-Categories/WisBlock/RAK13300/
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#endif
|
||||
|
||||
#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
#define BATTERY_SENSE_SAMPLES 30 // Set the number of samples, It has an effect of increasing sensitivity.
|
||||
#define ADC_MULTIPLIER 6.45
|
||||
#define CELL_TYPE_LION // same curve for liion/lipo
|
||||
|
||||
@@ -16,6 +16,7 @@ board_check = true
|
||||
build_flags = ${esp32_base.build_flags}
|
||||
-D TBEAM_V10
|
||||
-I variants/esp32/tbeam
|
||||
|
||||
upload_speed = 921600
|
||||
|
||||
[env:tbeam-displayshield]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#define BUTTON_PIN 39
|
||||
#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module.
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
|
||||
#define USE_RF95
|
||||
#define LORA_DIO0 26 // a No connect on the SX1262 module
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
|
||||
#define I2C_SDA 21 // I2C pins for this board
|
||||
#define I2C_SCL 22
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
|
||||
#define I2C_SDA 21 // I2C pins for this board
|
||||
#define I2C_SCL 22
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define BATTERY_PIN 35
|
||||
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
|
||||
#define ADC_CHANNEL ADC_CHANNEL_7
|
||||
#define BATTERY_SENSE_SAMPLES 30
|
||||
|
||||
// ratio of voltage divider = 2.0 (R42=100k, R43=100k)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user