mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-13 05:37:28 -04:00
fix(mqtt): tolerate object-typed payload in MQTT JSON messages (#6839)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3d843629b
commit
bd2863243b
2 files changed
+101
-1
No files matched your search
@@ -18,6 +18,10 @@ package org.meshtastic.core.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.JsonTransformingSerializer
|
||||
|
||||
@Serializable
|
||||
data class MqttJsonPayload(
|
||||
@@ -25,10 +29,17 @@ data class MqttJsonPayload(
|
||||
val from: Long,
|
||||
val to: Long? = null,
|
||||
val channel: Int? = null,
|
||||
val payload: String? = null,
|
||||
@Serializable(with = MqttPayloadStringSerializer::class) val payload: String? = null,
|
||||
@SerialName("hop_limit") val hopLimit: Int? = null,
|
||||
val id: Long? = null,
|
||||
val time: Long? = null,
|
||||
val sender: String? = null,
|
||||
// Add other fields as needed for position/telemetry
|
||||
)
|
||||
|
||||
// Firmware and MQTT bridges send "payload" as a string for text messages but as a nested JSON
|
||||
// object for position/telemetry/map reports; coerce non-strings to their compact JSON text.
|
||||
private object MqttPayloadStringSerializer : JsonTransformingSerializer<String>(String.serializer()) {
|
||||
override fun transformDeserialize(element: JsonElement): JsonElement =
|
||||
if (element is JsonPrimitive && element.isString) element else JsonPrimitive(element.toString())
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/** Locks down the tolerant "payload" decoding: string, nested object, and null/absent must all parse. */
|
||||
class MqttJsonPayloadTest {
|
||||
|
||||
// Matches the decode-relevant MQTTRepositoryImpl config.
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Test
|
||||
fun string_payload_decodes_unchanged() {
|
||||
val jsonStr =
|
||||
"""{"type":"text","from":12345678,"to":4294967295,"payload":"Hello World","hop_limit":3,"id":123}"""
|
||||
val decoded = json.decodeFromString<MqttJsonPayload>(jsonStr)
|
||||
|
||||
assertEquals("text", decoded.type)
|
||||
assertEquals(12345678L, decoded.from)
|
||||
assertEquals("Hello World", decoded.payload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun object_payload_decodes_to_compact_json_text() {
|
||||
val jsonStr =
|
||||
"""{"type":"position","from":12345678,"payload":{"latitude_i":123456789,"longitude_i":-987654321,"time":1600000000}}"""
|
||||
val decoded = json.decodeFromString<MqttJsonPayload>(jsonStr)
|
||||
|
||||
assertEquals("position", decoded.type)
|
||||
assertEquals("""{"latitude_i":123456789,"longitude_i":-987654321,"time":1600000000}""", decoded.payload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun null_payload_decodes_to_null() {
|
||||
val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1,"payload":null}""")
|
||||
assertNull(decoded.payload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun absent_payload_decodes_to_null() {
|
||||
val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1}""")
|
||||
assertNull(decoded.payload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun array_payload_decodes_to_compact_json_text() {
|
||||
val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1,"payload":[1,2]}""")
|
||||
assertEquals("[1,2]", decoded.payload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun non_string_primitive_payload_is_coerced_to_text() {
|
||||
val decoded = json.decodeFromString<MqttJsonPayload>("""{"type":"text","from":1,"payload":42}""")
|
||||
assertEquals("42", decoded.payload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun string_payload_round_trips_as_string() {
|
||||
val encoded =
|
||||
json.encodeToString(MqttJsonPayload.serializer(), MqttJsonPayload(type = "text", from = 1, payload = "hi"))
|
||||
assertTrue(encoded.contains(""""payload":"hi""""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun garbage_input_still_fails() {
|
||||
assertFailsWith<SerializationException> { json.decodeFromString<MqttJsonPayload>("""{"from":"not json""") }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user