diff --git a/.github/workflows/build_debian_src.yml b/.github/workflows/build_debian_src.yml
new file mode 100644
index 000000000..b2fcb5262
--- /dev/null
+++ b/.github/workflows/build_debian_src.yml
@@ -0,0 +1,66 @@
+name: Build Debian Source Package
+
+on:
+ workflow_call:
+ secrets:
+ PPA_GPG_PRIVATE_KEY:
+ required: true
+ inputs:
+ series:
+ description: Ubuntu series to target
+ required: true
+ type: string
+
+permissions:
+ contents: write
+ packages: write
+
+jobs:
+ build-debian-src:
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ path: meshtasticd
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+
+ - name: Install deps
+ shell: bash
+ working-directory: meshtasticd
+ run: |
+ 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 mk-build-deps --install --remove --tool='apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends --yes' debian/control
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v6
+ with:
+ gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
+ id: gpg
+
+ - name: Get release version string
+ working-directory: meshtasticd
+ run: |
+ echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
+ id: version
+
+ - name: Fetch libdeps, package debian source
+ working-directory: meshtasticd
+ run: debian/ci_pack_sdeb.sh
+ env:
+ SERIES: ${{ inputs.series }}
+ GPG_KEY_ID: ${{ steps.gpg.outputs.keyid }}
+ PKG_VERSION: ${{ steps.version.outputs.deb }}
+
+ - name: Store binaries as an artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
+ overwrite: true
+ path: |
+ meshtasticd_${{ steps.version.outputs.deb }}*
diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml
index a437411b5..267e34a94 100644
--- a/.github/workflows/main_matrix.yml
+++ b/.github/workflows/main_matrix.yml
@@ -332,12 +332,17 @@ jobs:
run: >-
bin/bump_version.py
+ - name: Update debian changelog
+ run: >-
+ debian/ci_changelog.sh
+
- name: Create version.properties pull request
uses: peter-evans/create-pull-request@v7
with:
title: Bump version.properties
add-paths: |
version.properties
+ debian/changelog
release-firmware:
strategy:
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index e249823a7..b7cf4bfc6 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -17,3 +17,13 @@ jobs:
uses: trunk-io/trunk-action@782e83f803ca6e369f035d64c6ba2768174ba61b
with:
trunk-token: ${{ secrets.TRUNK_TOKEN }}
+ package-ppa:
+ strategy:
+ fail-fast: false
+ matrix:
+ series: [plucky, oracular, noble, jammy]
+ uses: ./.github/workflows/package_ppa.yml
+ with:
+ ppa_repo: daily
+ series: ${{ matrix.series }}
+ secrets: inherit
diff --git a/.github/workflows/package_ppa.yml b/.github/workflows/package_ppa.yml
new file mode 100644
index 000000000..5705c6d49
--- /dev/null
+++ b/.github/workflows/package_ppa.yml
@@ -0,0 +1,72 @@
+name: Package Launchpad PPA
+
+on:
+ workflow_call:
+ secrets:
+ PPA_GPG_PRIVATE_KEY:
+ required: true
+ inputs:
+ ppa_repo:
+ description: Meshtastic PPA to target
+ required: true
+ type: string
+ series:
+ description: Ubuntu series to target
+ required: true
+ type: string
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ packages: write
+
+jobs:
+ build-debian-src:
+ uses: ./.github/workflows/build_debian_src.yml
+ secrets: inherit
+ with:
+ series: ${{ inputs.series }}
+
+ package-ppa:
+ runs-on: ubuntu-24.04
+ needs: build-debian-src
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ path: meshtasticd
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+
+ - name: Install deps
+ shell: bash
+ run: |
+ sudo apt-get update -y --fix-missing
+ sudo apt-get install -y dput
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v6
+ with:
+ gpg_private_key: ${{ secrets.PPA_GPG_PRIVATE_KEY }}
+ id: gpg
+
+ - name: Get release version string
+ working-directory: meshtasticd
+ run: |
+ echo "deb=$(./bin/buildinfo.py deb)" >> $GITHUB_OUTPUT
+ id: version
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: firmware-debian-${{ steps.version.outputs.deb }}~${{ inputs.series }}-src
+ merge-multiple: true
+
+ - name: Display structure of downloaded files
+ run: ls -lah
+
+ - name: Publish with dput
+ if: ${{ github.event_name != 'pull_request_target' && github.event_name != 'pull_request' }}
+ run: |
+ dput ppa:meshtastic/${{ inputs.ppa_repo }} meshtasticd_${{ steps.version.outputs.deb }}~${{ inputs.series }}_source.changes
diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml
new file mode 100644
index 000000000..d572568de
--- /dev/null
+++ b/.github/workflows/release_channels.yml
@@ -0,0 +1,20 @@
+name: Trigger release workflows upon Publish
+
+on:
+ release:
+ types: [published]
+
+permissions: read-all
+
+jobs:
+ package-ppa:
+ strategy:
+ fail-fast: false
+ matrix:
+ series: [plucky, oracular, noble, jammy]
+ uses: ./.github/workflows/package_ppa.yml
+ with:
+ ppa_repo: |-
+ ${{ contains(github.event.release.name, 'Beta') && 'beta' || contains(github.event.release.name, 'Alpha') && 'alpha' }}
+ series: ${{ matrix.series }}
+ secrets: inherit
diff --git a/.gitignore b/.gitignore
index 28f9a24cc..803aee139 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,8 @@
.pio
+pio
+pio.tar
+web
+web.tar
# ignore vscode IDE settings files
.vscode/*
@@ -30,4 +34,4 @@ release/
.vscode/extensions.json
/compile_commands.json
src/mesh/raspihttp/certificate.pem
-src/mesh/raspihttp/private_key.pem
+src/mesh/raspihttp/private_key.pem
\ No newline at end of file
diff --git a/arch/rp2xx0/rp2350.ini b/arch/rp2xx0/rp2350.ini
index c5849ff2a..ab16e24b4 100644
--- a/arch/rp2xx0/rp2350.ini
+++ b/arch/rp2xx0/rp2350.ini
@@ -7,12 +7,12 @@ platform_packages = framework-arduinopico@https://github.com/earlephilhower/ardu
board_build.core = earlephilhower
board_build.filesystem_size = 0.5m
build_flags =
- ${arduino_base.build_flags} -Wno-unused-variable
+ ${arduino_base.build_flags} -Wno-unused-variable -Wcast-align
-Isrc/platform/rp2xx0
- -D__PLAT_RP2040__
+ -D__PLAT_RP2350__
# -D _POSIX_THREADS
build_src_filter =
- ${arduino_base.build_src_filter} - - - - - - - - -
+ ${arduino_base.build_src_filter} - - - - - - - - - - -
lib_ignore =
BluetoothOTA
diff --git a/bin/.gitignore b/bin/.gitignore
new file mode 100644
index 000000000..5b6b0720c
--- /dev/null
+++ b/bin/.gitignore
@@ -0,0 +1 @@
+config.yaml
diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml
index 49de1675b..e68b01ba3 100644
--- a/bin/config-dist.yaml
+++ b/bin/config-dist.yaml
@@ -12,12 +12,6 @@ Lora:
# IRQ: 17
# Reset: 22
-# Module: RF95 # Adafruit RFM9x
-# Reset: 25
-# CS: 7
-# IRQ: 22
-# Busy: 23
-
# Module: RF95 # Elecrow Lora RFM95 IOT https://www.elecrow.com/lora-rfm95-iot-board-for-rpi.html
# Reset: 22
# CS: 7
diff --git a/bin/config.d/lora-Adafruit-RFM9x b/bin/config.d/lora-Adafruit-RFM9x
new file mode 100644
index 000000000..2d64f1f91
--- /dev/null
+++ b/bin/config.d/lora-Adafruit-RFM9x
@@ -0,0 +1,5 @@
+# Module: RF95 # Adafruit RFM9x
+# Reset: 25
+# CS: 7
+# IRQ: 22
+# Busy: 23
\ No newline at end of file
diff --git a/bin/readprops.py b/bin/readprops.py
index 4b730658a..8a1d3dc47 100644
--- a/bin/readprops.py
+++ b/bin/readprops.py
@@ -1,6 +1,7 @@
import configparser
import subprocess
-
+import os
+run_number = os.getenv('GITHUB_RUN_NUMBER', '0')
def readProps(prefsLoc):
"""Read the version of our project as a string"""
@@ -10,6 +11,7 @@ def readProps(prefsLoc):
version = dict(config.items("VERSION"))
verObj = dict(
short="{}.{}.{}".format(version["major"], version["minor"], version["build"]),
+ deb="unset",
long="unset",
)
@@ -27,13 +29,13 @@ def readProps(prefsLoc):
# if isDirty:
# # short for 'dirty', we want to keep our verstrings source for protobuf reasons
# suffix = sha + "-d"
- verObj["long"] = "{}.{}.{}.{}".format(
- version["major"], version["minor"], version["build"], suffix
- )
+ verObj["long"] = "{}.{}".format(verObj["short"], suffix)
+ verObj["deb"] = "{}-{}~ppa{}".format(verObj["short"], run_number, sha)
except:
# print("Unexpected error:", sys.exc_info()[0])
# traceback.print_exc()
verObj["long"] = verObj["short"]
+ verObj["deb"] = "{}-{}~ppa".format(verObj["short"], run_number)
# print("firmware version " + verStr)
return verObj
diff --git a/debian/.gitignore b/debian/.gitignore
new file mode 100644
index 000000000..b36ab39fc
--- /dev/null
+++ b/debian/.gitignore
@@ -0,0 +1,6 @@
+.debhelper
+debhelper-build-stamp
+meshtasticd
+files
+meshtasticd.substvars
+meshtasticd.postrm.debhelper
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 000000000..79c444aca
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+meshtasticd (2.5.19) UNRELEASED; urgency=medium
+
+ * Initial packaging
+
+ -- Austin Lane Thu, 02 Jan 2025 12:00:00 +0000
\ No newline at end of file
diff --git a/debian/ci_changelog.sh b/debian/ci_changelog.sh
new file mode 100755
index 000000000..7925ad5eb
--- /dev/null
+++ b/debian/ci_changelog.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/bash
+export DEBEMAIL="github-actions[bot]@users.noreply.github.com"
+PKG_VERSION=$(python3 bin/buildinfo.py short)
+
+dch --newversion "$PKG_VERSION-1" \
+ --distribution UNRELEASED \
+ "GitHub Actions Automatic version bump"
diff --git a/debian/ci_pack_sdeb.sh b/debian/ci_pack_sdeb.sh
new file mode 100755
index 000000000..1f311af93
--- /dev/null
+++ b/debian/ci_pack_sdeb.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/bash
+export DEBEMAIL="jbennett@incomsystems.biz"
+export PLATFORMIO_LIBDEPS_DIR=pio/libdeps
+export PLATFORMIO_PACKAGES_DIR=pio/packages
+export PLATFORMIO_CORE_DIR=pio/core
+
+# Download libraries to `pio`
+platformio pkg install -e native
+platformio pkg install -e native -t platformio/tool-scons@4.40502.0
+# Compress `pio` directory to prevent dh_clean from sanitizing it
+tar -cf pio.tar pio/
+rm -rf pio
+# Download the latest meshtastic/web release build.tar to `web.tar`
+curl -L https://github.com/meshtastic/web/releases/download/latest/build.tar -o web.tar
+
+package=$(dpkg-parsechangelog --show-field Source)
+
+rm -rf debian/changelog
+dch --create --distribution "$SERIES" --package "$package" --newversion "$PKG_VERSION~$SERIES" \
+ "GitHub Actions Automatic packaging for $PKG_VERSION~$SERIES"
+
+# Build the source deb
+debuild -S -nc -k"$GPG_KEY_ID"
diff --git a/debian/control b/debian/control
new file mode 100644
index 000000000..bb79d1958
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,32 @@
+Source: meshtasticd
+Section: misc
+Priority: optional
+Maintainer: Austin Lane
+Build-Depends: debhelper-compat (= 13),
+ tar,
+ gzip,
+ platformio,
+ python3-protobuf,
+ python3-grpcio,
+ git,
+ g++,
+ pkg-config,
+ libyaml-cpp-dev,
+ libgpiod-dev,
+ libbluetooth-dev,
+ libusb-1.0-0-dev,
+ libi2c-dev,
+ openssl,
+ libssl-dev,
+ libulfius-dev,
+ liborcania-dev
+Standards-Version: 4.6.2
+Homepage: https://github.com/meshtastic/firmware
+Rules-Requires-Root: no
+
+Package: meshtasticd
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends}
+Description: Meshtastic daemon for communicating with Meshtastic devices
+ Meshtastic is an off-grid text communication platform that uses inexpensive
+ LoRa radios.
\ No newline at end of file
diff --git a/debian/meshtasticd.dirs b/debian/meshtasticd.dirs
new file mode 100644
index 000000000..5f57ff7be
--- /dev/null
+++ b/debian/meshtasticd.dirs
@@ -0,0 +1,4 @@
+etc/meshtasticd
+etc/meshtasticd/config.d
+etc/meshtasticd/available.d
+usr/share/meshtasticd/web
\ No newline at end of file
diff --git a/debian/meshtasticd.install b/debian/meshtasticd.install
new file mode 100644
index 000000000..da1b0685d
--- /dev/null
+++ b/debian/meshtasticd.install
@@ -0,0 +1,8 @@
+.pio/build/native/meshtasticd usr/sbin
+
+bin/config.yaml etc/meshtasticd
+bin/config.d/* etc/meshtasticd/available.d
+
+bin/meshtasticd.service lib/systemd/system
+
+web/* usr/share/meshtasticd/web
\ No newline at end of file
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 000000000..a1a27c2f2
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,23 @@
+#!/usr/bin/make -f
+# export DH_VERBOSE = 1
+
+# Use the "dh" sequencer
+%:
+ dh $@
+
+# https://docs.platformio.org/en/latest/envvars.html
+PIO_ENV:=\
+ PLATFORMIO_CORE_DIR=pio/core \
+ PLATFORMIO_LIBDEPS_DIR=pio/libdeps \
+ PLATFORMIO_PACKAGES_DIR=pio/packages
+
+override_dh_auto_build:
+ # Extract tarballs within source deb
+ tar -xf pio.tar
+ mkdir -p web && tar -xf web.tar -C web
+ gunzip web/ -r
+ # Build with platformio
+ $(PIO_ENV) platformio run -e native
+ # Move the binary and default config to the correct name
+ mv .pio/build/native/program .pio/build/native/meshtasticd
+ cp bin/config-dist.yaml bin/config.yaml
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 000000000..9f6742789
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
\ No newline at end of file
diff --git a/debian/source/include-binaries b/debian/source/include-binaries
new file mode 100644
index 000000000..0c9848b72
--- /dev/null
+++ b/debian/source/include-binaries
@@ -0,0 +1,2 @@
+pio.tar
+web.tar
\ No newline at end of file
diff --git a/debian/source/options b/debian/source/options
new file mode 100644
index 000000000..0553b485d
--- /dev/null
+++ b/debian/source/options
@@ -0,0 +1 @@
+extend-diff-ignore = "\.pio"
\ No newline at end of file
diff --git a/platformio.ini b/platformio.ini
index cd32ed179..6a4466c01 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -3,43 +3,7 @@
[platformio]
default_envs = tbeam
-;default_envs = pico
-;default_envs = tbeam-s3-core
-;default_envs = tbeam0.7
-;default_envs = heltec-v1
-;default_envs = heltec-v2_0
-;default_envs = heltec-v2_1
-;default_envs = heltec-wireless-tracker
-;default_envs = chatter2
-;default_envs = tlora-v1
-;default_envs = tlora_v1_3
-;default_envs = tlora-v2
-;default_envs = tlora-v2-1-1_6
-;default_envs = tlora-v2-1-1_6-tcxo
-;default_envs = tlora-v3-3-0-tcxo
-;default_envs = tlora-t3s3-v1
-;default_envs = t-echo
-;default_envs = canaryone
-;default_envs = native
-;default_envs = nano-g1
-;default_envs = pca10059_diy_eink
-;default_envs = meshtastic-diy-v1
-;default_envs = meshtastic-diy-v1_1
-;default_envs = meshtastic-dr-dev
-;default_envs = m5stack-coreink
-;default_envs = rak4631
-;default_envs = rak4631_eth_gw
-;default_envs = rak2560
-;default_envs = rak11310
-;default_envs = rak_wismeshtap
-;default_envs = wio-e5
-;default_envs = radiomaster_900_bandit_nano
-;default_envs = radiomaster_900_bandit_micro
-;default_envs = radiomaster_900_bandit
-;default_envs = heltec_vision_master_t190
-;default_envs = heltec_vision_master_e213
-;default_envs = heltec_vision_master_e290
-;default_envs = heltec_mesh_node_t114
+
extra_configs =
arch/*/*.ini
variants/*/platformio.ini
@@ -124,8 +88,7 @@ lib_deps =
[radiolib_base]
lib_deps =
- ; jgromes/RadioLib@7.1.0
- https://github.com/jgromes/RadioLib.git#92b687821ff4e6c358d866f84566f66672ab02b8
+ jgromes/RadioLib@7.1.2
; Common libs for environmental measurements in telemetry module
; (not included in native / portduino)
diff --git a/protobufs b/protobufs
index c55f120a9..76f806e1b 160000
--- a/protobufs
+++ b/protobufs
@@ -1 +1 @@
-Subproject commit c55f120a9c1ce90c85e4826907a0b9bcb2d5f5a2
+Subproject commit 76f806e1bb1e2a7b157a14fadd095775f63db5e4
diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h
index 268c4308f..175d8a595 100644
--- a/src/mesh/MeshService.h
+++ b/src/mesh/MeshService.h
@@ -142,7 +142,7 @@ class MeshService
void sendToPhone(meshtastic_MeshPacket *p);
/// Send an MQTT message to the phone for client proxying
- void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m);
+ virtual void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m);
/// Send a ClientNotification to the phone
void sendClientNotification(meshtastic_ClientNotification *cn);
diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h
index 6ef385e2b..32c9a6383 100644
--- a/src/mesh/NodeDB.h
+++ b/src/mesh/NodeDB.h
@@ -148,7 +148,7 @@ class NodeDB
return &meshNodes->at(x);
}
- meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
+ virtual meshtastic_NodeInfoLite *getMeshNode(NodeNum n);
size_t getNumMeshNodes() { return numMeshNodes; }
// returns true if the maximum number of nodes is reached or we are running low on memory
diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp
index f53f13938..f866fbc6c 100644
--- a/src/mesh/RadioLibInterface.cpp
+++ b/src/mesh/RadioLibInterface.cpp
@@ -437,6 +437,7 @@ void RadioLibInterface::handleReceiveInterrupt()
// nodes.
meshtastic_MeshPacket *mp = packetPool.allocZeroed();
+ // Keep the assigned fields in sync with src/mqtt/MQTT.cpp:onReceiveProto
mp->from = radioBuffer.header.from;
mp->to = radioBuffer.header.to;
mp->id = radioBuffer.header.id;
diff --git a/src/mesh/Router.h b/src/mesh/Router.h
index e74f7c2fd..bf6b77226 100644
--- a/src/mesh/Router.h
+++ b/src/mesh/Router.h
@@ -75,7 +75,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
* RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for
* freeing the packet
*/
- void enqueueReceivedMessage(meshtastic_MeshPacket *p);
+ virtual void enqueueReceivedMessage(meshtastic_MeshPacket *p);
/**
* Send a packet on a suitable interface. This routine will
diff --git a/src/mesh/generated/meshtastic/config.pb.cpp b/src/mesh/generated/meshtastic/config.pb.cpp
index 6fd2161ae..5512584a7 100644
--- a/src/mesh/generated/meshtastic/config.pb.cpp
+++ b/src/mesh/generated/meshtastic/config.pb.cpp
@@ -63,6 +63,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig,
+
+
diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h
index 5e105ab17..14aed9dfe 100644
--- a/src/mesh/generated/meshtastic/config.pb.h
+++ b/src/mesh/generated/meshtastic/config.pb.h
@@ -139,6 +139,14 @@ typedef enum _meshtastic_Config_NetworkConfig_AddressMode {
meshtastic_Config_NetworkConfig_AddressMode_STATIC = 1
} meshtastic_Config_NetworkConfig_AddressMode;
+/* Available flags auxiliary network protocols */
+typedef enum _meshtastic_Config_NetworkConfig_ProtocolFlags {
+ /* Do not broadcast packets over any network protocol */
+ meshtastic_Config_NetworkConfig_ProtocolFlags_NO_BROADCAST = 0,
+ /* Enable broadcasting packets via UDP over the local network */
+ meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST = 1
+} meshtastic_Config_NetworkConfig_ProtocolFlags;
+
/* How the GPS coordinates are displayed on the OLED screen. */
typedef enum _meshtastic_Config_DisplayConfig_GpsCoordinateFormat {
/* GPS coordinates are displayed in the normal decimal degrees format:
@@ -429,6 +437,8 @@ typedef struct _meshtastic_Config_NetworkConfig {
meshtastic_Config_NetworkConfig_IpV4Config ipv4_config;
/* rsyslog Server and Port */
char rsyslog_server[33];
+ /* Flags for enabling/disabling network protocols */
+ uint32_t enabled_protocols;
} meshtastic_Config_NetworkConfig;
/* Display Config */
@@ -613,6 +623,10 @@ extern "C" {
#define _meshtastic_Config_NetworkConfig_AddressMode_MAX meshtastic_Config_NetworkConfig_AddressMode_STATIC
#define _meshtastic_Config_NetworkConfig_AddressMode_ARRAYSIZE ((meshtastic_Config_NetworkConfig_AddressMode)(meshtastic_Config_NetworkConfig_AddressMode_STATIC+1))
+#define _meshtastic_Config_NetworkConfig_ProtocolFlags_MIN meshtastic_Config_NetworkConfig_ProtocolFlags_NO_BROADCAST
+#define _meshtastic_Config_NetworkConfig_ProtocolFlags_MAX meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST
+#define _meshtastic_Config_NetworkConfig_ProtocolFlags_ARRAYSIZE ((meshtastic_Config_NetworkConfig_ProtocolFlags)(meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST+1))
+
#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC
#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MAX meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR
#define _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_ARRAYSIZE ((meshtastic_Config_DisplayConfig_GpsCoordinateFormat)(meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR+1))
@@ -674,7 +688,7 @@ extern "C" {
#define meshtastic_Config_DeviceConfig_init_default {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0}
#define meshtastic_Config_PositionConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN}
#define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0}
-#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, ""}
+#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0}
#define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN}
#define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
@@ -685,7 +699,7 @@ extern "C" {
#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0}
#define meshtastic_Config_PositionConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN}
#define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0}
-#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, ""}
+#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0}
#define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0}
#define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN}
#define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0}
@@ -739,6 +753,7 @@ extern "C" {
#define meshtastic_Config_NetworkConfig_address_mode_tag 7
#define meshtastic_Config_NetworkConfig_ipv4_config_tag 8
#define meshtastic_Config_NetworkConfig_rsyslog_server_tag 9
+#define meshtastic_Config_NetworkConfig_enabled_protocols_tag 10
#define meshtastic_Config_DisplayConfig_screen_on_secs_tag 1
#define meshtastic_Config_DisplayConfig_gps_format_tag 2
#define meshtastic_Config_DisplayConfig_auto_screen_carousel_secs_tag 3
@@ -867,7 +882,8 @@ X(a, STATIC, SINGULAR, STRING, ntp_server, 5) \
X(a, STATIC, SINGULAR, BOOL, eth_enabled, 6) \
X(a, STATIC, SINGULAR, UENUM, address_mode, 7) \
X(a, STATIC, OPTIONAL, MESSAGE, ipv4_config, 8) \
-X(a, STATIC, SINGULAR, STRING, rsyslog_server, 9)
+X(a, STATIC, SINGULAR, STRING, rsyslog_server, 9) \
+X(a, STATIC, SINGULAR, UINT32, enabled_protocols, 10)
#define meshtastic_Config_NetworkConfig_CALLBACK NULL
#define meshtastic_Config_NetworkConfig_DEFAULT NULL
#define meshtastic_Config_NetworkConfig_ipv4_config_MSGTYPE meshtastic_Config_NetworkConfig_IpV4Config
@@ -972,12 +988,12 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg;
#define meshtastic_Config_DisplayConfig_size 30
#define meshtastic_Config_LoRaConfig_size 85
#define meshtastic_Config_NetworkConfig_IpV4Config_size 20
-#define meshtastic_Config_NetworkConfig_size 196
+#define meshtastic_Config_NetworkConfig_size 202
#define meshtastic_Config_PositionConfig_size 62
#define meshtastic_Config_PowerConfig_size 52
#define meshtastic_Config_SecurityConfig_size 178
#define meshtastic_Config_SessionkeyConfig_size 0
-#define meshtastic_Config_size 199
+#define meshtastic_Config_size 205
#ifdef __cplusplus
} /* extern "C" */
diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h
index 0c4f5384e..f090b5b4f 100644
--- a/src/mesh/generated/meshtastic/device_ui.pb.h
+++ b/src/mesh/generated/meshtastic/device_ui.pb.h
@@ -51,6 +51,8 @@ typedef enum _meshtastic_Language {
meshtastic_Language_GREEK = 13,
/* Norwegian */
meshtastic_Language_NORWEGIAN = 14,
+ /* Slovenian */
+ meshtastic_Language_SLOVENIAN = 15,
/* Simplified Chinese (experimental) */
meshtastic_Language_SIMPLIFIED_CHINESE = 30,
/* Traditional Chinese (experimental) */
@@ -71,6 +73,8 @@ typedef struct _meshtastic_NodeFilter {
bool position_switch;
/* Filter nodes by matching name string */
char node_name[16];
+ /* Filter based on channel */
+ int8_t channel;
} meshtastic_NodeFilter;
typedef struct _meshtastic_NodeHighlight {
@@ -138,10 +142,10 @@ extern "C" {
/* Initializer values for message structs */
#define meshtastic_DeviceUIConfig_init_default {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_default, false, meshtastic_NodeHighlight_init_default, {0, {0}}}
-#define meshtastic_NodeFilter_init_default {0, 0, 0, 0, 0, ""}
+#define meshtastic_NodeFilter_init_default {0, 0, 0, 0, 0, "", 0}
#define meshtastic_NodeHighlight_init_default {0, 0, 0, 0, ""}
#define meshtastic_DeviceUIConfig_init_zero {0, 0, 0, 0, 0, 0, _meshtastic_Theme_MIN, 0, 0, 0, _meshtastic_Language_MIN, false, meshtastic_NodeFilter_init_zero, false, meshtastic_NodeHighlight_init_zero, {0, {0}}}
-#define meshtastic_NodeFilter_init_zero {0, 0, 0, 0, 0, ""}
+#define meshtastic_NodeFilter_init_zero {0, 0, 0, 0, 0, "", 0}
#define meshtastic_NodeHighlight_init_zero {0, 0, 0, 0, ""}
/* Field tags (for use in manual encoding/decoding) */
@@ -151,6 +155,7 @@ extern "C" {
#define meshtastic_NodeFilter_hops_away_tag 4
#define meshtastic_NodeFilter_position_switch_tag 5
#define meshtastic_NodeFilter_node_name_tag 6
+#define meshtastic_NodeFilter_channel_tag 7
#define meshtastic_NodeHighlight_chat_switch_tag 1
#define meshtastic_NodeHighlight_position_switch_tag 2
#define meshtastic_NodeHighlight_telemetry_switch_tag 3
@@ -198,7 +203,8 @@ X(a, STATIC, SINGULAR, BOOL, offline_switch, 2) \
X(a, STATIC, SINGULAR, BOOL, public_key_switch, 3) \
X(a, STATIC, SINGULAR, INT32, hops_away, 4) \
X(a, STATIC, SINGULAR, BOOL, position_switch, 5) \
-X(a, STATIC, SINGULAR, STRING, node_name, 6)
+X(a, STATIC, SINGULAR, STRING, node_name, 6) \
+X(a, STATIC, SINGULAR, INT32, channel, 7)
#define meshtastic_NodeFilter_CALLBACK NULL
#define meshtastic_NodeFilter_DEFAULT NULL
@@ -222,8 +228,8 @@ extern const pb_msgdesc_t meshtastic_NodeHighlight_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_DEVICE_UI_PB_H_MAX_SIZE meshtastic_DeviceUIConfig_size
-#define meshtastic_DeviceUIConfig_size 117
-#define meshtastic_NodeFilter_size 36
+#define meshtastic_DeviceUIConfig_size 128
+#define meshtastic_NodeFilter_size 47
#define meshtastic_NodeHighlight_size 25
#ifdef __cplusplus
diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h
index 30f70ed90..dc0f507c9 100644
--- a/src/mesh/generated/meshtastic/localonly.pb.h
+++ b/src/mesh/generated/meshtastic/localonly.pb.h
@@ -187,7 +187,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg;
/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size
-#define meshtastic_LocalConfig_size 735
+#define meshtastic_LocalConfig_size 741
#define meshtastic_LocalModuleConfig_size 699
#ifdef __cplusplus
diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp
index 2f8138921..dcfcdc047 100644
--- a/src/mesh/wifi/WiFiAPClient.cpp
+++ b/src/mesh/wifi/WiFiAPClient.cpp
@@ -143,6 +143,11 @@ static int32_t reconnectWiFi()
delay(5000);
if (!WiFi.isConnected()) {
+#ifdef CONFIG_IDF_TARGET_ESP32C3
+ WiFi.mode(WIFI_MODE_NULL);
+ WiFi.useStaticBuffers(true);
+ WiFi.mode(WIFI_STA);
+#endif
WiFi.begin(wifiName, wifiPsw);
}
isReconnecting = false;
diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h
index 7c34c5bc9..c047f6e29 100644
--- a/src/modules/RoutingModule.h
+++ b/src/modules/RoutingModule.h
@@ -13,7 +13,8 @@ class RoutingModule : public ProtobufModule
*/
RoutingModule();
- void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0);
+ virtual void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
+ uint8_t hopLimit = 0);
// Given the hopStart and hopLimit upon reception of a request, return the hop limit to use for the response
uint8_t getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit);
diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp
index 10133fca5..9c794e31e 100644
--- a/src/modules/Telemetry/PowerTelemetry.cpp
+++ b/src/modules/Telemetry/PowerTelemetry.cpp
@@ -99,44 +99,45 @@ bool PowerTelemetryModule::wantUIFrame()
void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
- display->setFont(FONT_MEDIUM);
- display->drawString(x, y, "Power Telemetry");
+ display->setFont(FONT_SMALL);
+
if (lastMeasurementPacket == nullptr) {
- display->setFont(FONT_SMALL);
- display->drawString(x, y += _fontHeight(FONT_MEDIUM), "No measurement");
+ // In case of no valid packet, display "Power Telemetry", "No measurement"
+ display->drawString(x, y, "Power Telemetry");
+ display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement");
return;
}
+ // Decode the last power packet
meshtastic_Telemetry lastMeasurement;
-
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
const char *lastSender = getSenderShortName(*lastMeasurementPacket);
const meshtastic_Data &p = lastMeasurementPacket->decoded;
if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) {
- display->setFont(FONT_SMALL);
- display->drawString(x, y += _fontHeight(FONT_MEDIUM), "Measurement Error");
+ display->drawString(x, y, "Measurement Error");
LOG_ERROR("Unable to decode last packet");
return;
}
+ // Display "Pow. From: ..."
+ display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
+
// Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags
- display->setFont(FONT_SMALL);
- display->drawString(x, y += _fontHeight(FONT_MEDIUM) - 2, "From: " + String(lastSender) + "(" + String(agoSecs) + "s)");
if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
- "Ch1 Volt: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) +
- "V / Curr: " + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
+ "Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) +
+ "V " + String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA");
}
if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
- "Ch2 Volt: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) +
- "V / Curr: " + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
+ "Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) +
+ "V " + String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA");
}
if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) {
display->drawString(x, y += _fontHeight(FONT_SMALL),
- "Ch3 Volt: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) +
- "V / Curr: " + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
+ "Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) +
+ "V " + String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA");
}
}
diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp
index 3db3c37bb..f642af231 100644
--- a/src/mqtt/MQTT.cpp
+++ b/src/mqtt/MQTT.cpp
@@ -76,12 +76,22 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
return;
}
LOG_INFO("Received MQTT topic %s, len=%u", topic, length);
+ if (e.packet->hop_limit > HOP_MAX || e.packet->hop_start > HOP_MAX) {
+ LOG_INFO("Invalid hop_limit(%u) or hop_start(%u)", e.packet->hop_limit, e.packet->hop_start);
+ return;
+ }
- UniquePacketPoolPacket p = packetPool.allocUniqueCopy(*e.packet);
+ UniquePacketPoolPacket p = packetPool.allocUniqueZeroed();
+ p->from = e.packet->from;
+ p->to = e.packet->to;
+ p->id = e.packet->id;
+ p->channel = e.packet->channel;
+ p->hop_limit = e.packet->hop_limit;
+ p->hop_start = e.packet->hop_start;
+ p->want_ack = e.packet->want_ack;
p->via_mqtt = true; // Mark that the packet was received via MQTT
- // Unset received SNR/RSSI which might have been added by the MQTT gateway
- p->rx_snr = 0;
- p->rx_rssi = 0;
+ p->which_payload_variant = e.packet->which_payload_variant;
+ memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted)));
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
if (moduleConfig.mqtt.encryption_enabled) {
diff --git a/src/platform/rp2xx0/main-rp2xx0.cpp b/src/platform/rp2xx0/main-rp2xx0.cpp
index a46b0face..6c73e385a 100644
--- a/src/platform/rp2xx0/main-rp2xx0.cpp
+++ b/src/platform/rp2xx0/main-rp2xx0.cpp
@@ -2,14 +2,11 @@
#include "hardware/xosc.h"
#include
#include
-#include
#include
#include
-void setBluetoothEnable(bool enable)
-{
- // not needed
-}
+#ifdef __PLAT_RP2040__
+#include
static bool awake;
@@ -66,7 +63,20 @@ void cpuDeepSleep(uint32_t msecs)
rp2040.reboot();
/* Set RP2040 in dormant mode. Will not wake up. */
- // xosc_dormant();
+ // xosc_dormant();
+}
+
+#else
+void cpuDeepSleep(uint32_t msecs)
+{
+ /* Set RP2040 in dormant mode. Will not wake up. */
+ xosc_dormant();
+}
+#endif
+
+void setBluetoothEnable(bool enable)
+{
+ // not needed
}
void updateBatteryLevel(uint8_t level)
diff --git a/test/test_mqtt/MQTT.cpp b/test/test_mqtt/MQTT.cpp
new file mode 100644
index 000000000..55ba479e2
--- /dev/null
+++ b/test/test_mqtt/MQTT.cpp
@@ -0,0 +1,856 @@
+#include "DebugConfiguration.h"
+#include "TestUtil.h"
+#include
+
+#ifdef ARCH_PORTDUINO
+#include "mesh/CryptoEngine.h"
+#include "mesh/Default.h"
+#include "mesh/MeshService.h"
+#include "mesh/NodeDB.h"
+#include "mesh/Router.h"
+#include "modules/RoutingModule.h"
+#include "mqtt/MQTT.h"
+#include "mqtt/ServiceEnvelope.h"
+
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+// Minimal router needed to receive messages from MQTT.
+class MockRouter : public Router
+{
+ public:
+ ~MockRouter()
+ {
+ // cryptLock is created in the constructor for Router.
+ delete cryptLock;
+ cryptLock = NULL;
+ }
+ void enqueueReceivedMessage(meshtastic_MeshPacket *p) override
+ {
+ packets_.emplace_back(*p);
+ packetPool.release(p);
+ }
+ std::list packets_; // Packets received by the Router.
+};
+
+// Minimal MeshService needed to receive messages from MQTT for testing PKI channel.
+class MockMeshService : public MeshService
+{
+ public:
+ void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override
+ {
+ messages_.emplace_back(*m);
+ releaseMqttClientProxyMessageToPool(m);
+ }
+ std::list messages_; // Messages received from the MeshService.
+};
+
+// Minimal NodeDB needed to return values from getMeshNode.
+class MockNodeDB : public NodeDB
+{
+ public:
+ meshtastic_NodeInfoLite *getMeshNode(NodeNum n) override { return &emptyNode; }
+ meshtastic_NodeInfoLite emptyNode = {};
+};
+
+// Minimal RoutingModule needed to return values from sendAckNak.
+class MockRoutingModule : public RoutingModule
+{
+ public:
+ void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex,
+ uint8_t hopLimit = 0) override
+ {
+ ackNacks_.emplace_back(err, to, idFrom, chIndex, hopLimit);
+ }
+ std::list>
+ ackNacks_; // ackNacks received by the RoutingModule.
+};
+
+// A WiFi client used by the MQTT::PubSubClient. Implements a minimal pub/sub server.
+// There isn't an easy way to mock PubSubClient due to it not having virtual methods, so we mock using
+// the WiFiClinet that PubSubClient uses.
+class MockPubSubServer : public WiFiClient
+{
+ public:
+ static constexpr char kTextTopic[] = "TextTopic";
+ uint8_t connected() override { return connected_; }
+ void flush() override {}
+ IPAddress remoteIP() const override { return IPAddress(htonl(ipAddress_)); }
+ void stop() override { connected_ = false; }
+
+ int connect(IPAddress ip, uint16_t port) override
+ {
+ if (refuseConnection_)
+ return 0;
+ connected_ = true;
+ return 1;
+ }
+ int connect(const char *host, uint16_t port) override
+ {
+ if (refuseConnection_)
+ return 0;
+ connected_ = true;
+ return 1;
+ }
+
+ int available() override
+ {
+ if (buffer_.empty())
+ return 0;
+ return buffer_.front().size();
+ }
+
+ int read() override
+ {
+ assert(available());
+ std::string &front = buffer_.front();
+ char ch = front[0];
+ front = front.substr(1, front.size());
+ if (front.empty())
+ buffer_.pop_front();
+ return ch;
+ }
+
+ size_t write(uint8_t data) override { return write(&data, 1); }
+ size_t write(const uint8_t *buf, size_t size) override
+ {
+ command_ += std::string(reinterpret_cast(buf), size);
+ if (command_.size() < 2)
+ return size;
+ const int len = (uint8_t)command_[1] + 2;
+ if (command_.size() < len)
+ return size;
+ handleCommand(command_[0], command_.substr(2, len));
+ command_ = command_.substr(len, command_.size());
+ return size;
+ }
+
+ // The pub/sub "server".
+ // https://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/MQTT_V3.1_Protocol_Specific.pdf
+ void handleCommand(uint8_t header, std::string_view message)
+ {
+ switch (header & 0xf0) {
+ case MQTTCONNECT:
+ LOG_DEBUG("MQTTCONNECT");
+ buffer_.push_back(std::string("\x20\x02\x00\x00", 4));
+ break;
+
+ case MQTTSUBSCRIBE: {
+ LOG_DEBUG("MQTTSUBSCRIBE");
+ assert(message.size() >= 5);
+ message.remove_prefix(2); // skip messageId
+
+ while (message.size() >= 3) {
+ const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1];
+ message.remove_prefix(2);
+
+ assert(message.size() >= topicSize + 1);
+ std::string topic(message.data(), topicSize);
+ message.remove_prefix(topicSize + 1);
+
+ LOG_DEBUG("Subscribed to topic: %s", topic.c_str());
+ subscriptions_.insert(std::move(topic));
+ }
+ break;
+ }
+
+ case MQTTPINGREQ:
+ LOG_DEBUG("MQTTPINGREQ");
+ buffer_.push_back(std::string("\xd0\x00", 2));
+ break;
+
+ case MQTTPUBLISH: {
+ LOG_DEBUG("MQTTPUBLISH");
+ assert(message.size() >= 3);
+ const uint16_t topicSize = ((uint8_t)message[0]) << 8 | (uint8_t)message[1];
+ message.remove_prefix(2);
+
+ assert(message.size() >= topicSize);
+ std::string topic(message.data(), topicSize);
+ message.remove_prefix(topicSize);
+
+ if (topic == kTextTopic) {
+ published_.emplace_back(std::move(topic), std::string(message.data(), message.size()));
+ } else {
+ published_.emplace_back(
+ std::move(topic), DecodedServiceEnvelope(reinterpret_cast(message.data()), message.size()));
+ }
+ break;
+ }
+ }
+ }
+
+ bool connected_ = false;
+ bool refuseConnection_ = false; // Simulate a failed connection.
+ uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server.
+ std::list buffer_; // Buffer of messages for the pubSub client to receive.
+ std::string command_; // Current command received from the pubSub client.
+ std::set subscriptions_; // Topics that the pubSub client has subscribed to.
+ std::list>>
+ published_; // Messages published from the pubSub client. Each list element is a pair containing the topic name and either
+ // a text message (if from the kTextTopic topic) or a DecodedServiceEnvelope.
+};
+
+// Instances of our mocks.
+class MQTTUnitTest;
+MQTTUnitTest *unitTest;
+MockPubSubServer *pubsub;
+MockRoutingModule *mockRoutingModule;
+MockMeshService *mockMeshService;
+MockRouter *mockRouter;
+
+// Keep running the loop until either conditionMet returns true or 4 seconds elapse.
+// Returns true if conditionMet returns true, returns false on timeout.
+bool loopUntil(std::function conditionMet)
+{
+ long start = millis();
+ while (start + 4000 > millis()) {
+ long delayMsec = concurrency::mainController.runOrDelay();
+ if (conditionMet())
+ return true;
+ concurrency::mainDelay.delay(std::min(delayMsec, 5L));
+ }
+ return false;
+}
+
+// Used to access protected/private members of MQTT for unit testing.
+class MQTTUnitTest : public MQTT
+{
+ public:
+ MQTTUnitTest() : MQTT(std::make_unique())
+ {
+ pubsub = reinterpret_cast(mqttClient.get());
+ }
+ ~MQTTUnitTest()
+ {
+ // Needed because WiFiClient does not have a virtual destructor.
+ mqttClient.release();
+ delete pubsub;
+ }
+ int queueSize() { return mqttQueue.numUsed(); }
+ void reportToMap(std::optional precision = std::nullopt)
+ {
+ if (precision.has_value())
+ map_position_precision = precision.value();
+ map_publish_interval_msecs = 0;
+ perhapsReportToMap();
+ }
+ void publish(const meshtastic_MeshPacket *p, std::string gateway = "!87654321", std::string channel = "test")
+ {
+ std::stringstream topic;
+ topic << "msh/2/e/" << channel << "/!" << gateway;
+ const meshtastic_ServiceEnvelope env = {.packet = const_cast(p),
+ .channel_id = const_cast(channel.c_str()),
+ .gateway_id = const_cast(gateway.c_str())};
+ uint8_t bytes[256];
+ size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, &env);
+ mqttCallback(const_cast(topic.str().c_str()), bytes, numBytes);
+ }
+ static void restart()
+ {
+ if (mqtt != NULL) {
+ delete mqtt;
+ mqtt = unitTest = NULL;
+ }
+ mqtt = unitTest = new MQTTUnitTest();
+ mqtt->start();
+
+ if (!moduleConfig.mqtt.enabled || moduleConfig.mqtt.proxy_to_client_enabled || *moduleConfig.mqtt.root) {
+ loopUntil([] { return true; }); // Loop once
+ return;
+ }
+ // Wait for MQTT to subscribe to all topics.
+ TEST_ASSERT_TRUE(loopUntil(
+ [] { return pubsub->subscriptions_.count("msh/2/e/test/+") && pubsub->subscriptions_.count("msh/2/e/PKI/+"); }));
+ }
+ PubSubClient &getPubSub() { return pubSub; }
+};
+
+// Packets used in unit tests.
+const meshtastic_MeshPacket decoded = {
+ .from = 1,
+ .to = 2,
+ .which_payload_variant = meshtastic_MeshPacket_decoded_tag,
+ .decoded = {.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP, .has_bitfield = true, .bitfield = BITFIELD_OK_TO_MQTT_MASK},
+ .id = 4,
+};
+const meshtastic_MeshPacket encrypted = {
+ .from = 1,
+ .to = 2,
+ .which_payload_variant = meshtastic_MeshPacket_encrypted_tag,
+ .encrypted = {.size = 0},
+ .id = 3,
+};
+} // namespace
+
+// Initialize mocks and configuration before running each test.
+void setUp(void)
+{
+ moduleConfig.mqtt =
+ meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true};
+ channelFile.channels[0] = meshtastic_Channel{
+ .index = 0,
+ .has_settings = true,
+ .settings = {.name = "test", .uplink_enabled = true, .downlink_enabled = true},
+ .role = meshtastic_Channel_Role_PRIMARY,
+ };
+ channelFile.channels_count = 1;
+ owner = meshtastic_User{.id = "!12345678"};
+ myNodeInfo = meshtastic_MyNodeInfo{.my_node_num = 10};
+ localPosition =
+ meshtastic_Position{.has_latitude_i = true, .latitude_i = 7 * 1e7, .has_longitude_i = true, .longitude_i = 3 * 1e7};
+
+ router = mockRouter = new MockRouter();
+ service = mockMeshService = new MockMeshService();
+ routingModule = mockRoutingModule = new MockRoutingModule();
+ MQTTUnitTest::restart();
+}
+
+// Deinitialize all objects created in setUp.
+void tearDown(void)
+{
+ delete unitTest;
+ mqtt = unitTest = NULL;
+ delete mockRoutingModule;
+ routingModule = mockRoutingModule = NULL;
+ delete mockMeshService;
+ service = mockMeshService = NULL;
+ delete mockRouter;
+ router = mockRouter = NULL;
+}
+
+// Test that the decoded MeshPacket is published when encryption_enabled = false.
+void test_sendDirectlyConnectedDecoded(void)
+{
+ mqtt->onSend(encrypted, decoded, 0);
+
+ TEST_ASSERT_EQUAL(1, pubsub->published_.size());
+ const auto &[topic, payload] = pubsub->published_.front();
+ const DecodedServiceEnvelope &env = std::get(payload);
+ TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str());
+ TEST_ASSERT_TRUE(env.validDecode);
+ TEST_ASSERT_EQUAL(decoded.id, env.packet->id);
+}
+
+// Test that the encrypted MeshPacket is published when encryption_enabled = true.
+void test_sendDirectlyConnectedEncrypted(void)
+{
+ moduleConfig.mqtt.encryption_enabled = true;
+
+ mqtt->onSend(encrypted, decoded, 0);
+
+ TEST_ASSERT_EQUAL(1, pubsub->published_.size());
+ const auto &[topic, payload] = pubsub->published_.front();
+ const DecodedServiceEnvelope &env = std::get(payload);
+ TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str());
+ TEST_ASSERT_TRUE(env.validDecode);
+ TEST_ASSERT_EQUAL(encrypted.id, env.packet->id);
+}
+
+// Verify that the decoded MeshPacket is proxied through the MeshService when encryption_enabled = false.
+void test_proxyToMeshServiceDecoded(void)
+{
+ moduleConfig.mqtt.proxy_to_client_enabled = true;
+ MQTTUnitTest::restart();
+
+ mqtt->onSend(encrypted, decoded, 0);
+
+ TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
+ const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
+ TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic);
+ TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant);
+ const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size);
+ TEST_ASSERT_TRUE(env.validDecode);
+ TEST_ASSERT_EQUAL(decoded.id, env.packet->id);
+}
+
+// Verify that the encrypted MeshPacket is proxied through the MeshService when encryption_enabled = true.
+void test_proxyToMeshServiceEncrypted(void)
+{
+ moduleConfig.mqtt.proxy_to_client_enabled = true;
+ moduleConfig.mqtt.encryption_enabled = true;
+ MQTTUnitTest::restart();
+
+ mqtt->onSend(encrypted, decoded, 0);
+
+ TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
+ const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
+ TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", message.topic);
+ TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant);
+ const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size);
+ TEST_ASSERT_TRUE(env.validDecode);
+ TEST_ASSERT_EQUAL(encrypted.id, env.packet->id);
+}
+
+// A packet without the OK to MQTT bit set should not be published to a public server.
+void test_dontMqttMeOnPublicServer(void)
+{
+ meshtastic_MeshPacket p = decoded;
+ p.decoded.bitfield = 0;
+ p.decoded.has_bitfield = 0;
+
+ mqtt->onSend(encrypted, p, 0);
+
+ TEST_ASSERT_TRUE(pubsub->published_.empty());
+}
+
+// A packet without the OK to MQTT bit set should be published to a private server.
+void test_okToMqttOnPrivateServer(void)
+{
+ // Cause a disconnect.
+ pubsub->connected_ = false;
+ pubsub->refuseConnection_ = true;
+ TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); }));
+
+ // Use 127.0.0.1 for the server's IP.
+ pubsub->ipAddress_ = 0x7f000001;
+
+ // Reconnect.
+ pubsub->refuseConnection_ = false;
+ TEST_ASSERT_TRUE(loopUntil([] { return unitTest->getPubSub().connected(); }));
+
+ // Send the same packet as test_dontMqttMeOnPublicServer.
+ meshtastic_MeshPacket p = decoded;
+ p.decoded.bitfield = 0;
+ p.decoded.has_bitfield = 0;
+
+ mqtt->onSend(encrypted, p, 0);
+
+ TEST_ASSERT_EQUAL(1, pubsub->published_.size());
+}
+
+// Range tests messages are not uplinked to the default server.
+void test_noRangeTestAppOnDefaultServer(void)
+{
+ meshtastic_MeshPacket p = decoded;
+ p.decoded.portnum = meshtastic_PortNum_RANGE_TEST_APP;
+
+ mqtt->onSend(encrypted, p, 0);
+
+ TEST_ASSERT_TRUE(pubsub->published_.empty());
+}
+
+// Detection sensor messages are not uplinked to the default server.
+void test_noDetectionSensorAppOnDefaultServer(void)
+{
+ meshtastic_MeshPacket p = decoded;
+ p.decoded.portnum = meshtastic_PortNum_DETECTION_SENSOR_APP;
+
+ mqtt->onSend(encrypted, p, 0);
+
+ TEST_ASSERT_TRUE(pubsub->published_.empty());
+}
+
+// Test that a MeshPacket is queued while the MQTT server is disconnected.
+void test_sendQueued(void)
+{
+ // Cause a disconnect.
+ pubsub->connected_ = false;
+ pubsub->refuseConnection_ = true;
+ TEST_ASSERT_TRUE(loopUntil([] { return !unitTest->getPubSub().connected(); }));
+
+ // Send while disconnected.
+ mqtt->onSend(encrypted, decoded, 0);
+ TEST_ASSERT_EQUAL(1, unitTest->queueSize());
+ TEST_ASSERT_TRUE(pubsub->published_.empty());
+ TEST_ASSERT_FALSE(unitTest->getPubSub().connected());
+
+ // Allow reconnect to happen. Expect to see the packet published now.
+ pubsub->refuseConnection_ = false;
+ TEST_ASSERT_TRUE(loopUntil([] { return !pubsub->published_.empty(); }));
+
+ TEST_ASSERT_EQUAL(0, unitTest->queueSize());
+ const auto &[topic, payload] = pubsub->published_.front();
+ const DecodedServiceEnvelope &env = std::get(payload);
+ TEST_ASSERT_EQUAL_STRING("msh/2/e/test/!12345678", topic.c_str());
+ TEST_ASSERT_TRUE(env.validDecode);
+ TEST_ASSERT_EQUAL(decoded.id, env.packet->id);
+}
+
+// Verify reconnecting with the proxy enabled does not reconnect to a MQTT server.
+void test_reconnectProxyDoesNotReconnectMqtt(void)
+{
+ moduleConfig.mqtt.proxy_to_client_enabled = true;
+ MQTTUnitTest::restart();
+
+ mqtt->reconnect();
+
+ TEST_ASSERT_FALSE(pubsub->connected_);
+}
+
+// Test receiving an empty MeshPacket on a subscribed topic.
+void test_receiveEmptyMeshPacket(void)
+{
+ unitTest->publish(NULL);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+ TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty());
+}
+
+// Test receiving a decoded MeshPacket on a subscribed topic.
+void test_receiveDecodedProto(void)
+{
+ unitTest->publish(&decoded);
+
+ TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
+ const meshtastic_MeshPacket &p = mockRouter->packets_.front();
+ TEST_ASSERT_EQUAL(decoded.id, p.id);
+ TEST_ASSERT_TRUE(p.via_mqtt);
+}
+
+// Test receiving a decoded MeshPacket from the phone proxy.
+void test_receiveDecodedProtoFromProxy(void)
+{
+ const meshtastic_ServiceEnvelope env = {
+ .packet = const_cast(&decoded), .channel_id = "test", .gateway_id = "!87654321"};
+ meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default;
+ strcat(message.topic, "msh/2/e/test/!87654321");
+ message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag;
+ message.payload_variant.data.size = pb_encode_to_bytes(
+ message.payload_variant.data.bytes, sizeof(message.payload_variant.data.bytes), &meshtastic_ServiceEnvelope_msg, &env);
+
+ mqtt->onClientProxyReceive(message);
+
+ TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
+ const meshtastic_MeshPacket &p = mockRouter->packets_.front();
+ TEST_ASSERT_EQUAL(decoded.id, p.id);
+ TEST_ASSERT_TRUE(p.via_mqtt);
+}
+
+// Properly handles the case where the received message is empty.
+void test_receiveEmptyDataFromProxy(void)
+{
+ meshtastic_MqttClientProxyMessage message = meshtastic_MqttClientProxyMessage_init_default;
+ message.which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag;
+
+ mqtt->onClientProxyReceive(message);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+}
+
+// Packets should be ignored if downlink is not enabled.
+void test_receiveWithoutChannelDownlink(void)
+{
+ channelFile.channels[0].settings.downlink_enabled = false;
+
+ unitTest->publish(&decoded);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+}
+
+// Test receiving an encrypted MeshPacket on the PKI topic.
+void test_receiveEncryptedPKITopicToUs(void)
+{
+ meshtastic_MeshPacket e = encrypted;
+ e.to = myNodeInfo.my_node_num;
+
+ unitTest->publish(&e, "!87654321", "PKI");
+
+ TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
+ const meshtastic_MeshPacket &p = mockRouter->packets_.front();
+ TEST_ASSERT_EQUAL(encrypted.id, p.id);
+ TEST_ASSERT_TRUE(p.via_mqtt);
+}
+
+// Should ignore messages published to MQTT by this gateway.
+void test_receiveIgnoresOwnPublishedMessages(void)
+{
+ unitTest->publish(&decoded, owner.id);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+ TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty());
+}
+
+// Considers receiving one of our packets an acknowledgement of it being sent.
+void test_receiveAcksOwnSentMessages(void)
+{
+ meshtastic_MeshPacket p = decoded;
+ p.from = myNodeInfo.my_node_num;
+
+ unitTest->publish(&p, owner.id);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+ TEST_ASSERT_EQUAL(1, mockRoutingModule->ackNacks_.size());
+ const auto &[err, to, idFrom, chIndex, hopLimit] = mockRoutingModule->ackNacks_.front();
+ TEST_ASSERT_EQUAL(meshtastic_Routing_Error_NONE, err);
+ TEST_ASSERT_EQUAL(myNodeInfo.my_node_num, to);
+ TEST_ASSERT_EQUAL(p.id, idFrom);
+}
+
+// Should ignore our own messages from MQTT that were heard by other nodes.
+void test_receiveIgnoresSentMessagesFromOthers(void)
+{
+ meshtastic_MeshPacket p = decoded;
+ p.from = myNodeInfo.my_node_num;
+
+ unitTest->publish(&p);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+ TEST_ASSERT_TRUE(mockRoutingModule->ackNacks_.empty());
+}
+
+// Decoded MQTT messages should be ignored when encryption is enabled.
+void test_receiveIgnoresDecodedWhenEncryptionEnabled(void)
+{
+ moduleConfig.mqtt.encryption_enabled = true;
+
+ unitTest->publish(&decoded);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+}
+
+// Non-encrypted messages for the Admin App should be ignored.
+void test_receiveIgnoresDecodedAdminApp(void)
+{
+ meshtastic_MeshPacket p = decoded;
+ p.decoded.portnum = meshtastic_PortNum_ADMIN_APP;
+
+ unitTest->publish(&p);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+}
+
+// Only the same fields that are transmitted over LoRa should be set in MQTT messages.
+void test_receiveIgnoresUnexpectedFields(void)
+{
+ meshtastic_MeshPacket input = decoded;
+ input.rx_snr = 10;
+ input.rx_rssi = 20;
+
+ unitTest->publish(&input);
+
+ TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
+ const meshtastic_MeshPacket &p = mockRouter->packets_.front();
+ TEST_ASSERT_EQUAL(0, p.rx_snr);
+ TEST_ASSERT_EQUAL(0, p.rx_rssi);
+}
+
+// Messages with an invalid hop_limit are ignored.
+void test_receiveIgnoresInvalidHopLimit(void)
+{
+ meshtastic_MeshPacket p = decoded;
+ p.hop_limit = 10;
+
+ unitTest->publish(&p);
+
+ TEST_ASSERT_TRUE(mockRouter->packets_.empty());
+}
+
+// Publishing to a text channel.
+void test_publishTextMessageDirect(void)
+{
+ TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0));
+
+ TEST_ASSERT_EQUAL(1, pubsub->published_.size());
+ const auto &[topic, payload] = pubsub->published_.front();
+ TEST_ASSERT_EQUAL_STRING("payload", std::get(payload).c_str());
+}
+
+// Publishing to a text channel via the MQTT client proxy.
+void test_publishTextMessageWithProxy(void)
+{
+ moduleConfig.mqtt.proxy_to_client_enabled = true;
+
+ TEST_ASSERT_TRUE(mqtt->publish(MockPubSubServer::kTextTopic, "payload", 0));
+
+ TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
+ const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
+ TEST_ASSERT_EQUAL_STRING(MockPubSubServer::kTextTopic, message.topic);
+ TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_text_tag, message.which_payload_variant);
+ TEST_ASSERT_EQUAL_STRING("payload", message.payload_variant.text);
+}
+
+// Helper method to verify the expected latitude/longitude was received.
+void verifyLatLong(const DecodedServiceEnvelope &env, uint32_t latitude, uint32_t longitude)
+{
+ TEST_ASSERT_TRUE(env.validDecode);
+ const meshtastic_MeshPacket &p = *env.packet;
+ TEST_ASSERT_EQUAL(NODENUM_BROADCAST, p.to);
+ TEST_ASSERT_EQUAL(meshtastic_MeshPacket_decoded_tag, p.which_payload_variant);
+ TEST_ASSERT_EQUAL(meshtastic_PortNum_MAP_REPORT_APP, p.decoded.portnum);
+
+ meshtastic_MapReport mapReport;
+ TEST_ASSERT_TRUE(
+ pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_MapReport_msg, &mapReport));
+ TEST_ASSERT_EQUAL(latitude, mapReport.latitude_i);
+ TEST_ASSERT_EQUAL(longitude, mapReport.longitude_i);
+}
+
+// Map reporting defaults to an imprecise location.
+void test_reportToMapDefaultImprecise(void)
+{
+ unitTest->reportToMap();
+
+ TEST_ASSERT_EQUAL(1, pubsub->published_.size());
+ const auto &[topic, payload] = pubsub->published_.front();
+ TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str());
+ verifyLatLong(std::get(payload), 70123520, 30015488);
+}
+
+// Precise location is reported when configured.
+void test_reportToMapPrecise(void)
+{
+ unitTest->reportToMap(/*precision=*/32);
+
+ TEST_ASSERT_EQUAL(1, pubsub->published_.size());
+ const auto &[topic, payload] = pubsub->published_.front();
+ TEST_ASSERT_EQUAL_STRING("msh/2/map/", topic.c_str());
+ verifyLatLong(std::get(payload), localPosition.latitude_i, localPosition.longitude_i);
+}
+
+// Location is sent over the phone proxy.
+void test_reportToMapPreciseProxied(void)
+{
+ moduleConfig.mqtt.proxy_to_client_enabled = true;
+ MQTTUnitTest::restart();
+
+ unitTest->reportToMap(/*precision=*/32);
+
+ TEST_ASSERT_EQUAL(1, mockMeshService->messages_.size());
+ const meshtastic_MqttClientProxyMessage &message = mockMeshService->messages_.front();
+ TEST_ASSERT_EQUAL_STRING("msh/2/map/", message.topic);
+ TEST_ASSERT_EQUAL(meshtastic_MqttClientProxyMessage_data_tag, message.which_payload_variant);
+ const DecodedServiceEnvelope env(message.payload_variant.data.bytes, message.payload_variant.data.size);
+ verifyLatLong(env, localPosition.latitude_i, localPosition.longitude_i);
+}
+
+// No location is reported when the precision is invalid.
+void test_reportToMapInvalidPrecision(void)
+{
+ unitTest->reportToMap(/*precision=*/0);
+
+ TEST_ASSERT_TRUE(pubsub->published_.empty());
+}
+
+// isUsingDefaultServer returns true when using the default server.
+void test_usingDefaultServer(void)
+{
+ TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer());
+}
+
+// isUsingDefaultServer returns true when using the default server and a port.
+void test_usingDefaultServerWithPort(void)
+{
+ std::string server = default_mqtt_address;
+ server += ":1883";
+ strcpy(moduleConfig.mqtt.address, server.c_str());
+ MQTTUnitTest::restart();
+
+ TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer());
+}
+
+// isUsingDefaultServer returns true when using the default server and invalid port.
+void test_usingDefaultServerWithInvalidPort(void)
+{
+ std::string server = default_mqtt_address;
+ server += ":invalid";
+ strcpy(moduleConfig.mqtt.address, server.c_str());
+ MQTTUnitTest::restart();
+
+ TEST_ASSERT_TRUE(mqtt->isUsingDefaultServer());
+}
+
+// isUsingDefaultServer returns false when not using the default server.
+void test_usingCustomServer(void)
+{
+ strcpy(moduleConfig.mqtt.address, "custom");
+ MQTTUnitTest::restart();
+
+ TEST_ASSERT_FALSE(mqtt->isUsingDefaultServer());
+}
+
+// Test that isEnabled returns true the MQTT module is enabled.
+void test_enabled(void)
+{
+ TEST_ASSERT_TRUE(mqtt->isEnabled());
+}
+
+// Test that isEnabled returns false the MQTT module not enabled.
+void test_disabled(void)
+{
+ moduleConfig.mqtt.enabled = false;
+ MQTTUnitTest::restart();
+
+ TEST_ASSERT_FALSE(mqtt->isEnabled());
+}
+
+// Subscriptions contain the moduleConfig.mqtt.root prefix.
+void test_customMqttRoot(void)
+{
+ strcpy(moduleConfig.mqtt.root, "custom");
+ MQTTUnitTest::restart();
+
+ TEST_ASSERT_TRUE(loopUntil(
+ [] { return pubsub->subscriptions_.count("custom/2/e/test/+") && pubsub->subscriptions_.count("custom/2/e/PKI/+"); }));
+}
+
+void setup()
+{
+ initializeTestEnvironment();
+ const std::unique_ptr mockNodeDB(new MockNodeDB());
+ nodeDB = mockNodeDB.get();
+
+ UNITY_BEGIN();
+ RUN_TEST(test_sendDirectlyConnectedDecoded);
+ RUN_TEST(test_sendDirectlyConnectedEncrypted);
+ RUN_TEST(test_proxyToMeshServiceDecoded);
+ RUN_TEST(test_proxyToMeshServiceEncrypted);
+ RUN_TEST(test_dontMqttMeOnPublicServer);
+ RUN_TEST(test_okToMqttOnPrivateServer);
+ RUN_TEST(test_noRangeTestAppOnDefaultServer);
+ RUN_TEST(test_noDetectionSensorAppOnDefaultServer);
+ RUN_TEST(test_sendQueued);
+ RUN_TEST(test_reconnectProxyDoesNotReconnectMqtt);
+ RUN_TEST(test_receiveEmptyMeshPacket);
+ RUN_TEST(test_receiveDecodedProto);
+ RUN_TEST(test_receiveDecodedProtoFromProxy);
+ RUN_TEST(test_receiveEmptyDataFromProxy);
+ RUN_TEST(test_receiveWithoutChannelDownlink);
+ RUN_TEST(test_receiveEncryptedPKITopicToUs);
+ RUN_TEST(test_receiveIgnoresOwnPublishedMessages);
+ RUN_TEST(test_receiveAcksOwnSentMessages);
+ RUN_TEST(test_receiveIgnoresSentMessagesFromOthers);
+ RUN_TEST(test_receiveIgnoresDecodedWhenEncryptionEnabled);
+ RUN_TEST(test_receiveIgnoresDecodedAdminApp);
+ RUN_TEST(test_receiveIgnoresUnexpectedFields);
+ RUN_TEST(test_receiveIgnoresInvalidHopLimit);
+ RUN_TEST(test_publishTextMessageDirect);
+ RUN_TEST(test_publishTextMessageWithProxy);
+ RUN_TEST(test_reportToMapDefaultImprecise);
+ RUN_TEST(test_reportToMapPrecise);
+ RUN_TEST(test_reportToMapPreciseProxied);
+ RUN_TEST(test_reportToMapInvalidPrecision);
+ RUN_TEST(test_usingDefaultServer);
+ RUN_TEST(test_usingDefaultServerWithPort);
+ RUN_TEST(test_usingDefaultServerWithInvalidPort);
+ RUN_TEST(test_usingCustomServer);
+ RUN_TEST(test_enabled);
+ RUN_TEST(test_disabled);
+ RUN_TEST(test_customMqttRoot);
+ exit(UNITY_END());
+}
+#else
+void setup()
+{
+ initializeTestEnvironment();
+ LOG_WARN("This test requires the ARCH_PORTDUINO variant of WiFiClient");
+ UNITY_BEGIN();
+ UNITY_END();
+}
+#endif
+void loop() {}
\ No newline at end of file