Skip to content

Commit

Permalink
Merge pull request #3365 from GUVWAF/mapReport
Browse files Browse the repository at this point in the history
Periodic reporting of device information to a map via MQTT
  • Loading branch information
caveman99 authored Mar 10, 2024
2 parents e33d014 + 70df36b commit eb372c1
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 23 deletions.
20 changes: 20 additions & 0 deletions src/mesh/Channels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "CryptoEngine.h"
#include "DisplayFormatters.h"
#include "NodeDB.h"
#include "RadioInterface.h"
#include "configuration.h"

#include <assert.h>
Expand Down Expand Up @@ -254,6 +255,25 @@ const char *Channels::getName(size_t chIndex)
return channelName;
}

bool Channels::hasDefaultChannel()
{
// If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel
if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency)
return false;
// Check if any of the channels are using the default name and PSK
for (size_t i = 0; i < getNumChannels(); i++) {
const auto &ch = getByIndex(i);
if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) {
const char *name = getName(i);
const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
// Check if the name is the default derived from the modem preset
if (strcmp(name, presetName) == 0)
return true;
}
}
return false;
}

/**
* Generate a short suffix used to disambiguate channels that might have the same "name" entered by the human but different PSKs.
* The ideas is that the PSK changing should be visible to the user so that they see they probably messed up and that's why they
Expand Down
3 changes: 3 additions & 0 deletions src/mesh/Channels.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ class Channels
*/
int16_t setActiveByIndex(ChannelIndex channelIndex);

// Returns true if we can be reached via a channel with the default settings given a region and modem preset
bool hasDefaultChannel();

private:
/** Given a channel index, change to use the crypto key specified by that index
*
Expand Down
7 changes: 5 additions & 2 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -744,14 +744,17 @@ uint32_t sinceReceived(const meshtastic_MeshPacket *p)

#define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline

size_t NodeDB::getNumOnlineMeshNodes()
size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
{
size_t numseen = 0;

// FIXME this implementation is kinda expensive
for (int i = 0; i < *numMeshNodes; i++)
for (int i = 0; i < *numMeshNodes; i++) {
if (localOnly && meshNodes[i].via_mqtt)
continue;
if (sinceLastSeen(&meshNodes[i]) < NUM_ONLINE_SECS)
numseen++;
}

return numseen;
}
Expand Down
8 changes: 5 additions & 3 deletions src/mesh/NodeDB.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ class NodeDB
// get channel channel index we heard a nodeNum on, defaults to 0 if not found
uint8_t getMeshNodeChannel(NodeNum n);

/// Return the number of nodes we've heard from recently (within the last 2 hrs?)
size_t getNumOnlineMeshNodes();
/* Return the number of nodes we've heard from recently (within the last 2 hrs?)
* @param localOnly if true, ignore nodes heard via MQTT
*/
size_t getNumOnlineMeshNodes(bool localOnly = false);

void initConfigIntervals(), initModuleConfigIntervals(), resetNodes(), removeNodeByNum(uint nodeNum);

Expand Down Expand Up @@ -246,4 +248,4 @@ extern uint32_t error_address;
#define Module_Config_size \
(ModuleConfig_CannedMessageConfig_size + ModuleConfig_ExternalNotificationConfig_size + ModuleConfig_MQTTConfig_size + \
ModuleConfig_RangeTestConfig_size + ModuleConfig_SerialConfig_size + ModuleConfig_StoreForwardConfig_size + \
ModuleConfig_TelemetryConfig_size + ModuleConfig_size)
ModuleConfig_TelemetryConfig_size + ModuleConfig_size)
6 changes: 6 additions & 0 deletions src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "RadioInterface.h"
#include "Channels.h"
#include "DisplayFormatters.h"
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
Expand Down Expand Up @@ -143,6 +144,7 @@ const RegionInfo regions[] = {
};

const RegionInfo *myRegion;
bool RadioInterface::uses_default_frequency_slot = true;

static uint8_t bytes[MAX_RHPACKETLEN];

Expand Down Expand Up @@ -486,6 +488,10 @@ void RadioInterface::applyModemConfig()
// channel_num is actually (channel_num - 1), since modulus (%) returns values from 0 to (numChannels - 1)
int channel_num = (loraConfig.channel_num ? loraConfig.channel_num - 1 : hash(channelName)) % numChannels;

// Check if we use the default frequency slot
RadioInterface::uses_default_frequency_slot =
channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false)) % numChannels;

// Old frequency selection formula
// float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num);

Expand Down
3 changes: 3 additions & 0 deletions src/mesh/RadioInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ class RadioInterface
/// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers
virtual bool isIRQPending() { return false; }

// Whether we use the default frequency slot given our LoRa config (region and modem preset)
static bool uses_default_frequency_slot;

protected:
int8_t power = 17; // Set by applyModemConfig()

Expand Down
109 changes: 96 additions & 13 deletions src/mqtt/MQTT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length)
if (jsonPayloadStr.length() <= sizeof(p->decoded.payload.bytes)) {
memcpy(p->decoded.payload.bytes, jsonPayloadStr.c_str(), jsonPayloadStr.length());
p->decoded.payload.size = jsonPayloadStr.length();
meshtastic_MeshPacket *packet = packetPool.allocCopy(*p);
service.sendToMesh(packet, RX_SRC_LOCAL);
service.sendToMesh(p, RX_SRC_LOCAL);
} else {
LOG_WARN("Received MQTT json payload too long, dropping\n");
}
Expand Down Expand Up @@ -186,10 +185,17 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE)
statusTopic = moduleConfig.mqtt.root + statusTopic;
cryptTopic = moduleConfig.mqtt.root + cryptTopic;
jsonTopic = moduleConfig.mqtt.root + jsonTopic;
mapTopic = moduleConfig.mqtt.root + jsonTopic;
} else {
statusTopic = "msh" + statusTopic;
cryptTopic = "msh" + cryptTopic;
jsonTopic = "msh" + jsonTopic;
mapTopic = "msh" + mapTopic;
}

if (moduleConfig.mqtt.map_reporting_enabled && moduleConfig.mqtt.has_map_report_settings) {
map_position_precision = moduleConfig.mqtt.map_report_settings.position_precision;
map_publish_interval_secs = moduleConfig.mqtt.map_report_settings.publish_interval_secs;
}

#ifdef HAS_NETWORKING
Expand Down Expand Up @@ -365,27 +371,30 @@ void MQTT::sendSubscriptions()

bool MQTT::wantsLink() const
{
bool hasChannel = false;
bool hasChannelorMapReport = false;

if (moduleConfig.mqtt.enabled) {
// No need for link if no channel needed it
size_t numChan = channels.getNumChannels();
for (size_t i = 0; i < numChan; i++) {
const auto &ch = channels.getByIndex(i);
if (ch.settings.uplink_enabled || ch.settings.downlink_enabled) {
hasChannel = true;
break;
hasChannelorMapReport = moduleConfig.mqtt.map_reporting_enabled;
if (!hasChannelorMapReport) {
// No need for link if no channel needed it
size_t numChan = channels.getNumChannels();
for (size_t i = 0; i < numChan; i++) {
const auto &ch = channels.getByIndex(i);
if (ch.settings.uplink_enabled || ch.settings.downlink_enabled) {
hasChannelorMapReport = true;
break;
}
}
}
}
if (hasChannel && moduleConfig.mqtt.proxy_to_client_enabled)
if (hasChannelorMapReport && moduleConfig.mqtt.proxy_to_client_enabled)
return true;

#if HAS_WIFI
return hasChannel && WiFi.isConnected();
return hasChannelorMapReport && WiFi.isConnected();
#endif
#if HAS_ETHERNET
return hasChannel && Ethernet.linkStatus() == LinkON;
return hasChannelorMapReport && Ethernet.linkStatus() == LinkON;
#endif
return false;
}
Expand All @@ -397,6 +406,8 @@ int32_t MQTT::runOnce()

bool wantConnection = wantsLink();

perhapsReportToMap();

// If connected poll rapidly, otherwise only occasionally check for a wifi connection change and ability to contact server
if (moduleConfig.mqtt.proxy_to_client_enabled) {
publishQueuedMessages();
Expand Down Expand Up @@ -536,6 +547,78 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp, const meshtastic_MeshPacket &
}
}

void MQTT::perhapsReportToMap()
{
if (!moduleConfig.mqtt.map_reporting_enabled || !(moduleConfig.mqtt.proxy_to_client_enabled || isConnectedDirectly()))
return;

if (map_position_precision == 0 || (localPosition.latitude_i == 0 && localPosition.longitude_i == 0)) {
LOG_WARN("MQTT Map reporting is enabled, but precision is 0 or no position available.\n");
return;
}

if (millis() - last_report_to_map < map_publish_interval_secs * 1000) {
return;
} else {
// Allocate ServiceEnvelope and fill it
meshtastic_ServiceEnvelope *se = mqttPool.allocZeroed();
se->channel_id = (char *)channels.getGlobalId(channels.getPrimaryIndex()); // Use primary channel as the channel_id
se->gateway_id = owner.id;

// Allocate MeshPacket and fill it
meshtastic_MeshPacket *mp = packetPool.allocZeroed();
mp->which_payload_variant = meshtastic_MeshPacket_decoded_tag;
mp->from = nodeDB.getNodeNum();
mp->to = NODENUM_BROADCAST;
mp->decoded.portnum = meshtastic_PortNum_MAP_REPORT_APP;

// Fill MapReport message
meshtastic_MapReport mapReport = meshtastic_MapReport_init_default;
memcpy(mapReport.long_name, owner.long_name, sizeof(owner.long_name));
memcpy(mapReport.short_name, owner.short_name, sizeof(owner.short_name));
mapReport.role = config.device.role;
mapReport.hw_model = owner.hw_model;
strncpy(mapReport.firmware_version, optstr(APP_VERSION), sizeof(mapReport.firmware_version));
mapReport.region = config.lora.region;
mapReport.modem_preset = config.lora.modem_preset;
mapReport.has_default_channel = channels.hasDefaultChannel();

// Set position with precision (same as in PositionModule)
if (map_position_precision < 32 && map_position_precision > 0) {
mapReport.latitude_i = localPosition.latitude_i & (UINT32_MAX << (32 - map_position_precision));
mapReport.longitude_i = localPosition.longitude_i & (UINT32_MAX << (32 - map_position_precision));
mapReport.latitude_i += (1 << (31 - map_position_precision));
mapReport.longitude_i += (1 << (31 - map_position_precision));
} else {
mapReport.latitude_i = localPosition.latitude_i;
mapReport.longitude_i = localPosition.longitude_i;
}
mapReport.altitude = localPosition.altitude;
mapReport.position_precision = map_position_precision;

mapReport.num_online_local_nodes = nodeDB.getNumOnlineMeshNodes(true);

// Encode MapReport message and set it to MeshPacket in ServiceEnvelope
mp->decoded.payload.size = pb_encode_to_bytes(mp->decoded.payload.bytes, sizeof(mp->decoded.payload.bytes),
&meshtastic_MapReport_msg, &mapReport);
se->packet = mp;

// FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets
static uint8_t bytes[meshtastic_MeshPacket_size + 64];
size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, se);

LOG_INFO("MQTT Publish map report to %s\n", mapTopic.c_str());
publish(mapTopic.c_str(), bytes, numBytes, false);

// Release the allocated memory for ServiceEnvelope and MeshPacket
mqttPool.release(se);
packetPool.release(mp);

// Update the last report time
last_report_to_map = millis();
}
}

// converts a downstream packet into a json message
std::string MQTT::meshPacketToJson(meshtastic_MeshPacket *mp)
{
Expand Down
20 changes: 15 additions & 5 deletions src/mqtt/MQTT.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,18 @@ class MQTT : private concurrency::OSThread
virtual int32_t runOnce() override;

private:
std::string statusTopic = "/2/stat/";
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID
/** return true if we have a channel that wants uplink/downlink
*/
std::string statusTopic = "/2/stat/"; // For "online"/"offline" message
std::string cryptTopic = "/2/e/"; // msh/2/e/CHANNELID/NODEID
std::string jsonTopic = "/2/json/"; // msh/2/json/CHANNELID/NODEID
std::string mapTopic = "/2/map/"; // For protobuf-encoded MapReport messages

// For map reporting (only applies when enabled)
uint32_t last_report_to_map = 0;
uint32_t map_position_precision = 32; // default to full precision
uint32_t map_publish_interval_secs = 60 * 15; // default to 15 minutes

/** return true if we have a channel that wants uplink/downlink or map reporting is enabled
*/
bool wantsLink() const;

/** Tell the server what subscriptions we want (based on channels.downlink_enabled)
Expand All @@ -102,6 +109,9 @@ class MQTT : private concurrency::OSThread
void publishStatus();
void publishQueuedMessages();

// Check if we should report unencrypted information about our node for consumption by a map
void perhapsReportToMap();

// returns true if this is a valid JSON envelope which we accept on downlink
bool isValidJsonEnvelope(JSONObject &json);

Expand Down

0 comments on commit eb372c1

Please sign in to comment.