From 3115bbe58daf0ebe104b3875b244f0da8944d32f Mon Sep 17 00:00:00 2001
From: James Rich <2199651+jamesarich@users.noreply.github.com>
Date: Tue, 3 Jun 2025 10:39:58 -0500
Subject: [PATCH] feat: add MQTT Map Reporting consent (#2006)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../compose/MapReportingPreferenceTest.kt | 109 ++++++++++++
.../components/PositionPrecisionPreference.kt | 6 +-
.../ui/common/components/SwitchPreference.kt | 87 ++++-----
.../components/MQTTConfigItemList.kt | 72 +++++---
.../components/MapReportingPreference.kt | 165 ++++++++++++++++++
app/src/main/res/values/strings.xml | 5 +
6 files changed, 360 insertions(+), 84 deletions(-)
create mode 100644 app/src/androidTest/java/com/geeksville/mesh/compose/MapReportingPreferenceTest.kt
create mode 100644 app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MapReportingPreference.kt
diff --git a/app/src/androidTest/java/com/geeksville/mesh/compose/MapReportingPreferenceTest.kt b/app/src/androidTest/java/com/geeksville/mesh/compose/MapReportingPreferenceTest.kt
new file mode 100644
index 000000000..343dcaebb
--- /dev/null
+++ b/app/src/androidTest/java/com/geeksville/mesh/compose/MapReportingPreferenceTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.compose
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.geeksville.mesh.R
+import com.geeksville.mesh.ui.radioconfig.components.MapReportingPreference
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MapReportingPreferenceTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private fun getString(id: Int): String =
+ InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
+
+ var mapReportingEnabled = false
+ var shouldReportLocation = false
+ var positionPrecision = 5
+ var positionReportingInterval = 60
+
+ var mapReportingEnabledChanged = { enabled: Boolean ->
+ mapReportingEnabled = enabled
+ }
+ var shouldReportLocationChanged = { enabled: Boolean ->
+ shouldReportLocation = enabled
+ }
+ var positionPrecisionChanged = { precision: Int ->
+ positionPrecision = precision
+ }
+ var positionReportingIntervalChanged = { interval: Int ->
+ positionReportingInterval = interval
+ }
+
+
+ private fun testMapReportingPreference() = composeTestRule.setContent {
+ Column {
+ MapReportingPreference(
+ mapReportingEnabled = mapReportingEnabled,
+ shouldReportLocation = shouldReportLocation,
+ positionPrecision = positionPrecision,
+ onMapReportingEnabledChanged = mapReportingEnabledChanged,
+ onShouldReportLocationChanged = shouldReportLocationChanged,
+ onPositionPrecisionChanged = positionPrecisionChanged,
+ publishIntervalSecs = positionReportingInterval,
+ onPublishIntervalSecsChanged = positionReportingIntervalChanged,
+ enabled = true,
+ focusManager = LocalFocusManager.current,
+ )
+ }
+ }
+
+ @Test
+ fun testMapReportingPreference_showsText() {
+ composeTestRule.apply {
+ testMapReportingPreference()
+ // Verify that the dialog title is displayed
+ onNodeWithText(getString(R.string.map_reporting)).assertIsDisplayed()
+ onNodeWithText(getString(R.string.map_reporting_summary)).assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun testMapReportingPreference_toggleMapReporting() {
+ composeTestRule.apply {
+ testMapReportingPreference()
+ onNodeWithText(getString(R.string.i_agree)).assertIsNotDisplayed()
+ onNodeWithText(getString(R.string.map_reporting)).performClick()
+ Assert.assertFalse(mapReportingEnabled)
+ Assert.assertFalse(shouldReportLocation)
+ onNodeWithText(getString(R.string.i_agree)).assertIsDisplayed()
+ onNodeWithText(getString(R.string.i_agree)).performClick()
+ Assert.assertTrue(shouldReportLocation)
+ Assert.assertTrue(mapReportingEnabled)
+ onNodeWithText(getString(R.string.map_reporting)).performClick()
+ onNodeWithText(getString(R.string.i_agree)).assertIsNotDisplayed()
+ Assert.assertTrue(shouldReportLocation)
+ Assert.assertFalse(mapReportingEnabled)
+ }
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt
index d1c23955e..547dd4bc4 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/PositionPrecisionPreference.kt
@@ -39,9 +39,9 @@ import kotlin.math.roundToInt
private const val PositionEnabled = 32
private const val PositionDisabled = 0
-const val PositionPrecisionMin = 10
-const val PositionPrecisionMax = 19
-const val PositionPrecisionDefault = 13
+const val PositionPrecisionMin = 12
+const val PositionPrecisionMax = 15
+const val PositionPrecisionDefault = 14
@Suppress("MagicNumber")
fun precisionBitsToMeters(bits: Int): Double = 23905787.925008 * 0.5.pow(bits.toDouble())
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/SwitchPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/SwitchPreference.kt
index e13f188e3..2c016c183 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/SwitchPreference.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/SwitchPreference.kt
@@ -17,18 +17,14 @@
package com.geeksville.mesh.ui.common.components
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.ListItem
-import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
@@ -36,78 +32,57 @@ import androidx.compose.ui.unit.dp
@Composable
fun SwitchPreference(
+ modifier: Modifier = Modifier,
title: String,
- summary: String,
+ summary: String = "",
checked: Boolean,
enabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
+ padding: PaddingValues? = null,
+ containerColor: Color? = null,
) {
- val color = if (enabled) {
- Color.Unspecified
- } else {
- MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
- }
-
ListItem(
- modifier = modifier,
+ colors = ListItemDefaults.colors().copy(
+ headlineColor = if (enabled) {
+ ListItemDefaults.colors().headlineColor
+ } else {
+ ListItemDefaults.colors().headlineColor.copy(alpha = 0.5f)
+ },
+ supportingTextColor = if (enabled) {
+ ListItemDefaults.colors().supportingTextColor
+ } else {
+ ListItemDefaults.colors().supportingTextColor.copy(alpha = 0.5f)
+ },
+ containerColor = containerColor ?: ListItemDefaults.colors().containerColor,
+ ),
+ modifier = (padding?.let { Modifier.padding(it) } ?: modifier).toggleable(
+ value = checked,
+ enabled = enabled,
+ onValueChange = onCheckedChange,
+ ),
trailingContent = {
Switch(
enabled = enabled,
checked = checked,
- onCheckedChange = onCheckedChange,
+ onCheckedChange = null,
)
},
supportingContent = {
- Text(
- text = summary,
- modifier = Modifier.padding(bottom = 16.dp),
- color = color,
- )
+ if (summary.isNotEmpty()) {
+ Text(
+ text = summary,
+ modifier = Modifier.padding(bottom = 16.dp),
+ )
+ }
},
headlineContent = {
Text(
text = title,
- color = color,
)
}
)
}
-@Composable
-fun SwitchPreference(
- title: String,
- checked: Boolean,
- enabled: Boolean,
- onCheckedChange: (Boolean) -> Unit,
- modifier: Modifier = Modifier,
- padding: PaddingValues = PaddingValues(horizontal = 16.dp),
-) {
- Row(
- modifier = modifier
- .fillMaxWidth()
- .size(48.dp)
- .padding(padding),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = title,
- style = MaterialTheme.typography.bodyLarge,
- color = if (enabled) {
- Color.Unspecified
- } else {
- MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
- },
- )
- Switch(
- enabled = enabled,
- checked = checked,
- onCheckedChange = onCheckedChange,
- )
- }
-}
-
@Preview(showBackground = true)
@Composable
private fun SwitchPreferencePreview() {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MQTTConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MQTTConfigItemList.kt
index 609fb8cac..f459331f9 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MQTTConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MQTTConfigItemList.kt
@@ -15,10 +15,12 @@
* along with this program. If not, see .
*/
+@file:Suppress("LongMethod")
+
package com.geeksville.mesh.ui.radioconfig.components
+import android.content.Context
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -29,12 +31,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
+import androidx.core.content.edit
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig.MQTTConfig
@@ -43,17 +46,20 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.ui.common.components.EditPasswordPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
-import com.geeksville.mesh.ui.common.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
+const val MapConsentPreferencesKey = "map_consent_preferences"
+
@Composable
fun MQTTConfigScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
) {
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
+ val destNode by viewModel.destNode.collectAsStateWithLifecycle()
+ val destNum = destNode?.num
if (state.responseState.isWaiting()) {
PacketResponseStateDialog(
@@ -63,6 +69,7 @@ fun MQTTConfigScreen(
}
MQTTConfigItemList(
+ nodeNum = destNum,
mqttConfig = state.moduleConfig.mqtt,
enabled = state.connected,
onSaveClicked = { mqttInput ->
@@ -74,12 +81,22 @@ fun MQTTConfigScreen(
@Composable
fun MQTTConfigItemList(
+ nodeNum: Int? = 0,
mqttConfig: MQTTConfig,
enabled: Boolean,
onSaveClicked: (MQTTConfig) -> Unit,
) {
val focusManager = LocalFocusManager.current
var mqttInput by rememberSaveable { mutableStateOf(mqttConfig) }
+ val sharedPrefs = LocalContext.current.getSharedPreferences(
+ MapConsentPreferencesKey, Context.MODE_PRIVATE
+ )
+ if (!mqttInput.mapReportSettings.shouldReportLocation) {
+ val settings = mqttInput.mapReportSettings.copy {
+ this.shouldReportLocation = sharedPrefs.getBoolean(nodeNum.toString(), false)
+ }
+ mqttInput = mqttInput.copy { mapReportSettings = settings }
+ }
LazyColumn(
modifier = Modifier.fillMaxSize()
@@ -191,40 +208,45 @@ fun MQTTConfigItemList(
)
}
item { HorizontalDivider() }
+ // mqtt map reporting opt in
+ item { PreferenceCategory(text = stringResource(R.string.map_reporting)) }
item {
- PositionPrecisionPreference(
- title = stringResource(R.string.map_reporting),
- enabled = enabled,
- value = mqttInput.mapReportSettings.positionPrecision,
- onValueChanged = {
- val settings = mqttInput.mapReportSettings.copy { positionPrecision = it }
+ MapReportingPreference(
+ mapReportingEnabled = mqttInput.mapReportingEnabled,
+ onMapReportingEnabledChanged = {
+ mqttInput = mqttInput.copy { mapReportingEnabled = it }
+ },
+ shouldReportLocation = mqttInput.mapReportSettings.shouldReportLocation,
+ onShouldReportLocationChanged = {
+ sharedPrefs.edit { putBoolean(nodeNum.toString(), it) }
+ val settings =
+ mqttInput.mapReportSettings.copy { this.shouldReportLocation = it }
mqttInput = mqttInput.copy {
- mapReportingEnabled = settings.positionPrecision > 0
mapReportSettings = settings
}
},
- modifier = Modifier.padding(horizontal = 16.dp)
+ positionPrecision = mqttInput.mapReportSettings.positionPrecision,
+ onPositionPrecisionChanged = {
+ val settings = mqttInput.mapReportSettings.copy { positionPrecision = it }
+ mqttInput = mqttInput.copy {
+ mapReportSettings = settings
+ }
+ },
+ enabled = enabled,
+ focusManager = focusManager
)
}
item { HorizontalDivider() }
item {
- EditTextPreference(
- title = stringResource(R.string.map_reporting_interval_seconds),
- value = mqttInput.mapReportSettings.publishIntervalSecs,
- enabled = enabled && mqttInput.mapReportingEnabled,
- keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
- onValueChanged = {
- val settings = mqttInput.mapReportSettings.copy { publishIntervalSecs = it }
- mqttInput = mqttInput.copy { mapReportSettings = settings }
- },
- )
- }
-
- item {
+ val consentValid = if (mqttInput.mapReportingEnabled) {
+ mqttInput.mapReportSettings.shouldReportLocation
+ } else {
+ true
+ }
PreferenceFooter(
- enabled = enabled && mqttInput != mqttConfig,
+ enabled = enabled && mqttInput != mqttConfig && consentValid,
onCancelClicked = {
focusManager.clearFocus()
mqttInput = mqttConfig
diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MapReportingPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MapReportingPreference.kt
new file mode 100644
index 000000000..b183d2130
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/MapReportingPreference.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.geeksville.mesh.ui.radioconfig.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.geeksville.mesh.R
+import com.geeksville.mesh.ui.common.components.EditTextPreference
+import com.geeksville.mesh.ui.common.components.PositionPrecisionMax
+import com.geeksville.mesh.ui.common.components.PositionPrecisionMin
+import com.geeksville.mesh.ui.common.components.SwitchPreference
+import com.geeksville.mesh.ui.common.components.precisionBitsToMeters
+import com.geeksville.mesh.util.DistanceUnit
+import com.geeksville.mesh.util.toDistanceString
+import kotlin.math.roundToInt
+
+@Suppress("LongMethod")
+@Composable
+fun MapReportingPreference(
+ mapReportingEnabled: Boolean = false,
+ onMapReportingEnabledChanged: (Boolean) -> Unit = {},
+ shouldReportLocation: Boolean = false,
+ onShouldReportLocationChanged: (Boolean) -> Unit = {},
+ positionPrecision: Int = 14,
+ onPositionPrecisionChanged: (Int) -> Unit = {},
+ publishIntervalSecs: Int = 3600,
+ onPublishIntervalSecsChanged: (Int) -> Unit = {},
+ enabled: Boolean,
+ focusManager: FocusManager
+) {
+ Column {
+ var showMapReportingWarning by rememberSaveable { mutableStateOf(mapReportingEnabled) }
+ LaunchedEffect(mapReportingEnabled) {
+ showMapReportingWarning = mapReportingEnabled
+ }
+ SwitchPreference(
+ title = stringResource(R.string.map_reporting),
+ summary = stringResource(R.string.map_reporting_summary),
+ checked = showMapReportingWarning,
+ enabled = enabled,
+ onCheckedChange = { checked ->
+ showMapReportingWarning = checked
+ if (checked && shouldReportLocation) {
+ onMapReportingEnabledChanged(true)
+ } else if (!checked) {
+ onMapReportingEnabledChanged(false)
+ }
+ }
+ )
+ AnimatedVisibility(showMapReportingWarning) {
+ Card(
+ modifier = Modifier.padding(16.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.map_reporting_consent_header),
+ modifier = Modifier.padding(16.dp),
+ )
+ HorizontalDivider()
+ Text(
+ stringResource(R.string.map_reporting_consent_text),
+ modifier = Modifier.padding(16.dp)
+ )
+
+ SwitchPreference(
+ title = stringResource(R.string.i_agree),
+ summary = stringResource(R.string.i_agree_to_share_my_location),
+ checked = shouldReportLocation,
+ enabled = enabled,
+ onCheckedChange = { checked ->
+ if (checked) {
+ onMapReportingEnabledChanged(true)
+ onShouldReportLocationChanged(true)
+ } else {
+ onShouldReportLocationChanged(false)
+ }
+ },
+ containerColor = CardDefaults.cardColors().containerColor,
+ )
+ if (shouldReportLocation && mapReportingEnabled) {
+ Slider(
+ modifier = Modifier.Companion.padding(horizontal = 16.dp),
+ value = positionPrecision.toFloat(),
+ onValueChange = { onPositionPrecisionChanged(it.roundToInt()) },
+ enabled = enabled,
+ valueRange = PositionPrecisionMin.toFloat()..PositionPrecisionMax.toFloat(),
+ steps = PositionPrecisionMax - PositionPrecisionMin - 1,
+ )
+ val precisionMeters = precisionBitsToMeters(positionPrecision).toInt()
+ val unit = DistanceUnit.Companion.getFromLocale()
+ Text(
+ text = precisionMeters.toDistanceString(unit),
+ modifier = Modifier.Companion.padding(
+ start = 16.dp,
+ end = 16.dp,
+ bottom = 16.dp
+ ),
+ fontSize = MaterialTheme.typography.bodyLarge.fontSize,
+ overflow = TextOverflow.Companion.Ellipsis,
+ maxLines = 1,
+ )
+ EditTextPreference(
+ modifier = Modifier.Companion.padding(bottom = 16.dp),
+ title = stringResource(R.string.map_reporting_interval_seconds),
+ value = publishIntervalSecs,
+ enabled = enabled,
+ keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
+ onValueChanged = onPublishIntervalSecsChanged,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun MapReportingPreview() {
+ val focusManager = LocalFocusManager.current
+ MapReportingPreference(
+ mapReportingEnabled = true,
+ onMapReportingEnabledChanged = {},
+ shouldReportLocation = true,
+ onShouldReportLocationChanged = {},
+ positionPrecision = 5,
+ onPositionPrecisionChanged = {},
+ enabled = true,
+ focusManager = focusManager
+ )
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9048bd7b9..59923c12a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -662,4 +662,9 @@
Nodes
Set your region
Reply
+ Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name.
+ Consent to Share Unencrypted Node Data via MQTT
+ By enabling this feature, you acknowledge and expressly consent to the transmission of your device’s real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions.
+ I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT
+ I agree.