Merge branch 'master' into store-and-forward

This commit is contained in:
Thomas Göttgens 2025-02-19 13:22:11 +01:00 committed by GitHub
commit e67b84ee06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 270 additions and 91 deletions

BIN
.github/meshtastic_logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -4,9 +4,11 @@ on:
- cron: 0 8 * * 1-5
workflow_dispatch: {}
permissions: read-all
jobs:
trunk_check:
name: Trunk Check Upload
name: Trunk Check and Upload
runs-on: ubuntu-latest
steps:
@ -14,6 +16,20 @@ jobs:
uses: actions/checkout@v4
- name: Trunk Check
uses: trunk-io/trunk-action@782e83f803ca6e369f035d64c6ba2768174ba61b
uses: trunk-io/trunk-action@v1
with:
trunk-token: ${{ secrets.TRUNK_TOKEN }}
trunk_upgrade:
name: Trunk Upgrade (PR)
runs-on: ubuntu-latest
permissions:
contents: write # For trunk to create PRs
pull-requests: write # For trunk to create PRs
steps:
- name: Checkout
uses: actions/checkout@v4
# See https://github.com/trunk-io/trunk-action/blob/v1/readme.md#automatic-upgrades
- name: Trunk Upgrade
uses: trunk-io/trunk-action/upgrade@v1

26
.github/workflows/trunk_annotate.pr.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Annotate PR with trunk issues
# See: https://github.com/trunk-io/trunk-action/blob/v1/readme.md#getting-inline-annotations-for-fork-prs
on:
workflow_run:
workflows: [Pull Request] # Name from `trunk_check.yml`
types: [completed]
permissions: read-all
jobs:
trunk_check:
name: Trunk Code Quality Annotate
runs-on: ubuntu-latest
permissions:
checks: write # For trunk to post annotations
contents: read # For repo checkout
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Trunk Check
uses: trunk-io/trunk-action@v1
with:
post-annotations: true

View File

@ -1,4 +1,7 @@
# Meshtastic Firmware
<div align="center" markdown="1">
<img src=".github/meshtastic_logo.png" alt="Meshtastic Logo" width="80"/>
<h1>Meshtastic Firmware</h1>
![GitHub release downloads](https://img.shields.io/github/downloads/meshtastic/firmware/total)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/firmware/main_matrix.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/firmware/actions/workflows/ci.yml)
@ -6,13 +9,31 @@
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
<a href="https://trendshift.io/repositories/5524" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5524" alt="meshtastic%2Ffirmware | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
</div>
<div align="center">
<a href="https://meshtastic.org">Website</a>
-
<a href="https://meshtastic.org/docs/">Documentation</a>
</div>
## Overview
This repository contains the device firmware for the Meshtastic project.
This repository contains the official device firmware for Meshtastic, an open-source LoRa mesh networking project designed for long-range, low-power communication without relying on internet or cellular infrastructure. The firmware supports various hardware platforms, including ESP32, nRF52, RP2040/RP2350, and Linux-based devices.
- **[Building Instructions](https://meshtastic.org/docs/development/firmware/build)**
- **[Flashing Instructions](https://meshtastic.org/docs/getting-started/flashing-firmware/)**
Meshtastic enables text messaging, location sharing, and telemetry over a decentralized mesh network, making it ideal for outdoor adventures, emergency preparedness, and remote operations.
### Get Started
- 🔧 **[Building Instructions](https://meshtastic.org/docs/development/firmware/build)** Learn how to compile the firmware from source.
- ⚡ **[Flashing Instructions](https://meshtastic.org/docs/getting-started/flashing-firmware/)** Install or update the firmware on your device.
Join our community and help improve Meshtastic! 🚀
## Stats
![Alt](https://repobeats.axiom.co/api/embed/a92f097d9197ae853e780ec53d7d126e545629ab.svg "Repobeats analytics image")
![Alt](https://repobeats.axiom.co/api/embed/8025e56c482ec63541593cc5bd322c19d5c0bdcf.svg "Repobeats analytics image")

View File

@ -291,8 +291,8 @@ extern int unishox2_decompress_simple(const char *in, int len, char *out);
* @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided
* @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples.
* @param[in] usx_hcode_lens Length of each element in usx_hcodes array
* @param[in] usx_freq_seq Frequently occuring sequences. See USX_FREQ_SEQ_* macros for samples
* @param[in] usx_templates Templates of frequently occuring patterns. See USX_TEMPLATES macro.
* @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples
* @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro.
*/
extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[],
@ -310,8 +310,8 @@ extern int unishox2_compress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(ch
* @param[in] olen length of 'out' buffer in bytes. Can be omitted if sufficient buffer is provided
* @param[in] usx_hcodes Horizontal codes (array of bytes). See macro section for samples.
* @param[in] usx_hcode_lens Length of each element in usx_hcodes array
* @param[in] usx_freq_seq Frequently occuring sequences. See USX_FREQ_SEQ_* macros for samples
* @param[in] usx_templates Templates of frequently occuring patterns. See USX_TEMPLATES macro.
* @param[in] usx_freq_seq Frequently occurring sequences. See USX_FREQ_SEQ_* macros for samples
* @param[in] usx_templates Templates of frequently occurring patterns. See USX_TEMPLATES macro.
*/
extern int unishox2_decompress(const char *in, int len, UNISHOX_API_OUT_AND_LEN(char *out, int olen),
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[], const char *usx_freq_seq[],
@ -344,4 +344,4 @@ extern int unishox2_decompress_lines(const char *in, int len, UNISHOX_API_OUT_AN
const unsigned char usx_hcodes[], const unsigned char usx_hcode_lens[],
const char *usx_freq_seq[], const char *usx_templates[], struct us_lnk_lst *prev_lines);
#endif
#endif

View File

@ -41,7 +41,6 @@ MQTT *mqtt;
namespace
{
constexpr int reconnectMax = 5;
constexpr uint16_t mqttPort = 1883;
// FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets
static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for channel name and 16 for nodeid
@ -251,6 +250,68 @@ bool isDefaultServer(const String &host)
{
return host.length() == 0 || host == default_mqtt_address;
}
struct PubSubConfig {
explicit PubSubConfig(const meshtastic_ModuleConfig_MQTTConfig &config)
{
if (*config.address) {
serverAddr = config.address;
mqttUsername = config.username;
mqttPassword = config.password;
}
if (config.tls_enabled) {
serverPort = 8883;
}
std::tie(serverAddr, serverPort) = parseHostAndPort(serverAddr.c_str(), serverPort);
}
// Defaults
static constexpr uint16_t defaultPort = 1883;
uint16_t serverPort = defaultPort;
String serverAddr = default_mqtt_address;
const char *mqttUsername = default_mqtt_username;
const char *mqttPassword = default_mqtt_password;
};
#if HAS_NETWORKING
bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &client)
{
pubSub.setBufferSize(1024);
pubSub.setClient(client);
pubSub.setServer(config.serverAddr.c_str(), config.serverPort);
LOG_INFO("Connecting directly to MQTT server %s, port: %d, username: %s, password: %s", config.serverAddr.c_str(),
config.serverPort, config.mqttUsername, config.mqttPassword);
const bool connected = pubSub.connect(owner.id, config.mqttUsername, config.mqttPassword);
if (connected) {
LOG_INFO("MQTT connected");
} else {
LOG_WARN("Failed to connect to MQTT server");
}
return connected;
}
#endif
inline bool isConnectedToNetwork()
{
#if HAS_WIFI
return WiFi.isConnected();
#elif HAS_ETHERNET
return Ethernet.linkStatus() == LinkON;
#else
return false;
#endif
}
/** return true if we have a channel that wants uplink/downlink or map reporting is enabled
*/
bool wantsLink()
{
const bool hasChannelorMapReport =
moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled());
return hasChannelorMapReport && (moduleConfig.mqtt.proxy_to_client_enabled || isConnectedToNetwork());
}
} // namespace
void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length)
@ -413,46 +474,18 @@ void MQTT::reconnect()
return; // Don't try to connect directly to the server
}
#if HAS_NETWORKING
// Defaults
int serverPort = mqttPort;
const char *serverAddr = default_mqtt_address;
const char *mqttUsername = default_mqtt_username;
const char *mqttPassword = default_mqtt_password;
const PubSubConfig config(moduleConfig.mqtt);
MQTTClient *clientConnection = mqttClient.get();
if (*moduleConfig.mqtt.address) {
serverAddr = moduleConfig.mqtt.address;
mqttUsername = moduleConfig.mqtt.username;
mqttPassword = moduleConfig.mqtt.password;
}
#if HAS_WIFI && !defined(ARCH_PORTDUINO) && !defined(CONFIG_IDF_TARGET_ESP32C6)
#if MQTT_SUPPORTS_TLS
if (moduleConfig.mqtt.tls_enabled) {
// change default for encrypted to 8883
try {
serverPort = 8883;
wifiSecureClient.setInsecure();
LOG_INFO("Use TLS-encrypted session");
clientConnection = &wifiSecureClient;
} catch (const std::exception &e) {
LOG_ERROR("MQTT ERROR: %s", e.what());
}
mqttClientTLS.setInsecure();
LOG_INFO("Use TLS-encrypted session");
clientConnection = &mqttClientTLS;
} else {
LOG_INFO("Use non-TLS-encrypted session");
}
#endif
std::pair<String, uint16_t> hostAndPort = parseHostAndPort(serverAddr, serverPort);
serverAddr = hostAndPort.first.c_str();
serverPort = hostAndPort.second;
pubSub.setServer(serverAddr, serverPort);
pubSub.setBufferSize(1024);
LOG_INFO("Connect directly to MQTT server %s, port: %d, username: %s, password: %s", serverAddr, serverPort, mqttUsername,
mqttPassword);
pubSub.setClient(*clientConnection);
bool connected = pubSub.connect(owner.id, mqttUsername, mqttPassword);
if (connected) {
LOG_INFO("MQTT connected");
if (connectPubSub(config, pubSub, *clientConnection)) {
enabled = true; // Start running background process again
runASAP = true;
reconnectCount = 0;
@ -507,23 +540,6 @@ void MQTT::sendSubscriptions()
#endif
}
bool MQTT::wantsLink() const
{
bool hasChannelorMapReport =
moduleConfig.mqtt.enabled && (moduleConfig.mqtt.map_reporting_enabled || channels.anyMqttEnabled());
if (hasChannelorMapReport && moduleConfig.mqtt.proxy_to_client_enabled)
return true;
#if HAS_WIFI
return hasChannelorMapReport && WiFi.isConnected();
#endif
#if HAS_ETHERNET
return hasChannelorMapReport && Ethernet.linkStatus() == LinkON;
#endif
return false;
}
int32_t MQTT::runOnce()
{
#if HAS_NETWORKING
@ -567,18 +583,42 @@ int32_t MQTT::runOnce()
return 30000;
}
bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config)
bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client)
{
String host;
uint16_t port;
std::tie(host, port) = parseHostAndPort(config.address, mqttPort);
const bool defaultServer = isDefaultServer(host);
const PubSubConfig parsed(config);
if (config.enabled && !config.proxy_to_client_enabled) {
#if HAS_NETWORKING
std::unique_ptr<MQTTClient> clientConnection;
if (config.tls_enabled) {
#if MQTT_SUPPORTS_TLS
MQTTClientTLS *tlsClient = new MQTTClientTLS;
clientConnection.reset(tlsClient);
tlsClient->setInsecure();
#else
LOG_ERROR("Invalid MQTT config: tls_enabled is not supported on this node");
return false;
#endif
} else {
clientConnection.reset(new MQTTClient);
}
std::unique_ptr<PubSubClient> pubSub(new PubSubClient);
if (isConnectedToNetwork()) {
return connectPubSub(parsed, *pubSub, (client != nullptr) ? *client : *clientConnection);
}
#else
LOG_ERROR("Invalid MQTT config: proxy_to_client_enabled must be enabled on nodes that do not have a network");
return false;
#endif
}
const bool defaultServer = isDefaultServer(parsed.serverAddr);
if (defaultServer && config.tls_enabled) {
LOG_ERROR("Invalid MQTT config: TLS was enabled, but the default server does not support TLS");
return false;
}
if (defaultServer && port != mqttPort) {
LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", port);
if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) {
LOG_ERROR("Invalid MQTT config: Unsupported port '%d' for the default MQTT server", parsed.serverPort);
return false;
}
return true;

View File

@ -10,12 +10,10 @@
#endif
#if HAS_WIFI
#include <WiFiClient.h>
#if !defined(ARCH_PORTDUINO)
#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR < 3
#if __has_include(<WiFiClientSecure.h>)
#include <WiFiClientSecure.h>
#endif
#endif
#endif
#if HAS_ETHERNET
#include <EthernetClient.h>
#endif
@ -61,7 +59,8 @@ class MQTT : private concurrency::OSThread
bool isUsingDefaultServer() { return isConfiguredForDefaultServer; }
static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config);
/// Validate the meshtastic_ModuleConfig_MQTTConfig.
static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config) { return isValidConfig(config, nullptr); }
protected:
struct QueueEntry {
@ -78,22 +77,23 @@ class MQTT : private concurrency::OSThread
#ifndef PIO_UNIT_TESTING
private:
#endif
// supposedly the current version is busted:
// http://www.iotsharing.com/2017/08/how-to-use-esp32-mqtts-with-mqtts-mosquitto-broker-tls-ssl.html
#if HAS_WIFI
using MQTTClient = WiFiClient;
#if !defined(ARCH_PORTDUINO)
#if (defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR < 3) || defined(RPI_PICO)
WiFiClientSecure wifiSecureClient;
#if __has_include(<WiFiClientSecure.h>)
using MQTTClientTLS = WiFiClientSecure;
#define MQTT_SUPPORTS_TLS 1
#endif
#endif
#endif
#if HAS_ETHERNET
#elif HAS_ETHERNET
using MQTTClient = EthernetClient;
#else
using MQTTClient = void;
#endif
#if HAS_NETWORKING
std::unique_ptr<MQTTClient> mqttClient;
#if MQTT_SUPPORTS_TLS
MQTTClientTLS mqttClientTLS;
#endif
PubSubClient pubSub;
explicit MQTT(std::unique_ptr<MQTTClient> mqttClient);
#endif
@ -109,10 +109,6 @@ class MQTT : private concurrency::OSThread
uint32_t map_position_precision = default_map_position_precision;
uint32_t map_publish_interval_msecs = default_map_publish_interval_secs * 1000;
/** return true if we have a channel that wants uplink/downlink or map reporting is enabled
*/
bool wantsLink() const;
/** Attempt to connect to server if necessary
*/
void reconnect();
@ -124,6 +120,8 @@ class MQTT : private concurrency::OSThread
/// Callback for direct mqtt subscription messages
static void mqttCallback(char *topic, byte *payload, unsigned int length);
static bool isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTClient *client);
/// Called when a new publish arrives from the MQTT server
void onReceive(char *topic, byte *payload, size_t length);

View File

@ -220,7 +220,11 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_RouteDiscovery_msg,
&scratch)) {
decoded = &scratch;
JSONArray route; // Route this message took
JSONArray route; // Route this message took
JSONArray routeBack; // Route this message took back
JSONArray snrTowards; // Snr for forward route
JSONArray snrBack; // Snr for reverse route
// Lambda function for adding a long name to the route
auto addToRoute = [](JSONArray *route, NodeNum num) {
char long_name[40] = "Unknown";
@ -236,7 +240,24 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
}
addToRoute(&route, mp->from); // Ended at the original destination (source of response)
addToRoute(&routeBack, mp->from); // Started at the original destination (source of response)
for (uint8_t i = 0; i < decoded->route_back_count; i++) {
addToRoute(&routeBack, decoded->route_back[i]);
}
addToRoute(&routeBack, mp->to); // Ended at the original transmitter (destination of response)
for (uint8_t i = 0; i < decoded->snr_back_count; i++) {
snrBack.push_back(new JSONValue((float)decoded->snr_back[i] / 4));
}
for (uint8_t i = 0; i < decoded->snr_towards_count; i++) {
snrTowards.push_back(new JSONValue((float)decoded->snr_towards[i] / 4));
}
msgPayload["route"] = new JSONValue(route);
msgPayload["route_back"] = new JSONValue(routeBack);
msgPayload["snr_back"] = new JSONValue(snrBack);
msgPayload["snr_towards"] = new JSONValue(snrTowards);
jsonObj["payload"] = new JSONValue(msgPayload);
} else if (shouldLog) {
LOG_ERROR(errStr, msgType.c_str());

View File

@ -94,6 +94,7 @@ class MockPubSubServer : public WiFiClient
int connect(IPAddress ip, uint16_t port) override
{
port_ = port;
if (refuseConnection_)
return 0;
connected_ = true;
@ -101,6 +102,8 @@ class MockPubSubServer : public WiFiClient
}
int connect(const char *host, uint16_t port) override
{
host_ = host;
port_ = port;
if (refuseConnection_)
return 0;
connected_ = true;
@ -197,6 +200,8 @@ class MockPubSubServer : public WiFiClient
bool connected_ = false;
bool refuseConnection_ = false; // Simulate a failed connection.
uint32_t ipAddress_ = 0x01010101; // IP address of the MQTT server.
std::string host_; // Requested host.
uint16_t port_; // Requested port.
std::list<std::string> buffer_; // Buffer of messages for the pubSub client to receive.
std::string command_; // Current command received from the pubSub client.
std::set<std::string> subscriptions_; // Topics that the pubSub client has subscribed to.
@ -242,6 +247,7 @@ class MQTTUnitTest : public MQTT
mqttClient.release();
delete pubsub;
}
using MQTT::isValidConfig;
using MQTT::reconnect;
int queueSize() { return mqttQueue.numUsed(); }
void reportToMap(std::optional<uint32_t> precision = std::nullopt)
@ -801,13 +807,25 @@ void test_customMqttRoot(void)
}
// Empty configuration is valid.
void test_configurationEmptyIsValid(void)
void test_configEmptyIsValid(void)
{
meshtastic_ModuleConfig_MQTTConfig config;
meshtastic_ModuleConfig_MQTTConfig config = {};
TEST_ASSERT_TRUE(MQTT::isValidConfig(config));
}
// Empty 'enabled' configuration is valid.
void test_configEnabledEmptyIsValid(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true};
MockPubSubServer client;
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
TEST_ASSERT_TRUE(client.connected_);
TEST_ASSERT_EQUAL_STRING(default_mqtt_address, client.host_.c_str());
TEST_ASSERT_EQUAL(1883, client.port_);
}
// Configuration with the default server is valid.
void test_configWithDefaultServer(void)
{
@ -832,6 +850,41 @@ void test_configWithDefaultServerAndInvalidTLSEnabled(void)
TEST_ASSERT_FALSE(MQTT::isValidConfig(config));
}
// isValidConfig connects to a custom host and port.
void test_configCustomHostAndPort(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server:1234"};
MockPubSubServer client;
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
TEST_ASSERT_TRUE(client.connected_);
TEST_ASSERT_EQUAL_STRING("server", client.host_.c_str());
TEST_ASSERT_EQUAL(1234, client.port_);
}
// isValidConfig returns false if a connection cannot be established.
void test_configWithConnectionFailure(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server"};
MockPubSubServer client;
client.refuseConnection_ = true;
TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client));
}
// isValidConfig returns true when tls_enabled is supported, or false otherwise.
void test_configWithTLSEnabled(void)
{
meshtastic_ModuleConfig_MQTTConfig config = {.enabled = true, .address = "server", .tls_enabled = true};
MockPubSubServer client;
#if MQTT_SUPPORTS_TLS
TEST_ASSERT_TRUE(MQTTUnitTest::isValidConfig(config, &client));
#else
TEST_ASSERT_FALSE(MQTTUnitTest::isValidConfig(config, &client));
#endif
}
void setup()
{
initializeTestEnvironment();
@ -875,10 +928,14 @@ void setup()
RUN_TEST(test_enabled);
RUN_TEST(test_disabled);
RUN_TEST(test_customMqttRoot);
RUN_TEST(test_configurationEmptyIsValid);
RUN_TEST(test_configEmptyIsValid);
RUN_TEST(test_configEnabledEmptyIsValid);
RUN_TEST(test_configWithDefaultServer);
RUN_TEST(test_configWithDefaultServerAndInvalidPort);
RUN_TEST(test_configWithDefaultServerAndInvalidTLSEnabled);
RUN_TEST(test_configCustomHostAndPort);
RUN_TEST(test_configWithConnectionFailure);
RUN_TEST(test_configWithTLSEnabled);
exit(UNITY_END());
}
#else